Windows XP 中断与异常(段内容补充一)
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 |
KTHREAD优先级的描述:
BasePriority
:静态优先级(基本优先级),继承至进程的BasePriority
。Priority
:动态优先级,运行在动态优先级类别中的线程,它们的实际优先级可能会根据特定的情形而作调,它往往是在BasePriority
域的基础上提升一定的优先级增量(使用函数KeSetPriorityAndQuantumProcess
、KeSetBasePriorityThread
)。但是,无论怎么调整,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传递过来的中断源。
2.3 APIC的功能
一个中断的起末会经历设备,中断控制器,CPU 三个阶段:设备产生中断信号,中断控制器翻译信号,CPU 来实际处理信号。
LAPIC 和 I/O APIC的功能很复杂,其中对于本节的学习最重要的就是:将中断信号翻译成中断向量,使得每一个中断都能够对应一个中断向量。
本地 APIC 的初始化工作是在 Windows 体系结构的 HAL模块中完成的。
可参考:一文讲透!Windows内核 & x86中断机制详解、再谈中断(APIC)、【x86架构】APIC – 高级可编程中断控制器、X86中断/异常与APIC。
2.4 中断向量表
当一个中断信号被转换为指定的中断向量之后,CPU会根据这个中断向量去执行对应的系统服务(函数)。系统初始化时使用KiSystemStartup
函数来完成相关的工作。
- Intel X86 一共定义了 256 个中断向量编号(也称为中断向量),从0~255。中断向量表也称为中断描述符表(IDT, Interrupt Descriptor Table)。
- IDT 将每个中断或异常与一个处理该中断或异常的函数程关联了起来。
- 与 GDT 和 LDT 类似,IDT 中的每一项是一个 8 字节的描述符。
- 处理器内部的 IDTR 寄存器记录了 IDT 的位置和它的最大限制。
LIDT
和SIDT
指令分别用于加载和存储 IDTR 寄存器。LITD
只有在特权级 0 下才可以执行。
下图反应如何将INT N
转换为指定函数地址的转换(注意IDT段描述符中的段选择子是一个对应GDT代码段的描述符):
具体见《Windows XP 段保护(三)》中断门。
中断和异常的差异:中断是异步的,异常是同步的。
中断向量:
- 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)
。
中断号的具体的分配情况:
中断号(十进制/十六进制) | 分配情况 |
---|---|
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 |
IRQL | IQRL级别 | 描述 |
---|---|---|
0 | PASSIVE_LEVEL | 用户模式代码(或驱动代码)运行在次最低级别上。 |
1 | APC_LEVEL | APC异步过程调用 |
2 | DISPATCH_LEVEL | 一般是线程调度器正在运行,如KiDispatchInterrupt 、SwapContext 。或者正在处理硬件中断的后半部分。注意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 ,以屏蔽所有其他的中断。 |
- Windows 总是运行在一个高度并发的环境中,任何时候,每个处理器都运行在某一IRQL 上。当中断发生时,处理器把被中断的线程的状态保存起来,然后调用 IDT中设定的中断例程。
- 一个处于内核模式中的线程根据它所要做的事情来提升
KeRaiselrql
或者降低KeLowerlrql
当前处理器的 IRQL。尤其是,当线程要访问全局的数据结构或执行关键的任务时,必须考虑是否提升IRQL,在完成以后,再降低IRQL。提升 IRQL可以屏较低 IRQL 的线程或中断。 - 但是,作为内核代码,它总是应该尽可能地保持当前处理器的 IRQL 在
PASSIVE_LEVEL
上,这
样用户代码才有机会得以运行。 KPCR->irql
记录当前CPU所处的IRQL。
关于DISPATCH_LEVEL
需要注意:
DISPATCH_LEVEL
是最高的软件中断级别,但是又要比所有的硬件中断IRQL低。
- 不能做任何等待动作:如果某个线程已经运行在这个级别,则不得切换到其他线程(比如,等待一个同步对象,当前IRQL得线程不能等其他低级别线程等待的被等待对象,换句话说就是不能做任何等待动作),因为线程切换是通过系统调度器来完成的,而系统调度器运行在
DISPATCH_LEVEL
上。 - 不能访问换页内存区:因为一旦发生换页动作,就需要执行 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 | kd> dt _KINTERRUPT |
参数 | 解释说明 |
---|---|
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建立的过程:
- PNP设备(即插即用设备)在插入系统后,相应的总线驱动会自动为其创建一个用作栈底基石的PDO,然后给这个PDO发出一个
IRP_MN_QUERY_RESOURCE_REQUIREMENTS
,查询得到初步的资源需求。 - 然后,PNP管理器会找到相应的硬件端口驱动,调用其
AddDevice
函数,当这个函数返回后,该硬件设备的设备栈已经建立起立了,PNP管理器就给栈顶设备发出一个IRP_MN_FILTER_RESOURCE_REQUIREMENTS
再次询问该硬件需要的资源(功能驱动此时可以拦截处理这个IRP,修改资源需求),当确定好最终的资源需求后,系统就协调分配端口号、中断号、DIRQL等硬件资源给它,即分配资源。 - 分配资源完后,就发出一个
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
封装了 Kelnitializelnterrupt
和 KeConnectInterrupt
这两个内核函数。会先后调用 Keinitializelnterrupt
和 KeConnectinterrupt
,前者是 KINTERRUPT
对象结构的初始化,后者将其连接到系统的中断向量及中断分发函数。
IoConnectInterrupt
需要说明的两个参数:
- ProcessorEnableMask:是个位图。在多处理器系统中,位图中的各个标志位代表着不同的 CPU。如果
某位为1,就说明中断服务程序可以在这个 CPU 上执行。 - KeActiveProcessors:是个位图,说明系统中实际具有哪一些 CPU。二者的交集就是可以执行给定中断服务程序而又实际存在
的 CPU 的集合,这个集合当然不能为空。
IoConnectInterrupt
函数处理流程:
KeInitializeInterrupt
:除了将参数中的值赋给中断对象以外,还会将KeInterruptTemplate
数组中的指令拷贝到中断对象的DispatchCode
成员中,并且修正其中的一条指令,使其指向当前中断对象。- 中断对象在经过初始化以后,便可以调用
KeConnectlnterrupt
函数,将它挂到中断对象中指定的中断向量的 IDT 项中。 KeConnectlnterrupt
用到了两个辅助函数KiConnectVectorAndlnterruptObject
和KiGetVeetorinfo
,并通过数据结构DISPATCHL_INFO
来传递中断设置。其中KiConnectVectorAndInterruptObject
函数负责把中断对象的DispatchCode
代码中的jmp _KeSynchronizeExecution
指令的目标地址修正为与该中断向量相匹配的中断分发函数。系统提供的中断分发两数为:KiInterruptDispatch
、KiFloatingDispatch
和KiChainedDispatch
,或者KilnterruptDispatch2ndLyl
、KilnterruptDispatch2ndLvl
和KiChainedDispatch2ndLvl
。- 其中后三个分发函数针对二级分发,由 HAL 决定。链式中断分发函数,即
KiChainedDispatch
和KiChainedDispatch2ndLvl
的流程是:针对链表中的每个中断对象,提升 IRQL,获得服务例程自旋锁(即中断对象的 ActualLock 域),调用中断对象的服务例程,然后释放自旋锁,恢复 IRQL。 - 对于 LevelSensitive 模式的中断,一旦链表上有一个中断对象的中断服务例程已经处理了一个中断,则后面的中断对象不再有机会处理该中断;而对于 Latched模式的中,中断分发函数会遍历整个链表,因而链表上所有中断对象都可以处理该中断。
KiConnectVectorAndlnterruptObject
函数通过C宏KiSetHandlerAddressToIDT
将中断对象中的中断分发函数插人到 IDT中。一旦KeConnectInterrupt
函数完成这一步并打开中断开关,以后当中断发生时,该中断分发函数就会被调用。
整个流程可参考:
初始化和连接工作完成后,以后只要中断一触发,就会首先调用 DispatchCode
成员,在该成员指向的代码中会jmp
到对应的KiInterruptDispatch
,然后分发(Dispatch)进入预设的中断服务程序(ISR)。
基于上述关于中断机制的介绍,我们可以知道,当一个中断发生时,其处理过程如下:
- 中断对象的
DispatchCode
代码块首先获得控制,它把该代码块所在的中断对象放到edi寄存器中,然后跳转到该中断向量对应的中断分发函数。 - 在中断分发函数中,它提升 IRQL,获取自旋锁,然后调用中断对象的服务例程,即 KINTERRUPT 的
ServiceRoutine
成员,待返回以后,释放自旋锁,恢复IRQL。依据不同的中断分发函数以及中断对象的模式,可能还会处理中断对象链表上的其他中断
对象,如上文所介绍。 - 最后,中断分发函数通过
Kei386EoiHelper
辅助函数返回。Kei386EoiHelper
中的iretd
指令结束整个中断处理。
图5.7显示了两个中断对象连接到同一个中断向量情形下的中断控制流程。图中的实线箭头代表了实际的控制流,虚线箭头代表了指针关系。
可以参考:windows内核情景分析—中断处理。
4.3 IoConnectInterrupt
1 | NTSTATUS IoConnectInterrupt( |
函数 KeInitializeInterrupt
:
1 | VOID KeInitializeInterrupt ( |
函数KeConnectInterrupt
:
1 | BOOLEAN KeConnectInterrupt(IN PKINTERRUPT Interrupt) |
4.4 总结
中断服务函数 ISR 是在设备 IRQL(DIRQL)级别上执行的。
5 DPC
DPC:延迟过程调用,Deferred Procedure Call。运行在 DISPATCH_LEVEL
级别。
DIRQL > DISPATCH_LEVEL
,DPC 执行时屏蔽了线程调度(线程调度在DISPATCH_LEVEL
)。
5.1 DPC的作用
使用 DPC 得主要原因有以下:
- 一般而言,整个中断服务程序 ISR 通常都是在关中断
cli
的条件下执行的,关中断的时间应该尽可能的短,如果关闭中断的时间太长会引起中断请求的丢失。 - 逻辑上应该放在 ISR 中完成的操作但并非那么紧迫、费时间、可以在开中断 sti 下执行的代码单独放到另一个函数中。这个函数就叫做 DPC 函数。
- 通常把在 ISR 中执行的代码成为中断服务的上半段,在 DPC 函数中执行的代码称作中断服务的下半段。
- 中断服务函数 ISR 是在
DIRQL
级别上执行的,DPC 是在DISPATCH_LEVEL
上执行。
DPC 函数的执行时机:
- 为此,内核中要有个 DPC 请求队列,中断服务程序执行完它的上半段之后就把一个 DPC 请求挂入这个队列,要求内核调用相应的 DPC 函数,然后(形式上)就从中断返回了。接着,如果没有别的中断请求,内核就会扫描这个 DPC 请求队列,依次在开中断的条件下执行这些 DPC 函数,直至又发生中断或执行完队列中的所有 DPC 函数。
- 至于当前线程所要执行的程序,则只有在 DPC 请求队列为空的时候才会继续得到执行。中断是最急的,DPC 函数其次,最后才是当前线程。
DPC 函数的堆栈:
- ISR 函数较小,使用的是当前线程的堆栈空间。DPC 函数执行时会单独开辟一个函数堆栈。
5.2 _KDPC
1 | kd> dt _KDPC |
源码定义如下:
1 | typedef struct _KDPC { |
参数 | 解释说明 | |
---|---|---|
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 |
DPC 是跟CPU相关的,APC 是跟线程相关的。
⚠️注意:《Windows内核原理与实现》与《Windows内核情景分析》中说有两种 DPC 对象,即 DpcObject
或 ThreadedDpcObject
,并且还说 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 | VOID __stdcall KeInitializeDpc ( |
5.4 KeInsertQueueDpc
1、DPC 对象的用法很简单,使用者先拿着已有的资源(参数)调用 KeInitializeDpc
函数来对 DPC 对象进行初始化。初始化完成后再调用函数 KeInsertQueueDpc
将这个 DPC 对象插入到指定 KPRCB 成员 DpcListHead
中去。
2、KeInsertQueueDpc
函数中,DPC 对象已经插入喉,会像 APC 那样检查一下刚才插入的 DPC 能否执行,如果条件满足就会触发中断,hal.dll
对中断处理后都会调用NTOS的函数KiDispatchInterrupt
去执行DPC
1 | BOOLEAN __stdcall KeInsertQueueDpc ( |
5.5 KeRemoveQueueDpc
1 | BOOLEAN __stdcall KeRemoveQueueDpc ( |
5.6 DPC 交付
根据在插入时是否触发软件中断,DPC 对象在以下三种情况下被交付:
- 当处理器的 IRQL 从 DISPATCH_LEVEL 或更高级别降低到 APC_LEVEL 或 PASSIVE_LEVEL 时,内核开始处理该处理器的 DPC 链表中的 DPC 对象,依次调用链表中 DPC 对象的延迟雨数,直至链表为空。
- 通过 KelnsertQueueDpe 插入 DPC 对象时,如果依据 DPC 对象的重要程度,以及目标处理器 DPC 链表中的 DPC 数量或请求率,有必要立刻请求一个 DISPATCH_LEVEL 的软件中断,那么,DPC 链表中的 DPC 对象将立即有机会获得处理。
- 在各个处理器的空闲线程中,如果发现有 DPC 对象尚未被执行,则交付这些 DPC 对象。参见3.4.5 节关于空闲循环的介绍。
在前两种情况下,HAL 调用内核中的 KiDispatchInterrupt 函数(注意,这不同于中断对象分发函数 KilnterruptDispatch ),KiDispatchInterrupt 函数进而调用 KiRetireDpcList。在上述第三种情况下,KildleLoop 调用 KiRetireDpcList 函数。关于KiDispatchInterrupt 和 KildleLoop 函数的代码,参见 baselntos lketi386lctxswap.asm 文件。
5.7 HalRequestSoftwareInterrupt
在
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 endpSWInterruptHandlerTable
如下: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 // ÿ在
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 | .text:0046E9B0 // Exported entry 639. KiDispatchInterrupt |
5.9 KiRetireDpcList
1 | .text:0046EE0E // =============== S U B R O U T I N E ======================================= |
Amd64:这里大概了解一下就好了,XP bit32 的实现是不一样的,比如就没有调用KiTimerExpiration
函数。
1 | VOID KiRetireDpcList ( |
RemoveEntryList:
1 |
|
5.a 练习:DPC定时器
火哥DPC总结:如果一个线程一直在DISPATCH_LEVEL
上运行的话,实际上该线程是切不走去执行更低IRQL
的代码的。因为线程切换调度是在DISPATCH_LEVEL
级别处理的,但是在函数KiDispatchInterrupt
中可以看到,如果KiRetireDpcList
不将DPC对象的函数处理完,是不会走到下面去执行SwapContext
函数的。
即使每一次时钟中断打断一个线程,该线程从CLOCK_LEVEL
往下将IRQL
时还是会先降到DISPATCH_LEVEL
。
1 |
|
上面的例子就是每隔3秒就执行一次dpc的回调函数dpcCall
。
1 | typedef struct _KTIMER { |