中断服务例程 延迟过程调用 线程切换 键盘信号传输
1. 背景
我一般用ctrl+alt+del能否呼出winlogon桌面作为Windows卡死(hang)还是个别程序卡死的鉴别手段。因为一则用户态的程序没办法干扰这个呼出流程,二则如果不能呼出任务管理器来终止进程或者呼出windbg等工具进行观察调试的话,其实排查的方法也跟windows卡死是一致的——触发scrolllock蓝屏。所以探究ctrl+alt+del呼出winlogon桌面的流程,就是分析这类卡死的第一步。
同样是按键后的反应,为什么在ctrl+alt+del不能呼出的场景下,scrolllock蓝屏还能够触发,也是一个获取关键知识的方向。
2. 键盘信号从按下到应用程序的窗口
大致分为中断服务例程,DPC, csrss,具体应用程序这四个阶段。
2.1 中断服务例程(ISR)
2.1.1 什么是中断服务例程
诸如按下键盘按下或处理器时钟产生的这类设备中断,Windows设置了对应函数(中断服务例程)来处理。Windows在启动的早期阶段先设置好中断号码和ISR的对应关系。当设备中断触发时,Windows先把当前线程的当前状态保存起来,然后用这个线程去执行对应的ISR。执行完毕后再从保存数据恢复那个线程。
2.1.2 查看PS2键盘信号对应的中断服务例程
直接用windbg的!idt查找对应关系
0: kd> .reload;!idt
Dumping IDT: fffff8033ea8e000
00: fffff8033c351100 nt!KiDivideErrorFaultShadow
01: fffff8033c351180 nt!KiDebugTrapOrFaultShadow Stack = 0xFFFFF8033EA929D0
02: fffff8033c351200 nt!KiNmiInterruptShadow Stack = 0xFFFFF8033EA927D0
03: fffff8033c351280 nt!KiBreakpointTrapShadow
……
90: fffff8033c352700 i8042prt!I8042KeyboardInterruptService (KINTERRUPT ffffbb007e792a00)
……
上文的90对应的函数i8042prt!I8042KeyboardInterruptService就是90中断号对应的ISR。给这个函数下断点,
C |
只有当ps2键盘按下或松开时才会执行这个函数。
具体如何通过90和idt寄存器计算出90对应的isr地址fffff803 3c35 2700的,请看下图:
0: kd> dt nt!_kidtentry64 @idtr+(90*10)
+0x000 OffsetLow : 0x2700
+0x002 Selector : 0x10
+0x004 IstIndex : 0y000
+0x004 Reserved0 : 0y00000 (0)
+0x004 Type : 0y01110 (0xe)
+0x004 Dpl : 0y00
+0x004 Present : 0y1
+0x006 OffsetMiddle : 0x3c35
+0x008 OffsetHigh : 0xfffff803
+0x00c Reserved1 : 0
+0x000 Alignment : 0x3c358e00`00102700
bp i8042prt!I8042KeyboardInterruptService"k";g
断点触发时,可以看到此时是idel线程fffff8033c591400调用了这个isr。
C |
这个isr内部会创建一个延迟过程调用(dpc),然后迅速结束isr。为的是确保响应硬件中断的接手工作尽可能快完结,而具体的事项留给其它时机处理这个dpc的时候来做。
2.2 延迟过程调用(DPC)
2.2.1 什么是延迟过程调用(DPC)
类似于一种延迟的任务。先尽快登记上这个任务——把任务插入排队的队列中。之后其它空闲的线程再扫描队列时候发现有任务未处理,那就会来处理它。dpc的有如下两个特点能满足操作系统的要求:
一般对于硬件中断的处理这类需求,需要尽快完成,则先用KeInsertQueueDpc函数登记上dpc,然后完成接手工作。dpc里面登记了个函数地址DeferredRoutine,等其它空闲线程发现有dpc要处理时,则执行此函数。
Windows抽象出中断请求级别(IRQL)的概念,在处理高级别的IRQL时,小于等于它的中断请求就不会处理了。大部分代码运行在被动级别上,线程切换流程里的某个阶段和dpc的DeferredRoutine处理是运行在dpc级别上,硬件中断都更高。dpc的第二个特点就是DeferredRoutine的IRQL又比一般的代码高,所以能尽快不被打扰地处理完毕。
2.2.2 查看PS2键盘ISR对应的DPC
接2.1.2,在该线程给函数nt!KeInsertQueueDpc下断点,这个函数的参数一就是DPC变量的结构体nt!_KDPC
C |
断点触发时,能看到DPC结构体里的数据,尤其是DefferedRoutine:
C |
再给这个DeferredRoutine下断点:
C |
断点触发时,内核调用KiRetireDpcList->KiExecuteAllDpcs->DeferredRoutine:
C |
2.3 DeferredRoutine调用kbdclass.sys进一步处理
执行i8042prt!I8042KeyboardIsrDpc函数,该函数调用i8042prt!I8xGetDataQueuePointer获取键盘端口驱动保存在设备扩展的按键信息队列指针,调用kbdclass!KeyboardClassServiceCallback完成按键信息的交付。然后调用i8042prt!I8xSetDataQueuePointer更新设备扩展的按键信息队列。kbdclass!KeyboardClassServiceCallback类驱动函数处理过程。将键盘信息从端口驱动的键盘信息队列中复制到类驱动的队列中。wdk的例子中有该函数源代码。[2]
将上述函数下断点,可以看到他们依此触发了:
C |
C |
2.3 csrss
csrss.exe进程一般有两个,0号session的和1号session的
C |
这两个进程里都各有一个线程调用win32kfull!RawInputThread,姑且称这两个线程为RawInputThread。
C |
3. 为什么hang witch dpc时,整个操作系统都挂起(卡死)了
因为线程切换的代码也是在dpc级别执行的。所以此时没有机会执行线程切换的代码。所以windows里大部分线程都无法工作。