首页
社区
课程
招聘
[原创]DPC机制分析记录
发表于: 2018-4-24 16:21 9985

[原创]DPC机制分析记录

2018-4-24 16:21
9985

APC机制分析中是主要分析了APC的执行流程,初始化、插入和派发,这里主要分析一下DPC的相关机制。
在此之前总结一下APC主要的应用点,由于初学不久,很多不全的请补充分享一下,毕竟对大神可能很简单的,对我们小白其实根本看不到路,甚至不知道有这条路,谢谢。

在用户层没有可以对DPC进行相关操作的函数,直接从0环开始分析。
KiInitializeDpc用于初始化DPC对象,
该函数只被KeInitializeDpcKeInitializeThreadedDpc所调用,这个和DPC的类型有关,下面会分析到。

以上就是初始的全部内容,十分简单。
插入DPC在KeInsertQueueDpc中实现。

DpcData = KiSelectDpcData(TargetPrcb, Dpc);

KiSelectDpcData内部的获取逻辑也很简单

如果Dpc.Type==ThreadedDpcObject并且CPU的环境块的TargetPrcb->ThreadDpcEnable==TRUE,那么返回 Prcb->DpcData[DPC_THREADED]

一般的Dpc就返回 Prcb->DpcData[DPC_NORMAL]

那么经过上面的步骤,已经获取了当前核心的_KPDC_DATA数据对象DpcData

如果 Dpc->DpcData是NULL,说明DPC object不在Dpc Queue中,
那么增加当前核心DpcData的DpcCount+=1,增加DpcQueueDepth+=1,
设置Dpc对象的参数1和参数2。

如果Dpc->DpcData不是NULL,就什么都不做,因为它已经在DPC队列中了。

上面DPC已经插入到了当前CPU的DPC队列或者指定CPU的DPC队列中。

这部分在Wrk源码和ReactOS源码上查找到的引用有些出入,错误之处希望指正。

最后注意,DPC不会受线程切换影响,因为线程切换也是在DISPATCH_LEVEL,所以无法中断当前DPC的执行。

APC是线程相关的,每个线程有一个APC队列;
而DPC是CPU核心相关的,每个CPU核心有一个DPC队列。

APC级别可以访问分页内存,DPC级别是不能访问分页内存的
(在同一处理器上,线程只能被更高级别IRQL的线程所中断。每个处理器都有自己的中断IRQL。)

 
 
 
 
PspTerminateThreadByPointer;
KeInitializeApc (ExitApc,
                PsGetKernelThread (Thread),
                OriginalApcEnvironment,
                PsExitSpecialApc,//里面调用了PspExitThread用于结束线程
                PspExitApcRundown,
                PspExitNormalApc,
                KernelMode,
                ULongToPtr (ExitStatus));
typedef struct _KDPC_DATA {
    LIST_ENTRY DpcListHead;
    KSPIN_LOCK DpcLock;
    volatile ULONG DpcQueueDepth; //深度
    ULONG DpcCount;
} KDPC_DATA, *PKDPC_DATA;
typedef struct _KDPC {
    UCHAR Type; //DPC类型(分为普通DPC和线程化DPC)
    UCHAR Importance; //该DPC的重要性,将决定挂在队列头还是尾
    UCHAR Number; //第5位为0就表示当前cpu,否则,最低4位表示目标cpu号
    UCHAR Expedite; 
    LIST_ENTRY DpcListEntry; //用来挂入dpc链表,和_KDPC_DATA中的DpcListHead对应,
                             //内核中很多这样的结构安排

    PKDEFERRED_ROUTINE DeferredRoutine; //dpc函数
    PVOID DeferredContext; //dpc函数的参数
    PVOID SystemArgument1;
    PVOID SystemArgument2;
    PVOID DpcData;
} KDPC, *PKDPC, *PRKDPC;
     Dpc->Type = (UCHAR)DpcType;//和ApcObject一样,属于_KOBJECTS枚举,分为DpcObject和ThreadedDpcObject(线程化的DPC对象)
    Dpc->Importance = MediumImportance; //重要性决定了DPC挂在队列的顺序
    Dpc->Number = 0; //初始的目标cpu为当前cpu
    Dpc->Expedite = 0;

    Dpc->DeferredRoutine = DeferredRoutine; //函数地址
    Dpc->DeferredContext = DeferredContext; //函数参数
    Dpc->DpcData = NULL; //表示该DPC尚未挂入任何DPC队列
//首先进行参数检查
 if (Dpc->Number >= MAXIMUM_PROCESSORS/*32*/) {
         //修正Number
        Number = Dpc->Number - MAXIMUM_PROCESSORS;
        //KiProcessorBlock是PKPRCB的数组,
        //这里取出DPC->Number对应的_KPRCB是因为DPC是CPU核心相关
        TargetPrcb = KiProcessorBlock[Number];

    } else {
        //参数不需要修正的分支,就是当前CPU的PRCB
        Number = CurrentPrcb->Number;
        TargetPrcb = CurrentPrcb;
    }
1. 如果Dpc->Importance == HighImportance,那么把DPC挂载在Dpc队列的头部,`InsertHeadList(&DpcData->DpcListHead, &Dpc->DpcListEntry);`,DPC队列就是由的CPU环境块中的dpcData进行维护。
2. 否则把DPC插入核心DPC队列的尾部

----------------------------------------

1. 如果DPC是线程化的DPC对象 DpcData == &TargetPrcb->DpcData[DPC_THREADED],且
`TargetPrcb->DpcThreadActive == FALSE&&
TargetPrcb->DpcThreadRequested == FALSE`,也就是说Dpc线程不是活动状态,因为执行Dpc的时候会设置TargetPrcb->DpcThreadActive == TRUE。这种情况下设置如下PRCB的标志:
TargetPrcb->DpcSetEventRequest=TRUE;//设置Prcb->DpcEvent有信号
TargetPrcb->DpcThreadRequested = TRUE;
TargetPrcb->QuantumEnd = TRUE;
也就是要把线程唤醒。

2. 如果DPC指定的不是当前CPU的PRCB,那么做一些检测以判断是否需要线程唤醒。

-------------------------------------------------------

1.如果DPC是一般的DPC对象,且
`TargetPrcb->DpcRoutineActive == FALSE&&
TargetPrcb->DpcInterruptRequested == FALSE`的情况下,
然后判断只要Dpc的Importance不是LowImportance或者
DpcData->DpcQueueDepth已经超过了PRCB的MaximumDpcQueueDepth(最大Queue深度)
获取TargetPrcb->DpcRequestRate小于TargetPrcb->MinimumDpcRate(最小请求率),
那么就会请求当前CPU的dispatch 中断,
且设置 TargetPrcb->DpcInterruptRequested=TRUE。

---------------------------------------------

那么经过上面的判断检验,需要dispatch 中断的都会执行
` KiRequestSoftwareInterrupt(DISPATCH_LEVEL);`,
这个函数在APC分析中也看到了,只不过那里的参数是APC_LEVEL。
这是由于APC是运行在APC_LEVEL,而DPC是运行在DISPATCH_LEVEL的。
 if (KeThreadDpcEnable != FALSE);//threaded DPCs是启用的
KeInitializeEvent(&Prcb->DpcEvent, SynchronizationEvent, FALSE);
InitializeListHead(&Prcb->DpcData[DPC_THREADED].DpcListHead);
KeInitializeSpinLock(&Prcb->DpcData[DPC_THREADED].DpcLock);
Prcb->DpcData[DPC_THREADED].DpcQueueDepth = 0;

Status = PsCreateSystemThread(&Handle,
                              THREAD_ALL_ACCESS,
                              &ObjectAttributes,
                              NULL,
                              NULL,
                              KiExecuteDpc,//注意这里
                              Prcb);//参数
//线程优先级最高
KeSetPriorityThread(Thread, HIGH_PRIORITY);
//启用线程化Dpc,前面KiSelectDpcData获取线程化的KDPC_DATA
//就会检查该字段是否为TRUE
Prcb->ThreadDpcEnable = TRUE;
//取出链表头,注意是取得DPC_THREADED中的
 ListHead = &Prcb->DpcData[DPC_THREADED].DpcListHead;


 do{
 //设置为TRUE,和前面对应
 Prcb->DpcThreadActive = TRUE;

 //调用DPC回调函数(此时的IRQL==PASSIVE_LEVEL)
 (DeferredRoutine)(Dpc,
                     DeferredContext,
                     SystemArgument1,
                    SystemArgument2);
 ...........
 //深度来判断DPC是否为空
 }while(Prcb->DpcData[DPC_THREADED].DpcQueueDepth != 0);

 //设置标志,和前面相对应
  Prcb->DpcThreadActive = FALSE;
  Prcb->DpcThreadRequested = FALSE;

  //最后判断如果线程华DPC list是空的,那么
  KeWaitForSingleObject(&Prcb->DpcEvent, //等待该事件,和Prcb->DpcSetEventRequest标志有关,在KiQuantumEnd中让事件有信号
                          Suspended,               
                        KernelMode, 
                        FALSE,
                        NULL);

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 1
支持
分享
最新回复 (11)
雪    币: 177
活跃值: (28)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
分析的很不错
2018-6-26 20:32
0
雪    币: 177
活跃值: (28)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
被挂起的线程 收到DPC事件后  会被唤醒?
2018-6-26 20:48
0
雪    币: 1202
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
应该是用户APC吧
2018-6-26 23:31
0
雪    币: 474
活跃值: (232)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
5
兔oO 被挂起的线程 收到DPC事件后 会被唤醒?
我也不确定是不是立刻唤醒,但是根据源码确实会经过一系列判断后唤醒线程,一般来说DPC在ISR后面降低IRQL级别的时候会去扫描时候有DPC队列,如果有就会执行。触发的具体语句就是KiRequestSoftwareInterrupt这一句。不对的地方请指正。
2018-7-4 11:03
0
雪    币: 474
活跃值: (232)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
6
wx_尚煜 应该是用户APC吧
DPC啊,不是用户APC吧
2018-7-4 11:03
0
雪    币: 177
活跃值: (28)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
cat喵 我也不确定是不是立刻唤醒,但是根据源码确实会经过一系列判断后唤醒线程,一般来说DPC在ISR后面降低IRQL级别的时候会去扫描时候有DPC队列,如果有就会执行。触发的具体语句就是KiRequestSo ...
貌似不对 ~当前线程处于挂起状态  ~并不存在线程的调度列队  所有不会被切换  按道理线程只有被切换的时候才会执行DPC
2018-7-6 09:59
0
雪    币: 368
活跃值: (431)
能力值: ( LV2,RANK:140 )
在线值:
发帖
回帖
粉丝
8
dpc和cpu相关
apc和线程相关
===
讨论dpc和进程、线程的关联不大,甚至没什么关联
最后于 2018-7-6 11:11 被又出bug了编辑 ,原因:
2018-7-6 11:11
0
雪    币: 474
活跃值: (232)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
9
兔oO 貌似不对 ~当前线程处于挂起状态 ~并不存在线程的调度列队 所有不会被切换 按道理线程只有被切换的时候才会执行DPC
enen,不过我认为DPC执行的时机应该是和线程切换没关系的,是在IRQL请求级别降低到DISPATCH_LEVEL以下的 时候执行的,APC才和线程切换有关系,这一点应该是没错的。然后我上面确实有写唤醒线程,但是确实是表述有误,估计我当时也理解有错吧。具体可参看KiQuantumEnd函数中有相关的线程切换的逻辑, 当我再次去寻找的时候发现这个函数在KiDispatchInterrupt被调用,在KiDispatchInterrupt函数中会用KiRetireDpcList函数首先处理当前核心的DPC队列,最后可能会发生线程切换。不过如@又出bug了 老哥说的一样,感觉上讨论DPC和线程的关系意义似乎不大,我乱写的锅。所以总结上来看在执行完成DPC队列后会尝试切换线程,具体是否切换线程需要具体判断,而我写的线程唤醒是有问题的,我上面说的关于KiDispatchInterrupt只是一部分的逻辑,是有分支的,在分支中也有线程切换,具体请自行翻阅一下。
2018-7-11 14:40
0
雪    币: 474
活跃值: (232)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
10
又出bug了 dpc和cpu相关apc和线程相关===讨论dpc和进程、线程的关联不大,甚至没什么关联
谢谢老哥指点,我觉得你说的这个确实短小精悍是核心
2018-7-11 14:41
0
雪    币: 177
活跃值: (28)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
每当降低当前IRQL的时候也会执行DPC
2018-7-30 10:24
0
雪    币: 4
活跃值: (369)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
cat喵 enen,不过我认为DPC执行的时机应该是和线程切换没关系的,是在IRQL请求级别降低到DISPATCH_LEVEL以下的 时候执行的,APC才和线程切换有关系,这一点应该是没错的。然后我上面确实有写 ...
老哥,你这看的IRQL级别降低的时候执行DPC,有到windows中去验证过么?
2020-6-30 21:49
0
游客
登录 | 注册 方可回帖
返回
//