<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

文章分类

导航

订阅

戏说IRQL(3)

在这个信息爆炸的时代,为何还要看书呢?举个例子吧,前几日翻看案头的旧书时看到这么一句话:

IRQL avoids destructive preemption on a single CPU, while spin locks forestall interference among CPUs.

(IRQL用于避免在单个CPU上发生有害的抢占,而自旋锁可以阻止多个CPU的相互干扰。)

多么精炼的概括啊,语出Walter Oney的Programming WDM,这是几乎所有写Windows驱动程序的人都看的一本书。

书接上回,继续讲IRQL中的1,2两个级别,也就是APC和DPC。它们的全称分别是异步过程调用(asynchronous procedure call)和延迟过程调用(deferred procedure call)。 两个名字中都包含了PC(过程调用),这代表了它们的一个共同特征,那就是做一种特殊的函数调用。进一步说,就是先把要调用的函数和参数插入到一个队列中,等待时机成熟时再执行。

以DPC为例,通常是硬件设备的中断处理函数在较高的IRQL级别调用KeInsertQueueDpc增加一个DPC调用,然后就宣布中断处理结束让IRQL降下来,防止CPU在高IRQL上停留时间太长,没有机会处理低IRQL的中断。

APC的情况更复杂些,可以在内核态注册APC调用,也可以在用户态注册,比如调用下面的API就可以在应用程序中增加一个APC调用:

DWORD WINAPI QueueUserAPC(
  _In_  PAPCFUNC pfnAPC,
  _In_  HANDLE hThread,
  _In_  ULONG_PTR dwData
);

除了APC可以在用户态增加外,DPC和APC的另一个区别就是队列的组织方式有很大不同。DPC队列是CPU相关的,每个CPU有一个DPC队列,比如,下面是一个单CPU的系统发生系统挂死时,系统中唯一一个CPU的DPC队列:

           DpcQueue:  0x80552380 0x804ff5b8 [Normal] nt!KiTimerExpiration
                      0x8abcfed4 0xba7843e8 [Normal] ACPI!ACPIInterruptServiceRoutineDPC
                      0x8a6560cc 0xba50fdf0 [Normal] NDIS!ndisMDpcX
                      0x8a899eec 0xbaa28650 [Normal] i8042prt!I8042KeyboardIsrDpc
                      0x8a899e90 0xbaa2b0ef [Normal] i8042prt!I8xKeyboardSysButtonEventDpc
                      0x8a899dd0 0xbaa2a163 [Normal] i8042prt!I8042ErrorLogDpc
                      0xb87ffa4c 0xb87e8020 [Normal] SynTP
                      0xb87ffa24 0xb87e7f60 [Normal] SynTP          

进一步讲,描述DPC队列的信息记录在每个CPU的处理器控制块上:

kd> dt _KPRCB ffdff120 -y DPC
nt!_KPRCB
   +0x4b0 DpcTime : 0x4f40
   +0x860 DpcListHead : _LIST_ENTRY [ 0x80552384 - 0xb87ffa28 ]
   +0x868 DpcStack : 0xbacd8000 Void
   +0x86c DpcCount : 0x256ae9
   +0x870 DpcQueueDepth : 8
   +0x874 DpcRoutineActive : 0x80549244
   +0x878 DpcInterruptRequested : 0
   +0x87c DpcLastCount : 0x256ae9
   +0x880 DpcRequestRate : 0
   +0x8a0 DpcLock : 0

而APC队列是线程相关的,也就是每个线程都可以有一个APC队列。比如,执行扩展命令!apc便可以逐一检查每个进程的每个线程的APC队列,如果发现有APC的话,就会列出来:

kd> !apc
*** Enumerating APCs in all processes
Process 8abf2490 System
...

Process 897a4020 ICSRV.EXE
Process 897a3da0 inetinfo.exe
    Thread 883feb80 ApcStateIndex 0 ApcListHead 883febbc [USER]
        KAPC @ 896edeb0
          Type           12
          KernelRoutine  80573206 nt!IopUserCompletion+0
          RundownRoutine 80573220 nt!IopUserRundown+0

...

APC的记录方式与DPC也有所不同,因为APC有来自内核态的,又有来自用户态的,所以在KTHREAD中,有个KAPC_STATE结构,然后在这个结构体里有两个队列,一个是用户态的,一个是内核态的。

kd> dt _KAPC_STATE
nt!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY
   +0x010 Process          : Ptr32 _KPROCESS
   +0x014 KernelApcInProgress : UChar
   +0x015 KernelApcPending : UChar
   +0x016 UserApcPending   : UCharv

观察上面的iis进程:

kd> dt _KAPC_STATE 883feb80+34
nt!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY [ 0x883febb4 - 0x883febb4 ]
   +0x010 Process          : 0x897a3da0 _KPROCESS
   +0x014 KernelApcInProgress : 0 ''
   +0x015 KernelApcPending : 0 ''
   +0x016 UserApcPending   : 0 ''

此外,KTHREAD结构还有一些与APC右关的字段:

kd> dt _KTHREAD -y APC 883feb80 -r
nt!_KTHREAD
   +0x034 ApcState : _KAPC_STATE
   +0x0e8 ApcQueueLock : 0
   +0x138 ApcStatePointer : [2] 0x883febb4 _KAPC_STATE
   +0x165 ApcStateIndex : 0 ''
   +0x166 ApcQueueable : 0x1 ''

观察了这些数据结构之后,我们应该可以对DPC和APC的“藏身之处”有了个比较深刻的印象。接下来的问题就是如何执行这些保存在DPC队列或者APC队列中的过程呢?简单说是通过所谓的软中断机制,但这个软中断不是像int 13那样基于IDT表的,而是基于一张纯软件的表,位于HAL模块中的SWInterruptHandlerTable全局变量记录着这样表:

kd> dds hal!SWInterruptHandlerTable

8301a0a4  8301310e hal!KiUnexpectedInterrupt

8301a0a8  83018510 hal!HalpApcInterrupt

8301a0ac  83018374 hal!HalpDispatchInterrupt

8301a0b0  8301310e hal!KiUnexpectedInterrupt

8301a0b4  830185aa hal!HalpApcInterrupt2ndEntry

8301a0b8  8301840e hal!HalpDispatchInterrupt2ndEntry

简单点说,在增加DPC或者APC时,增加函数(KiInsertQueueApcv)会请求软件中断:

#if defined(NT_UP)

#define KiRequestApcInterrupt(Processor) KiRequestSoftwareInterrupt(APC_LEVEL)

#else

#define KiRequestApcInterrupt(Processor)                  \
    if (KeGetCurrentProcessorNumber() == Processor) {     \
        KiRequestSoftwareInterrupt(APC_LEVEL);            \
    } else {                                              \
        KiSendSoftwareInterrupt(AFFINITY_MASK(Processor), APC_LEVEL);     \
    }

#endif

#if defined(NT_UP)

#define KiRequestDispatchInterrupt(Processor)

#else

#define KiRequestDispatchInterrupt(Processor)             \
    if (KeGetCurrentProcessorNumber() != Processor) {     \
        KiSendSoftwareInterrupt(AFFINITY_MASK(Processor), DISPATCH_LEVEL);     \
    }

#endif

当IRQL降至DPC或者APC时,CPU就会检查是否有DPC/APC需要执行,详细过程,下一讲通过实验继续讲。

 

posted on 2013年9月23日 20:41 由 Raymond

# re: 戏说IRQL(3) @ 2013年10月2日 13:53

期待您的下一讲

zou

# re: 戏说IRQL(3) @ 2013年10月2日 14:01

您太牛了,要达到您那水平估计得先学好外语

zou

# re: 戏说IRQL(3) @ 2013年10月16日 15:03

老师没坚持下来啊???

kkindof

# re: 戏说IRQL(3) @ 2013年10月18日 12:20

最近在国外出差,要做的事太多,没时间写博客,请等等

Raymond

# re: 戏说IRQL(3) @ 2013年11月10日 14:25

老师您好,为什么 windbg不支持v86模式程序调试呢

zou1

# re: 戏说IRQL(3) @ 2013年12月1日 11:34

不知不觉,过去1个半月了,看来张老师忘记写完了- -

kkindof

# re: 戏说IRQL(3) @ 2014年2月11日 11:19

半年真的很快。。。

kkindof

# re: 戏说IRQL(3) @ 2014年9月19日 0:57

期待老师写新博客

cpluse

Powered by Community Server Powered by CnForums.Net