探索黑客技术攻防,实战研究与安全创新

导航菜单

小议DPC(延迟过程调用)

操作系统实现使用的很多思想都可以在管理学中找到答案。比如你是某单位的办公室主任,有两个上司,分别是大Boss和二Boss。一天,大Boss找到你,“把这份文件改一改”,交代完注意事项,转身离开,你放下手头工作正在改,这时二BOSS来了,也要改一份文件,你当然放下手头文件,接待二BOSS,送走他后,你一般会先处理大BOSS的文件,再处理二BOSS的文件。如果大BOSS在交代注意事项时,二BOSS来了,那么他一般会等大BOSS讲完再说话。

以上的例子在我们日常生活中太平常了,道理也非常简单。实际上,隐含在DPC背后的原理也和上面的例子差不多,本质上是一样的。下面我们对号入座,大BOSS对应高级别的中断,二BOSS对应低级别的中断,而写文件对应硬件中断后操作系统应做的一些不太紧急的工作。上面的例子就对应高级别的中断产生(大BOSS来了),如果处理任务很多,占用时间长,那么就将紧急的任务(对应着注意事项)放在中断处理程序中,将不是很急的任务交给DPC去做,OK,退出中断服务程序,这时低级别的中断(二BOSS)就可以得以及时响应,低级别的中断也将一些不着急做的任务交给DPC完成,如果没有中断,将DPC排好队,就开始做DPC中的工作(对应着写文件),当然,这时也是允许中断发生的。

DPC的一些基础知识

DPC的全称是DeferredProcedureCall(延迟过程调用),顾名思义,就是推迟的过程调用。在IRQL中,排在最下面的倒数第三层,图一。


图片1.png

Windows操作系统中分为两种软件中断,DPC和APC,APC的实现与DPC类似,但要复杂很多,我将在以后的文章中讲解,目前还是将焦点指向DPC。通过图1可以看到:DPC处于硬件中断紧邻的下面一层,换句话说,没有硬件中断,就是老大喽;DPC是一个软件中断。所谓的硬件中断,就是需要CPU外围器件通过管脚上的电平变化告诉CPU的,而软件中断则是操作系统自己产生的。

DPC的一个最大特点就是,DPC是一个与处理器(CPU)相关联的内核对象,每个CPU都有一个DPC队列,在与CPU相关的结构_KPRCB中,可以看到大约20个带有DPC字样的成员。下面将_KPRCB结构中带DPC字样的成员列出,如果每个成员的内容都详细解释,显得繁琐,大家可能反而摸不清DPC的轮廓,本文将重点说明DpcData[2]和DpcStack成员,并以此为起点,顺藤摸瓜,整理出DPC的脉络。


typedefstruct_KPRCB
{
......
ULONGDpcTime;
ULONGDebugDpcTime;
......
ULONGAdjustDpcThreshold;
......
struct_KDPC_DATADpcData[2];//两个DPC请求队列
PVOIDDpcStack;
ULONGMaximumDpcQueueDepth;
ULONGDpcRequestRate;
ULONGMinimumDpcRate;
volatileUCHARDpcInterruptRequested;
volatileUCHARDpcThreadRequested;
volatileUCHARDpcRoutineActive;
volatileUCHARDpcThreadActive;
......
ULONGDpcLastCount;
......
PVOIDDpcThread;
KEVENTDpcEvent;
UCHARThreadDpcEnable;
......
LONGDpcSetEventRequest;
......
KDPCCallDpc;
......
}KPRCB,*PKPRCB;


首先注意DpcData[2]这个数组,该数组有两个成员,都是DpcData结构,下面是DpcData的结构代码:


typedefstruct_KDPC_DATA
{
LIST_ENTRYDpcListHead;//DPC队列头;
ULONGDpcLock;
//DPC队列锁,操作队列要先获得锁;
volatileULONGDpcQueueDepth;
ULONGDpcCount;
}KDPC_DATA,*PKDPC_DATA;


DpcData[2]这两个成员分别指向普通DPC队列和线程化DPC队列,如图2所示。

图片2.png

图2

普通的DPC可以在任何一个线程环境中运行,而线程化的DPC只能在一个专门的DPC线程中运行。线程化DPC是一个很有意思的DPC实现。在内核启动的阶段,系统会创建一个叫做DPC的线程,该线程虽然工作在passive层,但却有最高的线程优先级(Realtime级),因此,CPU一旦工作在passive级,最先执行的就是线程化DPC例程中的代码。下面我们的介绍将主要围绕普通的DPC展开,而对线程化DPC不做进一步的引申。另外值得一提的是,每个CPU都有一个专门的DPC堆栈,换句话说,当DPC对象中的例程得以执行时,其使用的堆栈并不在任何一个线程里,而是使用每个CPU一个的DPC堆栈。原因很简单,中断服务程序一般较小,它使用当前线程的系统空间堆栈,不会发生溢出,但是DPC执行例程一般较大,如果使用当前线程的堆栈可能会溢出,所以使用专门的DPC堆栈。

DPC的实现

DPC与其后要介绍的APC在实现上很有类似之处,两者最重要的行为就是构造、排队(Queue)和投递(Deliver)。所谓构造就是生成对应的DPC或者APC对象体,排队就是将DPC或者APC对象放到队列中去,而投递就是在某些固定的条件下,执行DPC或者APC对象中的例程。如图3所示。

图片3.png

1)构造DPC对象


DPC对象是Windows内核对象之一,其结构为:
Typedefstruct_KDPC
{
UCHARType;//我是DpcObject或者ThreadedDpcObject类型哦!
UCHARImportance;//分为High,medium,low三种。默认是medium
USHORTNumber;//在哪个CPU上呢?对应着多个CPU或者多核的电脑;
LIST_ENTRYDpcListEntry;//每个CPU上的DPC是手拉手一串滴!
PKDEFERRED_ROUTINEDeferredRoutine;//具体的DPC函数;
PVOIDDeferredContext;//执行DPC时的上下文,由DPC函数解释;
PVOIDSystemArgument1;//执行DPC时的参数,由DPC函数解释;
PVOIDSystemArgument2;//执行DPC时的参数,由DPC函数解释;
VolatilePVOIDDpcData;//指回_KPRCB结构中的DpcData成员!
}KDPCDPC的Importance值不同,其被执行的次序也不相同


图片4.png

很明显,当DPC的Importance值为Low时,只有在某些特定条件下,比如DPC队列中人满为患,或者使用率门可罗雀了,就给个机会执行吧,而HIGH级对应大BOSS,肯定是会被优先执行的,因此放在队列头,这点和我们前面提到的例子也是吻合的,大BOSS即使后来,也会被优先服务。而Medium级对应着二BOSS,按照先来后到的顺序,放在队列尾。在《WindowsInternal》这本书里,上表中还包括DPC投递的目标是另外一个CPU的情况,在这里为了保持讲解的简洁性,就不列出了。

2)DPC队列的排队(Queue)

DPC的使用比较规范,一般会有下面几个步骤:DPC对象的建立、KeInitializeDPC(初始化DPC对象)和KeInsertQueueDPC(插入DPC队列)。下面对这几条语句分别进行描述。

①使用ExAllocatePoolWithTag,在非换页区分配一段DPC结构大小的内存

ExAllocatePoolWithTag(NonPagedPool,sizeof(KDPC),“DPCwodi”);

②用KeInitializeDpc初始化DPC对象,里面要指定一个与DPC对象关联的DPC例程。

VOIDKeInitializeDpc(INPRKDPCDpc,INPKDEFERRED_ROUTINEDeferredRoutine,INPVOIDDeferredContext);

这个函数的功能非常简单,就是将传入的DPC对象进行初始化,Type设为DPCType,number设为0,Importance设为Medium,DeferredRoutine与DeferredContext都设为传入的参数,DpcData设为NULL。

③KeInsertQueueDPC(插入DPC队列),通过字面的意思就可以猜到,是将初始化后的DPC对象插入DPC队列等待执行。

BOOLEANKeInsertQueueDPC(INPRKDPCDpc,INPVOIDDeferredRoutine,INPVOIDDeferredContext)

3)DPC队列的投递(Deliver)

在教科书中,我们经常会看到DPC对象的投递,投递实际上就是这个DPC对象中的DPC例程的执行。通常,将一个DPC放入DPC队列,内核会请求一个在Dispatch/DPC级的软件中断,注意,此时内核工作在较高的IRQL级上,即硬件中断级别的IRQL上,因此这个软件中断并不会得到马上响应,当内核降低IRQL到APC级或低于APC级时,如果发现有DPC中断存在,会将IRQL保持在Dispatch/DPC级,并将DPC队列中的对象投递。此时DPC例程在执行的时候,有可能在任何一个进程的运行环境中,因此通常DPC例程中不会涉及应用层地址空间。

在具体实现上,KfLowerIrql函数调用HalpLowerIrql函数,而HalpLowerIrql函数中判断,如果NewIrql低于DISPATCH_LEVEL时,会调用KiDispatchInterrupt函数。VOIDHalpLowerIrql(KIRQLNewIrql)


{
„„„
If(NewIrql=DISPATCH_LEVEL)
{
KeGetPcr()-Irql=NewIrql;
Return;
}
//如果NewIrql低于DISPATCH_LEVELKeGetPcr()-Irql=DISPATCH_LEVEL;
If(((PKIPCR)KeGetPcr())-;HalReserved[HAL_DPC_REQUEST]){//有DPC级别的软件中断
((PKIPCR)KeGetPcr())-;HalReserved[HAL_DPC_REQUEST]=FALSE;KiDispatchInterrupt();
}
„„„
}


KiDispatchInterrut函数是一段汇编语言函数,主要是将当前的堆栈切换到DPCSTACK后,调用KiRetireDpcList函数。KiRetireDpcList函数的调用界面为:VOIDFASTCALLKiRetireDpcList(INPKPRCBPrcb);KiRetireDpcList函数的参数是一个指向处理器控制块的指针,其伪代码如下:


do
{
处理定时器;
While(DPC队列非空)
{
获得DPC链表的自旋锁;
从链表中移除一个DPC对象;
开中断;
执行DPC对象中的例程;
关中断;
}
}while(DPC队列非空)


注意,在对DPC队列操作时,系统处于关中断状态,而在DPC对象中的例程执行过程中,系统处于开中断状态,因此,也就不难理解为什么使用两层while循环,因为在执行DPC例程的过程中系统处于开中断状态,此时有可能会再次产生中断,在DPC队列中加入新的DPC对象。

4)DPC使用的整体流程

DPC的使用流程参见图4,按照这个流程走一遍,对DPC的整体就会有一个更加清楚的了解了。

图片5.png

图4

①事情的缘起是设备中断产生的;

②按照正常的程序,进入中断服务程序;

③完成紧急的部分后,DPC出场,将不紧迫的代码放于DPC队列中;

④中断程序返回,正常运行;

⑤地雷已经埋下,什么时候触发呢?当CPU的IRQL降到低于DPC级别时,注意是低于,系统保持IRQL在DPC级别,好戏开场;

⑥依次执行DPC队列中的DPC对象;

⑦我们的代码得到执行。

当然,操作系统的故事还没有结束,当DPC队列为空时,降低IRQL的级别,让正常的线程执行过程继续进行。

小结

以上我们对DPC的使用进行了说明,DPC的使用只要把握住构造、初始化、投递三部曲即可,而在DPC的实现上,本质就是一个对队列的操作。相对DPC,与线程相关的APC(异步过程调用)就要复杂很多了。