Windows XP 中断与异常(段内容补充一)

ʕ •̀ o •́ ʔ

1 线程优先级

本节内容是对《Windows内核原理与实现3.5.1 P150》及《Windows内核情景分析 5.10 P409》关于线程优先级的内容整理。

Windows 内核基本上是严格按优先级高低调度的。Windows 内核将线程的优先级分成 32 级,分别对应着 32 个就绪线程队列。其中以 0 级为最低,31 级为最高,16 级以上用于实时线程,16 级以下则用于普通线程。

1.1 线程优先级

线程优先级:0-31,共32个等级:

优先级类别 线程优先级 描述
实时类别 16~31
动态类别 1~15 动态优先级,运行在动态优先级类别中的线程,它们的实际优先级可能会根据特定的情形而作调,它往往是在 BasePriority 域的基础上提升一定的优先级增量。但是,无论怎么调整,Priority域值的范围一直是1~15
系统类别 0 Passive Level
1
2
3
4
5
6
7
8
9
#define LOW_PRIORITY 0              // Lowest thread priority level
#define LOW_REALTIME_PRIORITY 16 // Lowest realtime priority level
#define HIGH_PRIORITY 31 // Highest thread priority level
#define MAXIMUM_PRIORITY 32 // Number of thread priority levels

#define MAXIMUM_WAIT_OBJECTS 64 // Maximum number of wait objects

#define MAXCHAR 255
#define MAXIMUM_SUSPEND_COUNT MAXCHAR // Maximum times thread can be suspended

KTHREAD优先级的描述:

  • BasePriority静态优先级(基本优先级),继承至进程的BasePriority
  • Priority动态优先级,运行在动态优先级类别中的线程,它们的实际优先级可能会根据特定的情形而作调,它往往是在 BasePriority 域的基础上提升一定的优先级增量(使用函数KeSetPriorityAndQuantumProcessKeSetBasePriorityThread)。但是,无论怎么调整,Priority域值的范围一直是1~15

1.2 线程优先级调整

优先级提升应该发生在该线程被作出调度决定(即插入到对应优先级的调度链表中)以前。内核提供的带Increment参数的函数包括:

  • KeInsertQueueApc
  • KePulseEvent
  • KeSetEvent
  • KeReleaseMutant
  • KeReleaseSemaphore
  • KeSetProcess
  • KeBoostProirityThread
  • KeTerminateThread.

⚠️注意:如果一个线程已经被挂入了某个优先级就绪队列,可是现在优先级变了,岂不就应该给它换一个就绪队列吗,所以线程优先级的改变并不只是简单的赋值。由函数KiSetPriorityThread来完成相关操作。

函数KiSetPriorityThread说明,对处于不同状态的线程,改变其线程优先级时,处理方法不同(《Windows内核情景分析5.10 P417》)。

IRQL:CPU此时所处的状态。

线程优先级:线程的优先级只影响系统线程调度程序(分发函数Dispatch)的决策,在同一堆就绪线程中,优先级高的线程较优先级低的线程先被调度。还有另一个区别,优先级高时间片就越大

  • 非分页内存池:内存页不可以交换出内存。
  • 分页内存池:内存页可以交换出内存。

2 中断与异常

2.1 中断和异常的异同

本节内容是对《Windows内核原理与实现5.2 P310》内容进行整理。

中断和异常都会改变一个正在执行的线程的正常任务执行,甚至切换线程。但是中断和异常是不同的,这个区别非常重要:中断是异步的异常是同步的

中断是异步的:一个正在执行的线程,突然受到一个时钟中断或硬件设备打扰,那会在一定条件下去处理这个中断(类似于APC)。

异常是同步的:在代码中,如果某个地方可能会发生错误导致异常,一般会使用try-except来处理,在一个线程上处理时不是异步处理的。

中断和异常都可以由硬件/软件产生:

  • 硬件中断:由I/O设备引发。

  • 软件中断:如使用INT N指令。

    汇编代码中可以屏蔽那些可屏蔽的中断:cli,解除屏蔽:sti

  • 硬件异常:如除零错误、页面错误。

  • 软件异常:如内存访问错误等。

用于处理中断的机制是一种被称为中断对象的内核对象,它允许设备驱动程序为它的设备注册一个ISR(中断服务例程)。内核在接收到一个中断时,可以将该中断分发到那些已经为该中断向量注册了中断对象的设备驱动程序中,5.2.3节将介绍中断对象。类似地,Windows内核也提供了一套灵活的、可扩展的机制来分发异常,允许内核调试器或进程调试器、内核或应用程序本身,以及环境子系统有机会处理异常,5.2.7节将介绍异常分发过程。

2.2 硬件中断的发生和处理

现在绝大多数 Intel x86 系统使用了 APIC(高级可编程中断控制器),在 Intel x86 处理器的芯片中内置了一个本地 APIC模块,由它负责接收来自处理器上中断管脚、内部中断源和外部 I/ O APIC 的中断。而在多处理器系统中,本地 APIC 也负责发送和接收处理器间的中断(IPI, interprocessor interrupt)消息。因此,系统总线上的逻辑处理器之间通过各个处理器的本地 APIC 发送和接收 IPI 消息进行通信。

⚠️注意:APIC 和 I/O APIC是不同的。APIC是中断的处理模块(包含本地APIC及I/O APIC)APIC 分成两部分 LAPIC 和 I/O APIC,前者 LAPIC 位于 CPU 内部,每个 CPU 都有一个 LAPIC,后者 I/O APIC 与外设相连。I/O APIC 负责接收外部中断,并且将它们传递给本地 APIC,也就是说 APIC = LAPIC + I/O APIC。APIC就是一个总称。

APIC可以接收两种中断源,LAPIC接收本地中断源(如热传感器中断、APIC内部错误中断),还能接收I/O APIC传递过来的中断源。

1.png

2.3 APIC的功能

一个中断的起末会经历设备,中断控制器,CPU 三个阶段:设备产生中断信号,中断控制器翻译信号,CPU 来实际处理信号

LAPIC 和 I/O APIC的功能很复杂,其中对于本节的学习最重要的就是:将中断信号翻译成中断向量,使得每一个中断都能够对应一个中断向量。

本地 APIC 的初始化工作是在 Windows 体系结构的 HAL模块中完成的。

可参考:一文讲透!Windows内核 & x86中断机制详解再谈中断(APIC)【x86架构】APIC – 高级可编程中断控制器X86中断/异常与APIC

2.4 中断向量表

当一个中断信号被转换为指定的中断向量之后,CPU会根据这个中断向量去执行对应的系统服务(函数)。系统初始化时使用KiSystemStartup函数来完成相关的工作。

  1. Intel X86 一共定义了 256 个中断向量编号(也称为中断向量),从0~255。中断向量表也称为中断描述符表(IDT, Interrupt Descriptor Table)。
  2. IDT 将每个中断或异常与一个处理该中断或异常的函数程关联了起来。
  3. 与 GDT 和 LDT 类似,IDT 中的每一项是一个 8 字节的描述符。
  4. 处理器内部的 IDTR 寄存器记录了 IDT 的位置和它的最大限制。LIDTSIDT 指令分别用于加载和存储 IDTR 寄存器。LITD 只有在特权级 0 下才可以执行。

下图反应如何将INT N转换为指定函数地址的转换(注意IDT段描述符中的段选择子是一个对应GDT代码段的描述符):

具体见《Windows XP 段保护(三)》中断门。

INT_N转换为服务过程.png

中断和异常的差异:中断是异步的,异常是同步的

中断向量:

  • 0~19:给异常使用(2号是不可屏蔽中断)。
  • 20~31:Intel保留。
  • 32~255:用户定义的中断。

异常分为三类:

  • 错误(fault):处理器期待这类异常条件可被修正,它会返回到原来出错的指令处,典型的例子是页面错误。
  • 陷阱(trap):处理器期待原来的指令流仍然是连续可执行的,这只是一个陷阱而已,所以,它会返回到发生异常的指令后面的那条指令,典型的例子是调试断点。
  • 中止(abort):处理器并不期待程序或系统还能够从失败中恢复,这通常用于报告严重的错误,比如硬件错误或者系统数据结构中的不一致性。

具体如下表(Intel x86中断和异常一览表):

向量号 名称 类型 错误码 描述(或来源)
0 除法错 错误 DIV 和 IDIV 指令产生的错误
1 调试 错误/陷阱 调试异常
2 NMI 中断 不可屏蔽外部中断
3 断点 陷阱 INT 3 指令
4 溢出 Overflow 陷阱 int O指令,不是int 0
5 BOUND 越界 错误 BOUND 指令
6 无效操作码 错误 UD2 指令或保留的操作码(opcode)
7 协处理器不可用 错误 浮点或 WAIT/ FWAIT 指令
8 双重错误 中止 是(零) 可产生异常、NMI 或 INTR 的任何指令
9 协处理器段溢出 错误 浮点指令
10 无效 TSS 错误 任务切换或 TSS 访问
11 段不存在 错误 装载段寄存器或访问系统段
12 栈段错误 错误 栈操作和 SS 寄存器装载
13 一般保护错 错误 任何内存引用或其他保护检查
14 页面错误 错误 任何内存引用
15 Intel 保留 / / /
16 x87 浮点 错误 x87 FPU 浮点或 WAIT/ FWAIT 指令
17 对齐检查 错误 是(零) 对内存中数据的引用
18 机器检查 中止 错误码和异常来源与特定模型相关
19 SIMD 浮点异常 错误 SSE/SSE2/SSE3浮点指令
20~31 Intel 保留 / / /
32~255 用户定义的中断 中断 外部中断或 INT N 指令(N >= 32)

⚠️注意:这里的错误码实际上就是TrapFrame里面ErrCode(0x64)

40.png

中断号的具体的分配情况:

中断号(十进制/十六进制) 分配情况
0D~19D,0x00~0x13 固定分配给异常使用。
20D~31D,0x14-0x1F Intel保留给他公司将来自己使用(OS和用户都不要试图去使用这个号段,不安全)。
32D~41D,0x20-0x29 Windows没占用,因此这块号段我们也可以自由使用。
42D~46D,0x2A-0x2E Windows自己本身使用的5个中断号:KiGetTickCount、KiCallbaclReturn、KiRaiseAssertion、KiDebugService、KiSystemService。
48D~255D,0x30-0xFF Windows决定把这块剩余的号段让给硬件和用户使用。

参见《寒江独钓》一书P93页注册键盘中断时,搜索空闲未用表项是从0x20开始,到0x29结束的,就知道为什么寒江独钓是在这段范围内搜索空白表项了(其实我们也完全可以从0x14开始搜索)。

Windows系统中,0x30-0xFF这块号段让给了硬件和用户自己使用。事实上,这块号段的开头部分默认都是让给硬件IRQ使用的,也即是分配给硬件IRQ的。IRQ N默认映射到中断号0x30+N,如IRQ0用于系统时钟,系统时钟中断号默认对应就是0x30。当然程序员也可以修改APIC(可编程中断控制器)将IRQ映射到自定义的中断号。

3 中断请求级别(IRQL)

Intel 在 CPU 中虽然内置了APIC 来对中断信号进行转换及其他处理,但是 Software 在区分各类中断的优先级/紧急情况时自己做了一套优先级方案,称为中断请求级别(IRQL, Interrupt Request Level)。在 Intel x86 系统中,Windows 使用 0~31来表示优先级,数值越大,优先级越高

  • 软件中断或非中断代码的 IRQL是在内核中管理的。
  • 硬件中断则在 HAL 中被映射到对应的 IRQL。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define PASSIVE_LEVEL 0             // Passive release level
#define LOW_LEVEL 0 // Lowest interrupt level
#define APC_LEVEL 1 // APC interrupt level
#define DISPATCH_LEVEL 2 // Dispatcher level

#define PROFILE_LEVEL 27 // timer used for profiling.
#define CLOCK1_LEVEL 28 // Interval clock 1 level - Not used on x86
#define CLOCK2_LEVEL 28 // Interval clock 2 level
#define IPI_LEVEL 29 // Interprocessor interrupt level
#define POWER_LEVEL 30 // Power failure level
#define HIGH_LEVEL 31 // Highest interrupt level

#if defined(NT_UP)
#define SYNCH_LEVEL DISPATCH_LEVEL // 针对单处理器系统的同步级别
#else
#define SYNCH_LEVEL (IPI_LEVEL-1) // 针对多处理器系统的同步级别
#endif
IRQL IQRL级别 描述
0 PASSIVE_LEVEL 用户模式代码(或驱动代码)运行在次最低级别上。
1 APC_LEVEL APC异步过程调用
2 DISPATCH_LEVEL 一般是线程调度器正在运行,如KiDispatchInterruptSwapContext。或者正在处理硬件中断的后半部分。注意DPC与线程无关。是最高的软件中断级别,但是又要比所有的硬件中断IRQL低。
3~26 DIRQL 3~26 之间的 IRQL 被分配给设备,称为设备 IRQL(或 DIRQL),HAL 规定它们的分配方案。
在 Intel x86 多处理器系统中,HAL 会循环地将中断向量号映射到这段 IRQL 范围中。
DIRQL 范围中的 IRQL并无优先级区别,不同的设备中断只是被映射到相同或不同的 IRQL 而已。
27 PROFILE_LEVEL 是一个专用于性能剖析定时器的 IRQL。
当内核的性能剖析功能(利用 Microsoft 提供的 Kernrate 工具)打开时,系统时钟就会周期性地对当前处理器的代码地址(即被中断处的代码的地址)进行采样,从而建立起一张地址采样表,用来分析各个内核模块被执行的情况。在执行内核性能剖析功能时,处理器的 IRQL 将提升到PROFILE_LEVEL
28 CLOCK_LEVEL 是系统时钟中断使用的 IRQL,内核利用此 IRQL来更新系统时间以及触发定时器
29 IPI_LEVEL 是处理器之间通信的中断级别,它优先于时钟中断。
30 POWER_LEVEL 被定义为电源失败,但并未在 Windows 中真正使用。
31 HIGH_LEVEL 用于一些最高级别的任务,它禁止了所有其他级别的中断。当系统发生不可恢复错误而进入错误检查(BugCheck)状态时,会将 IRQL 提升至 HIGH_LEVEL,以屏蔽所有其他的中断。

3.png

  • Windows 总是运行在一个高度并发的环境中,任何时候,每个处理器都运行在某一IRQL 上。当中断发生时,处理器把被中断的线程的状态保存起来,然后调用 IDT中设定的中断例程。
  • 一个处于内核模式中的线程根据它所要做的事情来提升KeRaiselrql或者降低KeLowerlrql当前处理器的 IRQL。尤其是,当线程要访问全局的数据结构或执行关键的任务时,必须考虑是否提升IRQL,在完成以后,再降低IRQL。提升 IRQL可以屏较低 IRQL 的线程或中断。
  • 但是,作为内核代码,它总是应该尽可能地保持当前处理器的 IRQL 在 PASSIVE_LEVEL,这
    样用户代码才有机会得以运行。
  • KPCR->irql记录当前CPU所处的IRQL。

关于DISPATCH_LEVEL需要注意:

DISPATCH_LEVEL是最高的软件中断级别,但是又要比所有的硬件中断IRQL低。

  1. 不能做任何等待动作:如果某个线程已经运行在这个级别,则不得切换到其他线程(比如,等待一个同步对象,当前IRQL得线程不能等其他低级别线程等待的被等待对象,换句话说就是不能做任何等待动作),因为线程切换是通过系统调度器来完成的,而系统调度器运行在 DISPATCH_LEVEL 上。
  2. 不能访问换页内存区:因为一旦发生换页动作,就需要执行 I/O 操作,从磁盘上读入页面,期间至少有一个等待动作。因此,在中断处理例程中,只能访问非换页内存。

在 Intel x86 系统上,DPC 也运行在 DISPATCH _LEVEL 上,所以,这些规则均适用于 DPC 代码。在设备驱动程序中,这也是常见的错误原因。

4 中断对象(外部设备使用)

中断对象是 Windows 提供的一种中断扩展机制,它允许设备驱动程序将一个中断服务例程(ISR,Interrupt Service Routine)与某一特定的中断向量关联起来。通过中断对象机制,设备驱动程序无须操纵 IDT 就可以处理设备中断。

其实我们不必要关注这么多细节,这些任务操作系统或者设备驱动开发的人已经做好了。最重要的就是要知道:APIC将中断信号转换成中断向量,IoConnectInterrupt将中断向量与中断服务函数ISR关联起来,以后只要对应的中断发生就直接去执行ISR了。我们常说的IDT HOOK,实际上就是将这里的ISR替换成我们事先准备好的一个函数,然后我们去修改IDT段描述符的偏移Offset和对应的GDT段描述符的BaseAddress即可。

4.1 KINTERRUPT

中断对象是一种强大而方便的系统机制,它允许设备驱动程序通过一种可移植的途径插入它们的中断服务例程,从而支持硬件设备的中断处理。这种能力有重要的意义,它使得硬件设备,包括各种硬件总线、视频设备、网络设备等,共享同样的硬件中断向量。然而,Windows内核本身并不使用中断对象来处理中断。内核处理的中断包括定时器中断、电源失败、机器错误检查,以及内部使用的一些软件中断。这些中断例程是用汇编语言编写的,往往与具体的系统硬件平台紧密相关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kd> dt _KINTERRUPT
nt!_KINTERRUPT
+0x000 Type : Int2B //0x16 == 22
+0x002 Size : Int2B //168
+0x004 InterruptListEntry : _LIST_ENTRY //同一个中断向量关联的中断对象构成了一个双链表,当中断发生时,这些中断对象都能被执行
+0x00c ServiceRoutine : Ptr32 unsigned char //中断服务例程,ISR
+0x010 ServiceContext : Ptr32 Void //调用给定中断服务程序时的上下文,ISR的参数
+0x014 SpinLock : Uint4B //用于同步的自旋锁
+0x018 TickCount : Uint4B
+0x01c ActualLock : Ptr32 Uint4B
+0x020 DispatchAddress : Ptr32 void //中断分发函数的地址,KiInterruptDispatch
+0x024 Vector : Uint4B //中断向量,在PDO获得
+0x028 Irql : UChar //该中断处理例程执行时的 IRQL,在PDO获得
+0x029 SynchronizeIrql : UChar //ISR实际运行在的IRQL
+0x02a FloatingSave : UChar //X86平台默认为FALSE
+0x02b Connected : UChar //表示本中断对象是否挂上去了
+0x02c Number : Char //此中断对象要挂往的目标CPU编号
+0x02d ShareVector : UChar //是否与其他设备共享中断向量,TRUE/FALSE
+0x030 Mode : _KINTERRUPT_MODE //枚举类型,代表了该中断对象的模式。Latched(电平触发),LevelSensitive(边沿触发)
+0x034 ServiceCount : Uint4B //没用
+0x038 DispatchCount : Uint4B //没用
+0x03c DispatchCode : [106] Uint4B //基本的中断处理代码,它是在中断对象初始化时填好的少量汇编指令
参数 解释说明
InterruptListEntry 同一个中断向量关联的中断对象构成了一个双链表,当中断发生时,这些中断对象都能被执行。
ServiceRoutine 中断服务例程,ISR。当一个中断发生时,中断对象的 DispatchCode 代码块首先获得控制,它把该代码块所在的中断对象放到edi寄存器中,然后跳转到该中断向量对应的中断分发函数,即 ServiceRoutine。
ServiceContext 调用给定中断服务程序时的上下文,ISR的参数。
DispatchAddress 中断分发函数的地址,KiInterruptDispatch/KiInterruptDispatch/ KiFloatingDispatch/KiChainedDispatch/KilnterruptDispatch2ndLyl/KilnterruptDispatch2ndLvl/ KiChainedDispatch2ndLvl。对于常规模式的中断响应,这就是 KilnterruptDispatch。
IoConnectlnterrupt --> KeConnectlnterrupt --> KiConnectVectorAndInterruptObject 函数负责把中断对象的 DispatchCode 代码中的 jmp _KeSynchronizeExecution 指令的目标地址修正为与该中断向量相匹配的中断分发函数
Vector 中断向量。在PDO获得。
Irql 该中断处理例程执行时的 IRQL,在PDO获得。
Connected 表示本中断对象是否挂上去了。
Number 此中断对象要挂往的目标CPU编号。
ShareVector 是否与其他设备共享中断向量,TRUE/FALSE。
Mode 枚举类型成员,代表了该中断对象的模式,有两种可能:
- LevelSensitive 模式的中断是指,当中断请求信号出现(asserted)时,中断就会发生,因此,中断对象的例程必须消除中断的原因;
- Latched 模式的中断是指,仅当中断请求信号从无信号(deasserted)到有信号(asserted )发生变化时,中断才会发生。
DispatchCode 基本的中断处理代码,它是在中断对象初始化时(KeInitializeInterrupt)填好的少量汇编指令。当中断发生时,这些代码首先被执行

下面描述一个外部设备的建立,然后将分配给改设备的中断向量号与对应的ISR建立的过程:

  1. PNP设备(即插即用设备)在插入系统后,相应的总线驱动会自动为其创建一个用作栈底基石的PDO,然后给这个PDO发出一个IRP_MN_QUERY_RESOURCE_REQUIREMENTS查询得到初步的资源需求
  2. 然后,PNP管理器会找到相应的硬件端口驱动,调用其AddDevice函数,当这个函数返回后,该硬件设备的设备栈已经建立起立了,PNP管理器就给栈顶设备发出一个IRP_MN_FILTER_RESOURCE_REQUIREMENTS再次询问该硬件需要的资源(功能驱动此时可以拦截处理这个IRP,修改资源需求),当确定好最终的资源需求后,系统就协调分配端口号、中断号、DIRQL等硬件资源给它,即分配资源
  3. 分配资源完后,就发出一个IRP_MN_START_DEVICE 的 IRP 给栈顶设备请求启动该硬件设备。当该 IRP 下发来到端口驱动(指真正的硬件驱动)时, 端口驱动这时就使用IoConnectInterrupt函数将中断号与 ISR 挂接。即在分配的中断号上注册一个中断服务例程ISR,以处理硬件中断,与设备进行交互。

上面内容参考《Windows内核情景分析下第九章》与中断处理 - IoConnectInterrupt和中断处理例程,其实我们不必要关注这么多细节,这些任务操作系统或者设备驱动开发的人已经做好了。最重要的就是要知道:APIC将中断信号转换成中断向量,IoConnectInterrupt将中断向量与中断服务函数ISR关联起来,以后只要对应的中断发生就直接去执行ISR了。我们常说的IDT HOOK,实际上就是将这里的ISR替换成我们事先准备好的一个函数,然后我们去修改IDT段描述符的偏移Offset和对应的GDT段描述符的BaseAddress。

4.2 将IDT挂接ISR

函数 IoConnectInterrupt 将一个中断服务程序连接到给定的中断向量上。

设备驱动程序调用 IoConnectlnterrupt 函数来使用中断对象。IoConnectinterrupt 封装了 KelnitializelnterruptKeConnectInterrupt 这两个内核函数。会先后调用 KeinitializelnterruptKeConnectinterrupt,前者是 KINTERRUPT 对象结构的初始化,后者将其连接到系统的中断向量及中断分发函数

IoConnectInterrupt 需要说明的两个参数:

  • ProcessorEnableMask:是个位图。在多处理器系统中,位图中的各个标志位代表着不同的 CPU。如果
    某位为1,就说明中断服务程序可以在这个 CPU 上执行。
  • KeActiveProcessors:是个位图,说明系统中实际具有哪一些 CPU。二者的交集就是可以执行给定中断服务程序而又实际存在
    的 CPU 的集合,这个集合当然不能为空。

IoConnectInterrupt 函数处理流程

  1. KeInitializeInterrupt:除了将参数中的值赋给中断对象以外,还会将 KeInterruptTemplate 数组中的指令拷贝到中断对象的DispatchCode 成员中,并且修正其中的一条指令,使其指向当前中断对象。
  2. 中断对象在经过初始化以后,便可以调用 KeConnectlnterrupt 函数,将它挂到中断对象中指定的中断向量的 IDT 项中。
  3. KeConnectlnterrupt 用到了两个辅助函数 KiConnectVectorAndlnterruptObjectKiGetVeetorinfo,并通过数据结构 DISPATCHL_INFO 来传递中断设置。其中 KiConnectVectorAndInterruptObject 函数负责把中断对象的 DispatchCode 代码中的jmp _KeSynchronizeExecution 指令的目标地址修正为与该中断向量相匹配的中断分发函数。系统提供的中断分发两数为:KiInterruptDispatchKiFloatingDispatchKiChainedDispatch,或者 KilnterruptDispatch2ndLylKilnterruptDispatch2ndLvlKiChainedDispatch2ndLvl
  4. 其中后三个分发函数针对二级分发,由 HAL 决定。链式中断分发函数,即 KiChainedDispatchKiChainedDispatch2ndLvl 的流程是:针对链表中的每个中断对象,提升 IRQL,获得服务例程自旋锁(即中断对象的 ActualLock 域),调用中断对象的服务例程,然后释放自旋锁,恢复 IRQL。
  5. 对于 LevelSensitive 模式的中断,一旦链表上有一个中断对象的中断服务例程已经处理了一个中断,则后面的中断对象不再有机会处理该中断;而对于 Latched模式的中,中断分发函数会遍历整个链表,因而链表上所有中断对象都可以处理该中断。
  6. KiConnectVectorAndlnterruptObject 函数通过C宏 KiSetHandlerAddressToIDT 将中断对象中的中断分发函数插人到 IDT中。一旦 KeConnectInterrupt 函数完成这一步并打开中断开关,以后当中断发生时,该中断分发函数就会被调用

整个流程可参考:

初始化和连接工作完成后,以后只要中断一触发,就会首先调用 DispatchCode 成员,在该成员指向的代码中会jmp到对应的KiInterruptDispatch,然后分发(Dispatch)进入预设的中断服务程序(ISR)。

基于上述关于中断机制的介绍,我们可以知道,当一个中断发生时,其处理过程如下:

  • 中断对象的 DispatchCode 代码块首先获得控制,它把该代码块所在的中断对象放到edi寄存器中,然后跳转到该中断向量对应的中断分发函数。
  • 在中断分发函数中,它提升 IRQL,获取自旋锁,然后调用中断对象的服务例程,即 KINTERRUPT 的 ServiceRoutine 成员,待返回以后,释放自旋锁,恢复IRQL。依据不同的中断分发函数以及中断对象的模式,可能还会处理中断对象链表上的其他中断
    对象,如上文所介绍。
  • 最后,中断分发函数通过 Kei386EoiHelper 辅助函数返回。Kei386EoiHelper 中的 iretd 指令结束整个中断处理。

图5.7显示了两个中断对象连接到同一个中断向量情形下的中断控制流程。图中的实线箭头代表了实际的控制流,虚线箭头代表了指针关系。

4.png

可以参考:windows内核情景分析—中断处理

4.3 IoConnectInterrupt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
NTSTATUS IoConnectInterrupt(
OUT PKINTERRUPT *InterruptObject,
IN PKSERVICE_ROUTINE ServiceRoutine,
IN PVOID ServiceContext,
IN PKSPIN_LOCK SpinLock OPTIONAL,
IN ULONG Vector,
IN KIRQL Irql,
IN KIRQL SynchronizeIrql,
IN KINTERRUPT_MODE InterruptMode,
IN BOOLEAN ShareVector,
IN KAFFINITY ProcessorEnableMask,
IN BOOLEAN FloatingSave
)
/*++
Routine Description:
This routine allocates, initializes, and connects interrupt objects for
all of the processors specified in the processor enable mask.
Arguments:
InterruptObject - Address of a variable to receive a pointer to the first
interrupt object allocated and initialized.
ServiceRoutine - Address of the interrupt service routine (ISR) that should
be executed when the interrupt occurs.
ServiceContext - Supplies a pointer to the context information required
by the ISR.
SpinLock - Supplies a pointer to a spin lock to be used when synchronizing
with the ISR.
Vector - Supplies the vector upon which the interrupt occurs.
Irql - Supplies the IRQL upon which the interrupt occurs.
SynchronizeIrql - The request priority that the interrupt should be
synchronized with.
InterruptMode - Specifies the interrupt mode of the device.
ShareVector - Supplies a boolean value that specifies whether the
vector can be shared with other interrupt objects or not. If FALSE
then the vector may not be shared, if TRUE it may be. Latched.
ProcessorEnableMask - Specifies a bit-vector for each processor on which
the interrupt is to be connected. A value of one in the bit position
cooresponding to the processor number indicates that the interrupt
should be allowed on that processor. At least one bit must be set.
FloatingSave - A BOOLEAN that specifies whether or not the machine's
floating point state should be saved before invoking the ISR.
Return Value:
The function value is the final function status. The three status values
that this routine can itself return are:
STATUS_SUCCESS - Everything worked successfully.
STATUS_INVALID_PARAMETER - No processors were specified.
STATUS_INSUFFICIENT_RESOURCES - There was not enough nonpaged pool.
--*/
{
CCHAR count;
BOOLEAN builtinUsed;
PKINTERRUPT interruptObject;
KAFFINITY processorMask;
NTSTATUS status;
PIO_INTERRUPT_STRUCTURE interruptStructure;
PKSPIN_LOCK spinLock;
#ifdef INTR_BINDING
ULONG AssignedProcessor;
#endif // INTR_BINDING

PAGED_CODE();

// Initialize the return pointer and assume success.
*InterruptObject = (PKINTERRUPT) NULL;
status = STATUS_SUCCESS;

// Determine how much memory is to be allocated based on how many
// processors this system may have and how many bits are set in the
// processor enable mask.
processorMask = ProcessorEnableMask & KeActiveProcessors;
count = 0;

while (processorMask) {
if (processorMask & 1) {
count++;
}
processorMask >>= 1;
}

// If any interrupt objects are to be allocated and initialized, allocate
// the memory now.
if (count) {

interruptStructure = ExAllocatePoolWithTag( NonPagedPool,
((count - 1) * sizeof( KINTERRUPT )) +
sizeof( IO_INTERRUPT_STRUCTURE ),
'nioI' );
if (interruptStructure == NULL) {
status = STATUS_INSUFFICIENT_RESOURCES;
}
} else {
status = STATUS_INVALID_PARAMETER;
}

// If the caller specified a spin lock to be used for the interrupt object,
// use it. Otherwise, provide one by using the one in the structure that
// was just allocated.
if (ARGUMENT_PRESENT( SpinLock )) {
spinLock = SpinLock;
} else {
spinLock = &interruptStructure->SpinLock;
}

// If the pool allocation was successful, initialize and connect the
// interrupt objects to the appropriate processors.
if (NT_SUCCESS( status )) {

// Return the address of the first interrupt object in case an
// interrupt is pending for the device when it is initially connected
// and the driver must synchronize its execution with the ISR.
*InterruptObject = &interruptStructure->InterruptObject;

// Begin by getting a pointer to the start of the memory to be used
// for interrupt objects other than the builtin object.
interruptObject = (PKINTERRUPT) (interruptStructure + 1);
builtinUsed = FALSE;
processorMask = ProcessorEnableMask & KeActiveProcessors;

// Now zero the interrupt structure itself so that if something goes
// wrong it can be backed out.
RtlZeroMemory( interruptStructure, sizeof( IO_INTERRUPT_STRUCTURE ) );

// For each entry in the processor enable mask that is set, connect
// and initialize an interrupt object. The first bit that is set
// uses the builtin interrupt object, and all others use the pointers that follow it.

for (count = 0; processorMask; count++, processorMask >>= 1) {

if (processorMask & 1) {
KeInitializeInterrupt( builtinUsed ?
interruptObject :
&interruptStructure->InterruptObject,
ServiceRoutine,
ServiceContext,
spinLock,
Vector,
Irql,
SynchronizeIrql,
InterruptMode,
ShareVector,
count,
FloatingSave );

if (!KeConnectInterrupt( builtinUsed ?
interruptObject :
&interruptStructure->InterruptObject )) {

// An error occurred while attempting to connect the
// interrupt. This means that the driver either specified
// the wrong type of interrupt mode, or attempted to connect
// to some processor that didn't exist, or whatever. In
// any case, the problem turns out to be an invalid
// parameter was specified. Simply back everything out
// and return an error status.
//
// Note that if the builtin entry was used, then the entire
// structure needs to be walked as there are entries that
// were successfully connected. Otherwise, the first
// attempt to connect failed, so simply free everything in-line.

if (builtinUsed) {
IoDisconnectInterrupt( &interruptStructure->InterruptObject );
} else {
ExFreePool( interruptStructure );
}
status = STATUS_INVALID_PARAMETER;
break;
}

// If the builtin entry has been used, then the interrupt
// object just connected was one of the pointers, so fill
// it in with the address of the memory actually used.

if (builtinUsed) {
interruptStructure->InterruptArray[count] = interruptObject++;

} else {

// Otherwise, the builtin entry was used, so indicate
// that it is no longer valid to use and start using
// the pointers instead.

builtinUsed = TRUE;
}

}
}
}

// Finally, reset the address of the interrupt object if the function
// failed and return the final status of the operation.
if (!NT_SUCCESS( status )) {
*InterruptObject = (PKINTERRUPT) NULL;
}

return status;
}

函数 KeInitializeInterrupt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
VOID KeInitializeInterrupt (
IN PKINTERRUPT Interrupt,
IN PKSERVICE_ROUTINE ServiceRoutine,
IN PVOID ServiceContext,
IN PKSPIN_LOCK SpinLock OPTIONAL,
IN ULONG Vector,
IN KIRQL Irql,
IN KIRQL SynchronizeIrql,
IN KINTERRUPT_MODE InterruptMode,
IN BOOLEAN ShareVector,
IN CCHAR ProcessorNumber,
IN BOOLEAN FloatingSave
)
/*++
Routine Description:
This function initializes a kernel interrupt object. The service routine,
service context, spin lock, vector, IRQL, SynchronizeIrql, and floating
context save flag are initialized.
Arguments:
Interrupt - Supplies a pointer to a control object of type interrupt.
ServiceRoutine - Supplies a pointer to a function that is to be
executed when an interrupt occurs via the specified interrupt vector.
ServiceContext - Supplies a pointer to an arbitrary data structure which is
to be passed to the function specified by the ServiceRoutine parameter.
SpinLock - Supplies a pointer to an executive spin lock.
Vector - Supplies the index of the entry in the Interrupt Dispatch Table
that is to be associated with the ServiceRoutine function.
Irql - Supplies the request priority of the interrupting source.
SynchronizeIrql - The request priority that the interrupt should be synchronized with.
InterruptMode - Supplies the mode of the interrupt; LevelSensitive or

ShareVector - Supplies a boolean value that specifies whether the
vector can be shared with other interrupt objects or not. If FALSE
then the vector may not be shared, if TRUE it may be. Latched.
ProcessorNumber - Supplies the number of the processor to which the
interrupt will be connected.
FloatingSave - Supplies a boolean value that determines whether the
floating point registers and pipe line are to be saved before calling
the ServiceRoutine function.
Return Value: None.
--*/
{

LONG Index;
PULONG pl;
PULONG NormalDispatchCode;

// Initialize standard control object header.
Interrupt->Type = InterruptObject;
Interrupt->Size = sizeof(KINTERRUPT);

// Initialize the address of the service routine,
// the service context, the address of the spin lock, the vector
// number, the IRQL of the interrupting source, the Irql used for
// synchronize execution, the interrupt mode, the processor
// number, and the floating context save flag.

Interrupt->ServiceRoutine = ServiceRoutine;
Interrupt->ServiceContext = ServiceContext;

if (ARGUMENT_PRESENT(SpinLock)) {
Interrupt->ActualLock = SpinLock;
} else {
KeInitializeSpinLock (&Interrupt->SpinLock);
Interrupt->ActualLock = &Interrupt->SpinLock;
}

Interrupt->Vector = Vector;
Interrupt->Irql = Irql;
Interrupt->SynchronizeIrql = SynchronizeIrql;
Interrupt->Mode = InterruptMode;
Interrupt->ShareVector = ShareVector;
Interrupt->Number = ProcessorNumber;
Interrupt->FloatingSave = FloatingSave;

// Initialize fields for the interrupt storm detection. Set these
// to -1 so that the first time through the interrupt dispatch they
// will be reset correctly.

Interrupt->TickCount = (ULONG)-1;
Interrupt->DispatchCount = (ULONG)-1;

// Copy the interrupt dispatch code template into the interrupt object
// and edit the machine code stored in the structure (please see
// _KiInterruptTemplate in intsup.asm.) Finally, flush the dcache
// on all processors that the current thread can
// run on to ensure that the code is actually in memory.

NormalDispatchCode = &(Interrupt->DispatchCode[0]);

pl = NormalDispatchCode;

for (Index = 0; Index < NORMAL_DISPATCH_LENGTH; Index += 1) {
*NormalDispatchCode++ = KiInterruptTemplate[Index];
}

// The following two instructions set the address of current interrupt
// object the the NORMAL dispatching code.

pl = (PULONG)((PUCHAR)pl + ((PUCHAR)&KiInterruptTemplateObject -
(PUCHAR)KiInterruptTemplate) -4);
*pl = (ULONG)Interrupt;

KeSweepDcache(FALSE);

// Set the connected state of the interrupt object to FALSE.

Interrupt->Connected = FALSE;
return;
}

函数KeConnectInterrupt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
BOOLEAN KeConnectInterrupt(IN PKINTERRUPT Interrupt)
/*++
Routine Description:
This function connects an interrupt object to the interrupt vector
specified by the interrupt object. If the interrupt object is already
connected, or an attempt is made to connect to an interrupt that cannot
be connected, then a value of FALSE is returned. Else the specified
interrupt object is connected to the interrupt vector, the connected
state is set to TRUE, and TRUE is returned as the function value.
Arguments: Interrupt - Supplies a pointer to a control object of type interrupt.
Return Value:
If the interrupt object is already connected or an attempt is made to
connect to an interrupt vector that cannot be connected, then a value
of FALSE is returned. Else a value of TRUE is returned.
--*/
{
DISPATCH_INFO DispatchInfo;
BOOLEAN Connected;
BOOLEAN ConnectError;
BOOLEAN Enabled;
KIRQL Irql;
CCHAR Number;
KIRQL OldIrql;
ULONG Vector;

// If the interrupt object is already connected, the interrupt vector
// number is invalid, an attempt is being made to connect to a vector
// that cannot be connected, the interrupt request level is invalid, or
// the processor number is invalid, then do not connect the interrupt
// object. Else connect interrupt object to the specified vector and
// establish the proper interrupt dispatcher.

Connected = FALSE;
ConnectError = FALSE;
Irql = Interrupt->Irql;
Number = Interrupt->Number;
Vector = Interrupt->Vector;
if ( !((Irql > HIGH_LEVEL) ||
(Number >= KeNumberProcessors) ||
(Interrupt->SynchronizeIrql < Irql) ||
(Interrupt->FloatingSave) // R0 x87 usage not supported on x86
)
) {

// Set system affinity to the specified processor.

KeSetSystemAffinityThread((KAFFINITY)(1<<Number));

// Raise IRQL to dispatcher level and lock dispatcher database.
KiLockDispatcherDatabase(&OldIrql);

// Is interrupt object already connected?
if (!Interrupt->Connected) {

// Determine interrupt dispatch vector
KiGetVectorInfo (
Vector,
&DispatchInfo
);

// If dispatch vector is not connected, then connect it
if (DispatchInfo.Type == NoConnect) {
Connected = TRUE;
Interrupt->Connected = TRUE;

// Connect interrupt dispatch to interrupt object dispatch code

InitializeListHead(&Interrupt->InterruptListEntry);
KiConnectVectorAndInterruptObject (Interrupt, NormalConnect);

// Enabled system vector
Enabled = HalEnableSystemInterrupt(Vector, Irql, Interrupt->Mode);
if (!Enabled) {
ConnectError = TRUE;
}

} else if (DispatchInfo.Type != UnkownConnect &&
Interrupt->ShareVector &&
DispatchInfo.Interrupt->ShareVector &&
DispatchInfo.Interrupt->Mode == Interrupt->Mode) {

// Vector is already connected as sharable. New vector is sharable
// and modes match. Chain new vector.

Connected = TRUE;
Interrupt->Connected = TRUE;

ASSERT (Irql <= SYNCH_LEVEL);

// If not already using chained dispatch handler, set it up

if (DispatchInfo.Type != ChainConnect) {
KiConnectVectorAndInterruptObject (DispatchInfo.Interrupt, ChainConnect);
}

// Add to tail of chained dispatch
InsertTailList(
&DispatchInfo.Interrupt->InterruptListEntry,
&Interrupt->InterruptListEntry
);

}
}

// Unlock dispatcher database and lower IRQL to its previous value.
KiUnlockDispatcherDatabase(OldIrql);

// Set system affinity back to the original value.
KeRevertToUserAffinityThread();
}

if (Connected && ConnectError) {
#if DBG
DbgPrint ("HalEnableSystemInterrupt failed\n");
#endif
KeDisconnectInterrupt (Interrupt);
Connected = FALSE;
}

// Return whether interrupt was connected to the specified vector.
return Connected;
}

4.4 总结

中断服务函数 ISR 是在设备 IRQL(DIRQL)级别上执行的

5 DPC

DPC:延迟过程调用,Deferred Procedure Call。运行在 DISPATCH_LEVEL 级别。

DIRQL > DISPATCH_LEVELDPC 执行时屏蔽了线程调度(线程调度在DISPATCH_LEVEL)。

5.1 DPC的作用

使用 DPC 得主要原因有以下

  1. 一般而言,整个中断服务程序 ISR 通常都是在关中断cli的条件下执行的,关中断的时间应该尽可能的短,如果关闭中断的时间太长会引起中断请求的丢失。
  2. 逻辑上应该放在 ISR 中完成的操作但并非那么紧迫费时间可以在开中断 sti 下执行的代码单独放到另一个函数中。这个函数就叫做 DPC 函数
  3. 通常把在 ISR 中执行的代码成为中断服务的上半段,在 DPC 函数中执行的代码称作中断服务的下半段
  4. 中断服务函数 ISR 是在 DIRQL 级别上执行的,DPC 是在 DISPATCH_LEVEL 上执行。

DPC 函数的执行时机

  1. 为此,内核中要有个 DPC 请求队列,中断服务程序执行完它的上半段之后就把一个 DPC 请求挂入这个队列,要求内核调用相应的 DPC 函数,然后(形式上)就从中断返回了。接着,如果没有别的中断请求,内核就会扫描这个 DPC 请求队列,依次在开中断的条件下执行这些 DPC 函数,直至又发生中断或执行完队列中的所有 DPC 函数。
  2. 至于当前线程所要执行的程序,则只有在 DPC 请求队列为空的时候才会继续得到执行。中断是最急的,DPC 函数其次,最后才是当前线程

DPC 函数的堆栈

  1. ISR 函数较小,使用的是当前线程的堆栈空间。DPC 函数执行时会单独开辟一个函数堆栈。

5.2 _KDPC

1
2
3
4
5
6
7
8
9
10
11
kd> dt _KDPC
nt!_KDPC
+0x000 Type : Int2B
+0x002 Number : UChar
+0x003 Importance : UChar
+0x004 DpcListEntry : _LIST_ENTRY
+0x00c DeferredRoutine : Ptr32 void
+0x010 DeferredContext : Ptr32 Void
+0x014 SystemArgument1 : Ptr32 Void
+0x018 SystemArgument2 : Ptr32 Void
+0x01c Lock : Ptr32 Uint4B

源码定义如下:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _KDPC {
CSHORT Type; //DPC 对象的类型,KOBJECTS.DpcObject == 19
UCHAR Number; //DPC 对象的在哪个目标处理器上执行
UCHAR Importance; //DPC 对象的重要程度,低Lowlmportance、中MediumImportance、高Highlmportance
LIST_ENTRY DpcListEntry; // DPC 对象加入到 DPC 链表中的节点对象,挂在腰上
PKDEFERRED_ROUTINE DeferredRoutine; //真正被延迟执行的函数指针
PVOID DeferredContext; //是DeferredRoutine的一个参数
PVOID SystemArgument1; //参数1
PVOID SystemArgument2; //参数1
PULONG_PTR Lock;
} KDPC, *PKDPC, *RESTRICTED_POINTER PRKDPC;
参数 解释说明
Type 在结构KOBJECTS结构中定义为DpcObject,值为19。
Number DPC 对象的在哪个目标处理器上执行。即它被插入到哪个处理器的 DPC 链表中,为了将 DPC 对象指定到某一特定的处理器上,应将 Number 域赋为 MAXIMUM_PROCESSORS 加上目标处理器编号,见 KeSetTargetProcessorDpc 函数的做法;否则,DPC 对象被插入到当前处理器的 DPC 链表中。
Importance 说明了一个 DPC 对象的重要程度、可以为低(Lowlmportance)、中(MediumImportance)或高(Highlmportance)。当重要程度为高时,DPC 对象被插人到 DPC 链表的头部,否则插入到尾部。
DpcListEntry DPC 对象加入到 DPC 链表中的节点对象,挂在腰上。
DeferredRoutine 真正被延迟执行的函数指针。DPC 函数。
DeterredContext 是一个指针,它可以指向任意数据结构,在 DPC 对象初始化时指定,当延迟函数被执行时,它也被传递到延迟函数中。
Lock 保存此DPC对象所在DPC队列的自旋锁,用于锁定DPC队列,同时也用于判断此DPC对象是否被加入到一个DPC队列中。
1
2
3
4
5
6
#if defined(_WIN64)
#define MAXIMUM_PROCESSORS 64

#else
#define MAXIMUM_PROCESSORS 32
#endif

DPC 是跟CPU相关的,APC 是跟线程相关的

⚠️注意:《Windows内核原理与实现》与《Windows内核情景分析》中说有两种 DPC 对象,即 DpcObjectThreadedDpcObject,并且还说 KPRCB 有 DpeData[2] 数组成员。

但是Windows XP 中只有 DpcObject 这种 DPC 对象,且KPRCB 也没有 DpeData[2] 数组成员。挂 DPC 对象直接挂到指定 KPRCB 的成员 DpcListHead 中去即可。

5.3 KeInitializeDpc

1、DPC 对象的用法很简单,使用者先拿着已有的资源(参数)调用 KeInitializeDpc 函数来对 DPC 对象进行初始化。初始化完成后再调用函数 KeInsertQueueDpc将这个 DPC 对象插入到指定 KPRCB 成员 DpcListHead 中去。

2、KeInsertQueueDpc函数中,DPC 对象已经插入喉,会像 APC 那样检查一下刚才插入的 DPC 能否执行,如果条件满足就会触发中断hal.dll对中断处理后都会调用NTOS的函数KiDispatchInterrupt去执行DPC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
VOID __stdcall KeInitializeDpc (
IN PRKDPC Dpc,
IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext
)
/*++
Routine Description:
This function initializes a kernel DPC object. The deferred routine
and context parameter are stored in the DPC object.
Arguments:
Dpc - Supplies a pointer to a control object of type DPC.
DeferredRoutine - Supplies a pointer to a function that is called when
the DPC object is removed from the current processor's DPC queue.
DeferredContext - Supplies a pointer to an arbitrary data structure which is
to be passed to the function specified by the DeferredRoutine parameter.
Return Value: None.
--*/
{
// Initialize standard control object header.
Dpc->Type = DpcObject;
Dpc->Number = 0;
Dpc->Importance = MediumImportance;

// Initialize deferred routine address and deferred context parameter.
Dpc->DeferredRoutine = DeferredRoutine;
Dpc->DeferredContext = DeferredContext;
Dpc->Lock = NULL;
return;
}

5.4 KeInsertQueueDpc

1、DPC 对象的用法很简单,使用者先拿着已有的资源(参数)调用 KeInitializeDpc 函数来对 DPC 对象进行初始化。初始化完成后再调用函数 KeInsertQueueDpc将这个 DPC 对象插入到指定 KPRCB 成员 DpcListHead 中去。

2、KeInsertQueueDpc函数中,DPC 对象已经插入喉,会像 APC 那样检查一下刚才插入的 DPC 能否执行,如果条件满足就会触发中断hal.dll对中断处理后都会调用NTOS的函数KiDispatchInterrupt去执行DPC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
BOOLEAN __stdcall KeInsertQueueDpc (
IN PRKDPC Dpc,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
)
/*++
Routine Description:
This function inserts a DPC object into the DPC queue. If the DPC object
is already in the DPC queue, then no operation is performed. Otherwise,
the DPC object is inserted in the DPC queue and a dispatch interrupt is
requested.
Arguments:
Dpc - Supplies a pointer to a control object of type DPC.
SystemArgument1, SystemArgument2 - Supply a set of two arguments that
contain untyped data provided by the executive.
Return Value:
If the DPC object is already in a DPC queue, then a value of FALSE is
returned. Otherwise a value of TRUE is returned.
--*/
{
ULONG Index;
PKSPIN_LOCK Lock;
KIRQL OldIrql;
PKPRCB Prcb;
ULONG Processor;

ASSERT_DPC(Dpc);

// Disable interrupts.

KeRaiseIrql(HIGH_LEVEL, &OldIrql);

// Acquire the DPC queue lock for the specified target processor.

#if !defined(NT_UP) //多核

if (Dpc->Number >= MAXIMUM_PROCESSORS) {
Processor = Dpc->Number - MAXIMUM_PROCESSORS;
Prcb = KiProcessorBlock[Processor];

} else {
Prcb = KeGetCurrentPrcb();
}

KiAcquireSpinLock(&Prcb->DpcLock);

#else

Prcb = KeGetCurrentPrcb();

#endif

// If the DPC object is not in a DPC queue, then store the system
// arguments, insert the DPC object in the DPC queue, increment the
// number of DPCs queued to the target processor, increment the DPC
// queue depth, set the address of the DPC target DPC spinlock, and
// request a dispatch interrupt if appropriate.

if ((Lock = InterlockedCompareExchangePointer(&Dpc->Lock, &Prcb->DpcLock, NULL)) == NULL) {
Prcb->DpcCount += 1;
Prcb->DpcQueueDepth += 1;
Dpc->SystemArgument1 = SystemArgument1;
Dpc->SystemArgument2 = SystemArgument2;

// If the DPC is of high importance, then insert the DPC at the
// head of the DPC queue. Otherwise, insert the DPC at the end
// of the DPC queue.

if (Dpc->Importance == HighImportance) {
InsertHeadList(&Prcb->DpcListHead, &Dpc->DpcListEntry);

} else {
InsertTailList(&Prcb->DpcListHead, &Dpc->DpcListEntry);
}

// If a DPC routine is not active on the target processor, then
// request a dispatch interrupt if appropriate.

if ((Prcb->DpcRoutineActive == FALSE) &&
(Prcb->DpcInterruptRequested == FALSE)) {

// Request a dispatch interrupt on the current processor if
// the DPC is not of low importance, the length of the DPC
// queue has exceeded the maximum threshold, or if the DPC
// request rate is below the minimum threshold.

#if defined(NT_UP) //单核

if ((Dpc->Importance != LowImportance) ||
(Prcb->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) ||
(Prcb->DpcRequestRate < Prcb->MinimumDpcRate)) {
Prcb->DpcInterruptRequested = TRUE;
KiRequestSoftwareInterrupt(DISPATCH_LEVEL);
}

// If the DPC is being queued to another processor and the
// DPC is of high importance, or the length of the other
// processor's DPC queue has exceeded the maximum threshold,
// then request a dispatch interrupt.

#else //多核

if (Prcb != KeGetCurrentPrcb()) {
if (((Dpc->Importance == HighImportance) ||
(Prcb->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth))) {
Prcb->DpcInterruptRequested = TRUE;
KiIpiSend(AFFINITY_MASK(Processor), IPI_DPC);
}

} else {

// Request a dispatch interrupt on the current processor if
// the DPC is not of low importance, the length of the DPC
// queue has exceeded the maximum threshold, or if the DPC
// request rate is below the minimum threshold.

if ((Dpc->Importance != LowImportance) ||
(Prcb->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) ||
(Prcb->DpcRequestRate < Prcb->MinimumDpcRate)) {
Prcb->DpcInterruptRequested = TRUE;
KiRequestSoftwareInterrupt(DISPATCH_LEVEL);
}
}

#endif

}
}

// Release the DPC lock, enable interrupts, and return whether the
// DPC was queued or not.

#if !defined(NT_UP)

KiReleaseSpinLock(&Prcb->DpcLock);

#endif

KeLowerIrql(OldIrql);
return (Lock == NULL);
}

5.5 KeRemoveQueueDpc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
BOOLEAN __stdcall KeRemoveQueueDpc (
IN PRKDPC Dpc
)
/*++
Routine Description:
This function removes a DPC object from the DPC queue. If the DPC object
is not in the DPC queue, then no operation is performed. Otherwise, the
DPC object is removed from the DPC queue and its inserted state is set
FALSE.
Arguments: Dpc - Supplies a pointer to a control object of type DPC.
Return Value:
If the DPC object is not in the DPC queue, then a value of FALSE is
returned. Otherwise a value of TRUE is returned.
--*/

{

PKSPIN_LOCK Lock;
PKPRCB Prcb;

ASSERT_DPC(Dpc);

// If the DPC object is in the DPC queue, then remove it from the queue
// and set its inserted state to FALSE.

_disable();
Lock = Dpc->Lock;
if (Lock != NULL) {

// Acquire the DPC lock of the target processor.

#if !defined(NT_UP)

KiAcquireSpinLock(Lock);

#endif

// If the specified DPC is still in the DPC queue, then remove it.
//
// N.B. It is possible for specified DPC to be removed from the
// specified DPC queue before the DPC lock is obtained.

if (Lock == Dpc->Lock) {
Prcb = CONTAINING_RECORD(Lock, KPRCB, DpcLock);
Prcb->DpcQueueDepth -= 1;
RemoveEntryList(&Dpc->DpcListEntry);
Dpc->Lock = NULL;
}

// Release the DPC lock of the target processor.

#if !defined(NT_UP)

KiReleaseSpinLock(Lock);

#endif

}

// Enable interrupts and return whether the DPC was removed from a DPC
// queue.

_enable();
return (Lock != NULL);
}

5.6 DPC 交付

根据在插入时是否触发软件中断,DPC 对象在以下三种情况下被交付:

  1. 当处理器的 IRQL 从 DISPATCH_LEVEL 或更高级别降低到 APC_LEVEL 或 PASSIVE_LEVEL 时,内核开始处理该处理器的 DPC 链表中的 DPC 对象,依次调用链表中 DPC 对象的延迟雨数,直至链表为空。
  2. 通过 KelnsertQueueDpe 插入 DPC 对象时,如果依据 DPC 对象的重要程度,以及目标处理器 DPC 链表中的 DPC 数量或请求率,有必要立刻请求一个 DISPATCH_LEVEL 的软件中断,那么,DPC 链表中的 DPC 对象将立即有机会获得处理。
  3. 在各个处理器的空闲线程中,如果发现有 DPC 对象尚未被执行,则交付这些 DPC 对象。参见3.4.5 节关于空闲循环的介绍。

在前两种情况下,HAL 调用内核中的 KiDispatchInterrupt 函数(注意,这不同于中断对象分发函数 KilnterruptDispatch ),KiDispatchInterrupt 函数进而调用 KiRetireDpcList。在上述第三种情况下,KildleLoop 调用 KiRetireDpcList 函数。关于KiDispatchInterrupt 和 KildleLoop 函数的代码,参见 baselntos lketi386lctxswap.asm 文件。

5.7 HalRequestSoftwareInterrupt

  1. C:\WINDOWS\Driver Cache\i386\sp3.cab\hal.dll 文件版本为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    .text:80017EA0 // =============== S U B R O U T I N E =======================================
    .text:80017EA0
    .text:80017EA0
    .text:80017EA0 // __fastcall HalRequestSoftwareInterrupt(x)
    .text:80017EA0 public @HalRequestSoftwareInterrupt@4
    .text:80017EA0 @HalRequestSoftwareInterrupt@4 proc near
    .text:80017EA0 // DATA XREF: .edata:off_80023728↓o
    .text:80017EA0 mov eax, 1
    .text:80017EA5 shl eax, cl
    .text:80017EA7 pushf
    .text:80017EA8 cli
    .text:80017EA9 or ds:0FFDFF028h, eax
    .text:80017EAF mov cl, ds:0FFDFF024h
    .text:80017EB5 mov eax, ds:0FFDFF028h
    .text:80017EBA and eax, 3
    .text:80017EBD xor edx, edx
    .text:80017EBF mov dl, ds:SWInterruptLookUpTable[eax]
    .text:80017EC5 cmp dl, cl
    .text:80017EC7 jbe short loc_80017ED0
    .text:80017EC9 call ds:SWInterruptHandlerTable[edx*4] // jumptable 8001783B case 0
    .text:80017EC9 // jumptable 8001787F case 0
    .text:80017EC9 // jumptable 80017929 case 0
    .text:80017ED0
    .text:80017ED0 loc_80017ED0: // CODE XREF: HalRequestSoftwareInterrupt(x)+27↑j
    .text:80017ED0 popf
    .text:80017ED1 retn
    .text:80017ED1 @HalRequestSoftwareInterrupt@4 endp

    SWInterruptHandlerTable 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    .data:80018BC8 SWInterruptHandlerTable dd offset _KiUnexpectedInterrupt
    .data:80018BC8 // DATA XREF: KfLowerIrql(x)+28↑r
    .data:80018BC8 // KfLowerIrql(x)+4A↑r ...
    .data:80018BC8 // jump table for switch statement
    .data:80018BCC dd offset _HalpApcInterrupt // jumptable 80017929 case 1
    .data:80018BD0 dd offset _HalpDispatchInterrupt2@0 // jumptable 80017929 case 2
    .data:80018BD4 dd offset _KiUnexpectedInterrupt // jumptable 8001783B case 0
    .data:80018BD4 // jumptable 8001787F case 0
    .data:80018BD4 // jumptable 80017929 case 0
    .data:80018BD8 off_80018BD8 dd offset _HalpHardwareInterruptTable@0
    .data:80018BD8 // DATA XREF: HalEnableSystemInterrupt(x,x,x):loc_80017D9B↑w
    .data:80018BD8 // HalpInitializePICs(x)+65↑w
    .data:80018BD8 // HalpHardwareInterruptTable()
    .data:80018BDC dd offset HalpHardwareInterrupt01
    .data:80018BE0 dd offset HalpHardwareInterrupt02
    .data:80018BE4 dd offset HalpHardwareInterrupt03
    .data:80018BE8 dd offset HalpHardwareInterrupt04
    .data:80018BEC dd offset HalpHardwareInterrupt05
    .data:80018BF0 dd offset HalpHardwareInterrupt06
    .data:80018BF4 dd offset HalpHardwareInterrupt07
    .data:80018BF8 dd offset HalpHardwareInterrupt08
    .data:80018BFC dd offset HalpHardwareInterrupt09
    .data:80018C00 dd offset HalpHardwareInterrupt10
    .data:80018C04 dd offset HalpHardwareInterrupt11
    .data:80018C08 dd offset HalpHardwareInterrupt12
    .data:80018C0C dd offset HalpHardwareInterrupt13
    .data:80018C10 dd offset HalpHardwareInterrupt14
    .data:80018C14 dd offset HalpHardwareInterrupt15
    .data:80018C18 byte_80018C18 db 28h // DATA XREF: HalStartProfileInterrupt(x)+F↑r
    .data:80018C18 // HalSetProfileInterval(x)+44↑w
    .data:80018C19 db 0FFh // ÿ
    .data:80018C1A db 0FFh // ÿ
    .data:80018C1B db 0FFh // ÿ
    .data:80018C1C db 0FFh // ÿ
  2. C:\WINDOWS\system32\hal.dll 文件版本为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    .text:80012B68 // =============== S U B R O U T I N E =======================================
    .text:80012B68
    .text:80012B68
    .text:80012B68 // __fastcall HalRequestSoftwareInterrupt(x)
    .text:80012B68 public @HalRequestSoftwareInterrupt@4
    .text:80012B68 @HalRequestSoftwareInterrupt@4 proc near
    .text:80012B68 // DATA XREF: .edata:off_8002AFA8↓o
    .text:80012B68 cmp cl, large fs:_KPCR.TSS+55h
    .text:80012B6F jz short loc_80012BA5
    .text:80012B71 xor eax, eax
    .text:80012B73 mov al, cl
    .text:80012B75 xor ecx, ecx
    .text:80012B77 mov cl, byte ptr ds:_HalpIRQLtoTPR[eax]
    .text:80012B7D or ecx, 40000h
    .text:80012B83 pushf
    .text:80012B84 cli
    .text:80012B85
    .text:80012B85 loc_80012B85: // CODE XREF: HalRequestSoftwareInterrupt(x)+27↓j
    .text:80012B85 test dword ptr ds:0FFFE0300h, 1000h
    .text:80012B8F jnz short loc_80012B85
    .text:80012B91 mov ds:0FFFE0300h, ecx
    .text:80012B97
    .text:80012B97 loc_80012B97: // CODE XREF: HalRequestSoftwareInterrupt(x)+39↓j
    .text:80012B97 test dword ptr ds:0FFFE0300h, 1000h
    .text:80012BA1 jnz short loc_80012B97
    .text:80012BA3 popf
    .text:80012BA4 retn
    .text:80012BA5 // ---------------------------------------------------------------------------

    解释说明可以参考:绿盟月刊

区别可能是这样:sp3.cab 是PIC(8259A)。事实上,老久的PIC在很早以前就被淘汰了,取而代之的是APIC。由于APIC可以兼容PIC,所以在很多单处理器系统上我们看到的PIC实际是APIC的兼容PIC模式。后者是APIC对应的代码。

5.8 KiDispatchInterrupt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
.text:0046E9B0 // Exported entry 639. KiDispatchInterrupt
.text:0046E9B0
.text:0046E9B0 // =============== S U B R O U T I N E =======================================
.text:0046E9B0
.text:0046E9B0
.text:0046E9B0 // _DWORD __stdcall KiDispatchInterrupt()
.text:0046E9B0 public _KiDispatchInterrupt@0
.text:0046E9B0 _KiDispatchInterrupt@0 proc near
.text:0046E9B0
.text:0046E9B0 var_C = dword ptr -0Ch
.text:0046E9B0 var_8 = dword ptr -8
.text:0046E9B0 var_4 = dword ptr -4
.text:0046E9B0
.text:0046E9B0 mov ebx, large fs:1Ch
.text:0046E9B7
.text:0046E9B7 loc_46E9B7: // CODE XREF: KiDispatchInterrupt()+B9↓j
.text:0046E9B7 lea eax, [ebx+_KPCR.PrcbData.DpcListHead]
.text:0046E9BD cli
.text:0046E9BE cmp eax, [eax]
.text:0046E9C0 jz short loc_46E9DF
.text:0046E9C2 push ebp
.text:0046E9C3 push dword ptr [ebx]
.text:0046E9C5 mov dword ptr [ebx], 0FFFFFFFFh
.text:0046E9CB mov edx, esp
.text:0046E9CD mov esp, [ebx+_KPCR.PrcbData.DpcStack]
.text:0046E9D3 push edx
.text:0046E9D4 mov ebp, eax // ebp = eax = &DpcListHead
.text:0046E9D6 call KiRetireDpcList // ======
.text:0046E9D6 // ebx:KPCR
.text:0046E9D6 // ebp = eax = &DpcListHead
.text:0046E9D6 // esp:KPCR.PrcbData.DpcStack
.text:0046E9D6 // ======
.text:0046E9DB pop esp
.text:0046E9DC pop dword ptr [ebx]
.text:0046E9DE pop ebp
.text:0046E9DF
.text:0046E9DF loc_46E9DF: // CODE XREF: KiDispatchInterrupt()+10↑j
.text:0046E9DF sti
.text:0046E9E0 cmp [ebx+_KPCR.PrcbData.QuantumEnd], 0
.text:0046E9E7 jnz loc_46EA6E
.text:0046E9ED cmp [ebx+_KPCR.PrcbData.NextThread], 0
.text:0046E9F4 jz short locret_46EA65
.text:0046E9F6 cli
.text:0046E9F7 cmp ds:_KiDispatcherLock, 0
.text:0046E9FE jnz short loc_46EA66
.text:0046EA00 lea ecx, [ebx+_KPCR.PrcbData.LockQueue]
.text:0046EA06 call @KeTryToAcquireQueuedSpinLockAtRaisedIrql@4 // KeTryToAcquireQueuedSpinLockAtRaisedIrql(x)
.text:0046EA0B jz short loc_46EA66
.text:0046EA0D mov ecx, 1Ch // NewIrql
.text:0046EA12 call ds:__imp_@KfRaiseIrql@4 // KfRaiseIrql(x)
.text:0046EA18 sti
.text:0046EA19 mov eax, [ebx+_KPCR.PrcbData.NextThread]
.text:0046EA1F
.text:0046EA1F loc_46EA1F: // CODE XREF: KiDispatchInterrupt()+CF↓j
.text:0046EA1F sub esp, 0Ch
.text:0046EA22 mov [esp+0Ch+var_4], esi
.text:0046EA26 mov [esp+0Ch+var_8], edi
.text:0046EA2A mov [esp+0Ch+var_C], ebp
.text:0046EA2D mov esi, eax
.text:0046EA2F mov edi, [ebx+124h]
.text:0046EA35 mov dword ptr [ebx+128h], 0
.text:0046EA3F mov [ebx+124h], esi
.text:0046EA45 mov ecx, edi
.text:0046EA47 mov byte ptr [edi+50h], 1
.text:0046EA4B call @KiReadyThread@4 // VOID FASTCALL KiReadyThread (
.text:0046EA4B // IN PKTHREAD Thread
.text:0046EA4B // )
.text:0046EA4B // 参数一:ecx,参数二:edx
.text:0046EA50 mov cl, 1
.text:0046EA52 call SwapContext
.text:0046EA57 mov ebp, [esp+0Ch+var_C]
.text:0046EA5A mov edi, [esp+0Ch+var_8]
.text:0046EA5E mov esi, [esp+0Ch+var_4]
.text:0046EA62 add esp, 0Ch
.text:0046EA65
.text:0046EA65 locret_46EA65: // CODE XREF: KiDispatchInterrupt()+44↑j
.text:0046EA65 retn
.text:0046EA66 // ---------------------------------------------------------------------------
.text:0046EA66
.text:0046EA66 loc_46EA66: // CODE XREF: KiDispatchInterrupt()+4E↑j
.text:0046EA66 // KiDispatchInterrupt()+5B↑j
.text:0046EA66 sti
.text:0046EA67 pause
.text:0046EA69 jmp loc_46E9B7
.text:0046EA6E // ---------------------------------------------------------------------------
.text:0046EA6E
.text:0046EA6E loc_46EA6E: // CODE XREF: KiDispatchInterrupt()+37↑j
.text:0046EA6E mov dword ptr [ebx+9ACh], 0
.text:0046EA78 call _KiQuantumEnd@0 // KiQuantumEnd()
.text:0046EA7D or eax, eax
.text:0046EA7F jnz short loc_46EA1F
.text:0046EA81 retn
.text:0046EA81 _KiDispatchInterrupt@0 endp
.text:0046EA81
.text:0046EA81 // ---------------------------------------------------------------------------

5.9 KiRetireDpcList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
.text:0046EE0E // =============== S U B R O U T I N E =======================================
.text:0046EE0E
.text:0046EE0E // ======
.text:0046EE0E // ebx:KPCR
.text:0046EE0E // ebp = eax = &DpcListHead
.text:0046EE0E // esp:KPCR.PrcbData.DpcStack
.text:0046EE0E // ======
.text:0046EE0E
.text:0046EE0E KiRetireDpcList proc near // CODE XREF: KiDispatchInterrupt()+26↑p
.text:0046EE0E // KiIdleLoop()+23↑p
.text:0046EE0E
.text:0046EE0E var_24 = dword ptr -24h
.text:0046EE0E var_14 = dword ptr -14h
.text:0046EE0E var_10 = dword ptr -10h
.text:0046EE0E var_C = dword ptr -0Ch
.text:0046EE0E var_8 = dword ptr -8
.text:0046EE0E
.text:0046EE0E push esi
.text:0046EE0F lea esi, [ebx+_KPCR.PrcbData.DpcLock]
.text:0046EE15 push 0
.text:0046EE17 sub esp, 0Ch
.text:0046EE1A cmp ds:_PPerfGlobalGroupMask, 0
.text:0046EE21 jnz loc_46EEAC
.text:0046EE27
.text:0046EE27 loc_46EE27: // CODE XREF: KiRetireDpcList+85↓j
.text:0046EE27 // KiRetireDpcList+A6↓j ...
.text:0046EE27 mov large fs:_KPCR.PrcbData.DpcRoutineActive, esp
.text:0046EE2E
.text:0046EE2E loc_46EE2E: // CODE XREF: KiRetireDpcList+6C↓j
.text:0046EE2E lock bts dword ptr [esi], 0
.text:0046EE33 jb short loc_46EE9F
.text:0046EE35 cmp ebp, [ebp+0]
.text:0046EE38 jz short loc_46EE9A
.text:0046EE3A mov edx, [ebp+0]
.text:0046EE3D mov ecx, [edx]
.text:0046EE3F mov [ebp+0], ecx
.text:0046EE42 mov [ecx+4], ebp
.text:0046EE45 sub edx, 4 // KDPC
.text:0046EE48 mov ecx, [edx+_KDPC.DeferredRoutine]
.text:0046EE4B push [edx+_KDPC.SystemArgument2]
.text:0046EE4E push [edx+_KDPC.SystemArgument1]
.text:0046EE51 push [edx+_KDPC.DeferredContext]
.text:0046EE54 push edx
.text:0046EE55 mov [edx+_KDPC.Lock], 0
.text:0046EE5C dec [ebx+_KPCR.PrcbData.DpcQueueDepth]
.text:0046EE62 mov byte ptr [esi], 0
.text:0046EE65 sti
.text:0046EE66 cmp [esp+24h+var_8], 0
.text:0046EE6B jnz short loc_46EED4
.text:0046EE6D
.text:0046EE6D loc_46EE6D: // CODE XREF: KiRetireDpcList+DD↓j
.text:0046EE6D call ecx
.text:0046EE6F cmp [esp+14h+var_8], 0
.text:0046EE74 jnz short loc_46EEED
.text:0046EE76
.text:0046EE76 loc_46EE76: // CODE XREF: KiRetireDpcList+98↓j
.text:0046EE76 // KiRetireDpcList+F1↓j
.text:0046EE76 cli
.text:0046EE77 cmp ebp, [ebp+0]
.text:0046EE7A jnz short loc_46EE2E
.text:0046EE7C
.text:0046EE7C loc_46EE7C: // CODE XREF: KiRetireDpcList+8F↓j
.text:0046EE7C mov [ebx+_KPCR.PrcbData.DpcRoutineActive], 0
.text:0046EE86 mov [ebx+_KPCR.PrcbData.DpcInterruptRequested], 0
.text:0046EE90 cmp ebp, [ebp+0]
.text:0046EE93 jnz short loc_46EE27
.text:0046EE95 add esp, 10h
.text:0046EE98 pop esi
.text:0046EE99 retn
.text:0046EE9A // ---------------------------------------------------------------------------
.text:0046EE9A
.text:0046EE9A loc_46EE9A: // CODE XREF: KiRetireDpcList+2A↑j
.text:0046EE9A mov byte ptr [esi], 0
.text:0046EE9D jmp short loc_46EE7C
.text:0046EE9F // ---------------------------------------------------------------------------
.text:0046EE9F
.text:0046EE9F loc_46EE9F: // CODE XREF: KiRetireDpcList+25↑j
.text:0046EE9F sti
.text:0046EEA0
.text:0046EEA0 loc_46EEA0: // CODE XREF: KiRetireDpcList+9C↓j
.text:0046EEA0 test dword ptr [esi], 1
.text:0046EEA6 jz short loc_46EE76
.text:0046EEA8 pause
.text:0046EEAA jmp short loc_46EEA0
.text:0046EEAC // ---------------------------------------------------------------------------
.text:0046EEAC
.text:0046EEAC loc_46EEAC: // CODE XREF: KiRetireDpcList+13↑j
.text:0046EEAC mov eax, ds:_PPerfGlobalGroupMask
.text:0046EEB1 cmp eax, 0
.text:0046EEB4 jz loc_46EE27
.text:0046EEBA test dword ptr [eax+4], 80h
.text:0046EEC1 jz loc_46EE27
.text:0046EEC7 mov [esp+14h+var_8], 1
.text:0046EECF jmp loc_46EE27
.text:0046EED4 // ---------------------------------------------------------------------------
.text:0046EED4
.text:0046EED4 loc_46EED4: // CODE XREF: KiRetireDpcList+5D↑j
.text:0046EED4 push ecx
.text:0046EED5 call ds:_WmiGetCpuClock // WmipGetSystemTime()
.text:0046EEDB pop ecx
.text:0046EEDC mov [esp+24h+var_14], eax
.text:0046EEE0 mov [esp+24h+var_10], edx
.text:0046EEE4 mov edx, [esp+24h+var_24]
.text:0046EEE7 mov [esp+24h+var_C], ecx
.text:0046EEEB jmp short loc_46EE6D
.text:0046EEED // ---------------------------------------------------------------------------
.text:0046EEED
.text:0046EEED loc_46EEED: // CODE XREF: KiRetireDpcList+66↑j
.text:0046EEED mov eax, [esp+14h+var_14]
.text:0046EEF0 mov edx, [esp+14h+var_10]
.text:0046EEF4 push edx
.text:0046EEF5 push eax
.text:0046EEF6 mov ecx, [esp+1Ch+var_C]
.text:0046EEFA call @PerfInfoLogDpc@12 // PerfInfoLogDpc(x,x,x)
.text:0046EEFF jmp loc_46EE76
.text:0046EEFF KiRetireDpcList endp

Amd64:这里大概了解一下就好了,XP bit32 的实现是不一样的,比如就没有调用KiTimerExpiration函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
VOID KiRetireDpcList (
PKPRCB Prcb
)
/*++
Routine Description:
This function processes the DPC list for the specified processor.
N.B. This function is entered with interrupts disabled and exits with
interrupts disabled.
Arguments: Prcb - Supplies the address of the processor block.
Return Value: None.
--*/
{
PKDPC Dpc;
PVOID DeferredContext;
PKDEFERRED_ROUTINE DeferredRoutine;
PLIST_ENTRY Entry;
PLIST_ENTRY ListHead;
PVOID SystemArgument1;
PVOID SystemArgument2;
ULONG TimerHand;

// Loop processing DPC list entries until the specified DPC list is empty.
//
// N.B. This following code appears to have a redundant loop, but it does
// not. The point of this code is to avoid as many dispatch interrupts
// as possible.

ListHead = &Prcb->DpcListHead;
do {
Prcb->DpcRoutineActive = TRUE;

// If the timer hand value is nonzero, then process expired timers.

if ((TimerHand = Prcb->TimerHand) != 0) {
Prcb->TimerHand = 0;
_enable();
KiTimerExpiration(NULL, NULL, UlongToHandle(TimerHand - 1), NULL);
_disable();
}

// If the DPC list is not empty, then process the DPC list.

if (Prcb->DpcQueueDepth != 0) {

// Acquire the DPC lock for the current processor and check if
// the DPC list is empty. If the DPC list is not empty, then
// remove the first entry from the DPC list, capture the DPC
// parameters, set the DPC inserted state false, decrement the
// DPC queue depth, release the DPC lock, enable interrupts, and
// call the specified DPC routine. Otherwise, release the DPC
// lock and enable interrupts.

do {
KeAcquireSpinLockAtDpcLevel(&Prcb->DpcLock);
Entry = Prcb->DpcListHead.Flink;
if (Entry != ListHead) {
RemoveEntryList(Entry);
Dpc = CONTAINING_RECORD(Entry, KDPC, DpcListEntry);
DeferredRoutine = Dpc->DeferredRoutine;
DeferredContext = Dpc->DeferredContext;
SystemArgument1 = Dpc->SystemArgument1;
SystemArgument2 = Dpc->SystemArgument2;
Dpc->Lock = NULL;
Prcb->DpcQueueDepth -= 1;
KeReleaseSpinLockFromDpcLevel(&Prcb->DpcLock);
_enable();
(DeferredRoutine)(Dpc,
DeferredContext,
SystemArgument1,
SystemArgument2);

ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);

_disable();

} else {

ASSERT(Prcb->DpcQueueDepth == 0);

KeReleaseSpinLockFromDpcLevel(&Prcb->DpcLock);
}

} while (ListHead != *((PLIST_ENTRY volatile *)&ListHead->Flink));
}

Prcb->DpcRoutineActive = FALSE;
Prcb->DpcInterruptRequested = FALSE;
} while (ListHead != *((PLIST_ENTRY volatile *)&ListHead->Flink));

return;
}

RemoveEntryList:

1
2
3
4
5
6
7
8
9
10
11
12
#define InsertTailList(ListHead, Entry) \
(Entry)->Flink = (ListHead);\
(Entry)->Blink = (ListHead)->Blink;\
(ListHead)->Blink->Flink = (Entry);\
(ListHead)->Blink = (Entry)

#define RemoveEntryList(Entry) {\
PLIST_ENTRY _EX_Entry;\
_EX_Entry = (Entry);\
_EX_Entry->Blink->Flink = _EX_Entry->Flink;\
_EX_Entry->Flink->Blink = _EX_Entry->Blink;\
}

5.a 练习:DPC定时器

火哥DPC总结:如果一个线程一直在DISPATCH_LEVEL上运行的话,实际上该线程是切不走去执行更低IRQL的代码的。因为线程切换调度是在DISPATCH_LEVEL级别处理的,但是在函数KiDispatchInterrupt中可以看到,如果KiRetireDpcList不将DPC对象的函数处理完,是不会走到下面去执行SwapContext函数的。

即使每一次时钟中断打断一个线程,该线程从CLOCK_LEVEL往下将IRQL时还是会先降到DISPATCH_LEVEL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <ntddk.h>

KDPC dpc = { 0 };
KTIMER ktimer = {0};
LARGE_INTECER dueTime = {0};

VOID dpcCall(
_In_ struct KDPC *Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_optPVOID SvstemArgument2
)
{
DbgPrint("DPC执行了.\n");
KeSetTimer (&ktimer,dueTime,&dpc); //当dueTime时间到期之后,就会去执行这个dpc的回调函数dpcCall
}

VOID UnloadDriver (PDRIVER_OBJECT pDriver)
{
DbgPrint("卸载了.\n");
KeCancelTimer(&ktimer);
}

NTSTATUS DriverEntry (PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
pDriver->DriverUnload = UnloadDriver;
KeInitializeDpc(&dpc, dpeCall, NULL);
KeInitializeTimer (&ktimer);
dueTime. QuadPart = -30 * 1000 * 1000; //负数是相对时间,3秒
KeSetTimer (&ktimer,dueTime,&dpc); //当dueTime时间到期之后,就会去执行这个dpc的回调函数dpcCall

//KeInsertQueueDpc(&dpc, NULL, NULL);

return 0;
}

上面的例子就是每隔3秒就执行一次dpc的回调函数dpcCall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _KTIMER {
DISPATCHER_HEADER Header;
ULARGE_INTEGER DueTime; //指定的到期时间
LIST_ENTRY TimerListEntry; //插入到哪个链表是由 DueTime 决定
struct _KDPC *Dpc;
LONG Period;
} KTIMER, *PKTIMER, *RESTRICTED_POINTER PRKTIMER;

kd> dt _KTIMER
nt!_KTIMER
+0x000 Header : _DISPATCHER_HEADER
+0x010 DueTime : _ULARGE_INTEGER //指定的到期时间
+0x018 TimerListEntry : _LIST_ENTRY //插入到哪个链表是由 DueTime 决定,根据 DueTime 的值,计算得到一个下标 Index 值
+0x020 Dpc : Ptr32 _KDPC
+0x024 Period : Int4B

typedef struct FARSTRUCT _ULARGE_INTEGER {
DWORD LowPart;
DWORD HighPart;
} ULARGE_INTEGER, *PULARGE_INTEGER;