首页
论坛
课程
招聘
雪    币: 439
活跃值: 活跃值 (17)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝

[原创]DPC机制分析记录

2018-4-24 16:21 5097

[原创]DPC机制分析记录

2018-4-24 16:21
5097

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

APC主要的使用场景

  1. windows内部异步IO
  2. APC注入,但是限制条件太多,感觉不实用
  3. 通过在APC的KernelRouting中回调PspExitThread,达到结束当前APC线程的目的,由于结束进程其实就是把进程的所有线程都结束掉,故可用来结束进程。
    源码如下:
    PspTerminateThreadByPointer;
    KeInitializeApc (ExitApc,
                    PsGetKernelThread (Thread),
                    OriginalApcEnvironment,
                    PsExitSpecialApc,//里面调用了PspExitThread用于结束线程
                    PspExitApcRundown,
                    PspExitNormalApc,
                    KernelMode,
                    ULongToPtr (ExitStatus));
    

DPC(Delayed Procedure Call)的创建

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

首先看一下DPC相关的数据结构把,实际在内核系统中,各种3环的抽象概念基本都是有对应的数据结构可以追溯的。
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;

KiInitializeDpc分析:

     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队列

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

KeInsertQueueDpc分析

首先明确一点,如果当前DPC已经在DPC queue中,那么实际上什么都不做。
//首先进行参数检查
 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;
    }
紧接着获取_KDPC_DATA的指针
 

DpcData = KiSelectDpcData(TargetPrcb, Dpc);

 

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

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

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

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

 

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

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的。

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

 

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

DPC的执行

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

  • 在系统初始化的时候KeInitSystem,如果threaded DPCs是启用的,那么将为每个CPU核心创建一个DPC线程,
    初始化DPC_THREADED的DPC链表头,
    初始化CPU的DpcEvent事件为同步事件,且初始化为无信号状态,
    初始化_KDPC_DATA的DpcQueueDepth==0
 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);//参数

  • KiExecuteDpc的具体分析,这里是执行DPC_THREADED,也就是线程化DPC的地点。
//线程优先级最高
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);

  • KiRetireDpcList,是由KiDispatchInterrupt调用
    KiRetireDpcList中基本流程和KiExecuteDpc的一样,注意以下几点:
    1. 这里面处理的是Prcb->DpcData[DPC_NORMAL]中的普通DPC
    2. Prcb->DpcRoutineActive = TRUE;//和前面插入部分对应
    3. 如果Prcb->TimerRequest != 0,就要处理定时器的请求
      KiTimerExpiration
    4. DeferredRoutine执行的时候是DISPATCH_LEVEL
    5. 最后设置标志:
      Prcb->DpcRoutineActive = FALSE;
      Prcb->DpcInterruptRequested = FALSE;

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

DPC的应用

  1. 在内核hook高频函数的时候,用于挂起其它CPU
  2. KeSetTimer定时器
  3. windows内核里面的一些硬件IO相关的ISR(不太清楚)

DPC和APC区别

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

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

HWS计划·2020安全精英夏令营来了!我们在华为松山湖欧洲小镇等你

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