戏说IRQL(2)
做软件是多么好的一个工作啊,但有时,做软件又是多么差的一个工作啊!哈哈,脑海中突然冒出这么两句,权作这个IRQL系列的第二集的开篇吧!
书接上回,继续讲上次的试验。请出WinDBG,附加到目标系统,开始内核调试,kn:
kd> knL
# ChildEBP RetAddr
00 8af1fa50 83093bfb nt!RtlpBreakWithStatusInstruction
01 8af1fa58 83093bcd nt!KdCheckForDebugBreak+0x22
02 8af1fa88 83093a5b nt!KeUpdateRunTime+0x164
03 8af1fae0 830983e3 nt!KeUpdateSystemTime+0x613
04 8af1fae0 9574b43c nt!KeUpdateSystemTimeAssist+0x13
05 8af1fb6c 9574baf0 RealBug!RoamAtIRQL+0x1c
06 8af1fb90 9574bcf0 RealBug!RealBugDeviceControl+0xc0
07 8af1fbdc 8335e6c3 RealBug!RealBugDispatch+0x90
08 8af1fc00 83069efb nt!IovCallDriver+0x258
09 8af1fc14 8323f2e7 nt!IofCallDriver+0x1b
0a 8af1fc34 832416da nt!IopSynchronousServiceTail+0x1f8
0b 8af1fcd0 83248727 nt!IopXxxControlFile+0x6aa
0c 8af1fd04 8307079a nt!NtDeviceIoControlFile+0x2a
0d 8af1fd04 775c64f4 nt!KiFastCallEntry+0x12a
栈帧4和5之间明显有中断的痕迹,其实就是著名的时钟中断,#5就是我们上期提到的RoamAtIRQLRoamAtIRQL函数,在中断发生前,CPU在那里转圈,但尽管是在26这样的高IRQL转圈,因为时钟中断具有更高的IRQL(28),所以还是将RoamAtIRQLRoamAtIRQL函数打断了,令CPU跳出循环去执行时钟中断。
执行!pcr命令观察CPU的控制区:
kd> !pcr
KPCR for Processor 0 at 83140c00:
Major 1 Minor 1
NtTib.ExceptionList: 8aec130c
NtTib.StackBase: 00000000
NtTib.StackLimit: 00000000
NtTib.SubSystemTib: 801c8000
NtTib.Version: 0001ca27
NtTib.UserPointer: 00000001
NtTib.SelfTib: 7ffdf000
SelfPcr: 83140c00
Prcb: 83140d20
Irql: 0000001f
IRR: 00000004
IDR: ffff2070
InterruptMode: 00000000
IDT: 80b95400
GDT: 80b95000
TSS: 801c8000
CurrentThread: 910a3030
NextThread: 00000000
IdleThread: 8314a240
DpcQueue:
注意其中的IRQL字段,这就是记录在CPU控制区中的IRQL值,也是常常把IRQL说成是CPU属性的一个原因。
Irql: 0000001f
但是细心的读者可能立刻生出一个疑问,为什么是1f(31),而不是26呢?
原因是,现在已经停在调试器了,在中断到调试器时,内核调试引擎会提升IRQL到HIGH_LEVEL(X86上也就是31),最高级别。因为这个原因,在软件调试器中观察时,CPU控制区中记录的IRQL永远是31。为了解决这个问题,可以观察调试引擎提升IRQL之前的本来IRQL,从Server 2003开始,会故意将老的IRQL值保存在PCR的DebuggerSavedIRQL字段中,并可以通过!irql这个扩展命令将其读出来:
执行!irql命令:
kd> !irql
Debugger saved IRQL for processor 0x0 -- 26
直接观察,也可以看到:
kd> dd 83140d20+4c4
831411e4 0000001a
那么为什么CPU在IRQL 26兜圈时,系统就表现出挂死的症状呢?原因是鼠标键盘中断对应的IRQL都是属于设备IRQL范围,都是低于26的,这意味着鼠标键盘中断都被屏蔽了。另一个重要的原因是普通线程的IRQL是0,因此绘制窗口这样的代码根本没机会执行。
IRQL 0有很多个别名,其中之一叫PASSIVE_LEVEL,被动级别,何谓被动级别,意思是它只能被动等待被执行,从来没有机会去主动抢夺CPU的执行权。IRQL 0的另一个常见别名叫LOW_LEVEL,与最高级别的HIGH_LEVEL相对应。
是时候把所有的IRQL定义请出来了,在WDK的头文件中就可以找到它们:
// wdm.h
#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 CMCI_LEVEL 5 // CMCI handler 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
#define CLOCK_LEVEL (CLOCK2_LEVEL)
看这个列表,通过前面的实验,我们对HIGH_LEVEL、CLOCK_LEVEL和一般分给硬件设备中断的IRQL 26已经有所认识了。 对于前几个可能还有一些疑问,尤其是2和1这两个级别,它们被统称为软件中断级别,我们下一次继续讲。