中断服务例程 延迟过程调用 线程切换 键盘信号传输
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 bp i8042prt!I8042KeyboardInterruptService "!thread" |
只有当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 THREAD fffff8033c591400??Cid 0000.0000 ?Teb: 0000000000000000?Win32Thread: 0000000000000000 RUNNING on processor 0 Not impersonating DeviceMap ????????????????ffff9388274154e0 Owning Process ???????????fffff8033c58e9c0???????Image: ????????Idle …… Call Site i8042prt!I8042KeyboardInterruptService nt!KiCallInterruptServiceRoutine+0xa5 nt!KiInterruptSubDispatch+0x11f nt!KiInterruptDispatch+0x37 hal!HalProcessorIdle+0xf nt!PpmIdleDefaultExecute+0x1b nt!PpmIdleExecuteTransition+0x70c nt!PoIdle+0x36e nt!KiIdleLoop+0x48 |
这个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 bp /t ?fffff8033c591400 nt!KeInsertQueueDpc "dt nt!_KDPC @rcx" |
断点触发时,能看到DPC结构体里的数据,尤其是DefferedRoutine:
C +0x000 TargetInfoAsUlong : 0x113 ???+0x000 Type ????????????: 0x13 '' ???+0x001 Importance ??????: 0x1 '' ???+0x002 Number ??????????: 0 ???+0x008 DpcListEntry?????: _SINGLE_LIST_ENTRY ???+0x010 ProcessorHistory : 1 ???+0x018 DeferredRoutine??: 0xfffff8033fe45fb0 ????void ?i8042prt!I8042KeyboardIsrDpc+0 ???+0x020 DeferredContext ?: 0xffffd0036c0d1040 Void ???+0x028 SystemArgument1 ?: (null) ???+0x030 SystemArgument2 ?: (null) ???+0x038 DpcData ?????????: (null) |
再给这个DeferredRoutine下断点:
C bd 0,1;bp I8042KeyboardIsrDpc "!thread" |
断点触发时,内核调用KiRetireDpcList->KiExecuteAllDpcs->DeferredRoutine:
C THREAD fffff8033c591400??Cid 0000.0000 ?Teb: 0000000000000000?Win32Thread: 0000000000000000 RUNNING on processor 0 Not impersonating DeviceMap ????????????????ffff9388274154e0 Owning Process ???????????fffff8033c58e9c0???????Image: ????????Idle …… Call Site i8042prt!I8042KeyboardIsrDpc nt!KiExecuteAllDpcs+0x305 nt!KiRetireDpcList+0x1ef nt!KiIdleLoop+0x84 |
2.3 DeferredRoutine调用kbdclass.sys进一步处理
执行i8042prt!I8042KeyboardIsrDpc函数,该函数调用i8042prt!I8xGetDataQueuePointer获取键盘端口驱动保存在设备扩展的按键信息队列指针,调用kbdclass!KeyboardClassServiceCallback完成按键信息的交付。然后调用i8042prt!I8xSetDataQueuePointer更新设备扩展的按键信息队列。kbdclass!KeyboardClassServiceCallback类驱动函数处理过程。将键盘信息从端口驱动的键盘信息队列中复制到类驱动的队列中。wdk的例子中有该函数源代码。[2]
将上述函数下断点,可以看到他们依此触发了:
C bd 2; bp /t fffff8033c591400 i8042prt!I8xGetDataQueuePointer "k"; bp /t fffff8033c591400 kbdclass!KeyboardClassServiceCallback "k"; bp /t fffff8033c591400 i8042prt!I8xSetDataQueuePointer "k" |
C i8042prt!I8xGetDataQueuePointer nt!KeSynchronizeExecution+0x48 i8042prt!I8042KeyboardIsrDpc+0x109 nt!KiExecuteAllDpcs+0x305 nt!KiRetireDpcList+0x1ef nt!KiIdleLoop+0x84 …… kbdclass!KeyboardClassServiceCallback i8042prt!I8042KeyboardIsrDpc+0x2f8 nt!KiExecuteAllDpcs+0x305 nt!KiRetireDpcList+0x1ef nt!KiIdleLoop+0x84 …… i8042prt!I8xSetDataQueuePointer nt!KeSynchronizeExecution+0x48 i8042prt!I8042KeyboardIsrDpc+0x397 nt!KiExecuteAllDpcs+0x305 nt!KiRetireDpcList+0x1ef nt!KiIdleLoop+0x84 |
2.3 csrss
csrss.exe进程一般有两个,0号session的和1号session的
C 0: kd> !process 0 0 csrss.exe PROCESS ffffca057479e080 ????SessionId: 0 ?Cid: 019c ???Peb: 162aa55000??ParentCid: 0194 ????DirBase: 01fba002 ?ObjectTable: ffffdf0200b584c0 ?HandleCount: 537. ????Image: csrss.exe
PROCESS ffffca0574b59080 ????SessionId: 1 ?Cid: 01f4 ???Peb: 8216b90000??ParentCid: 01e0 ????DirBase: 131bc7002 ?ObjectTable: ffffdf0200bfce40 ?HandleCount: 352. ????Image: csrss.exe |
这两个进程里都各有一个线程调用win32kfull!RawInputThread,姑且称这两个线程为RawInputThread。
C THREAD ffffca0574b6a080 Owning Process ???????????ffffca057479e080???????Image: ????????csrss.exe Call Site nt!KiSwapContext+0x76 nt!KiSwapThread+0xbfd nt!KiCommitThreadWait+0x144 nt!KeWaitForMultipleObjects+0x287 win32kbase!LegacyInputDispatcher::WaitAndDispatch+0x8b win32kfull!RawInputThread+0x95e win32kbase!xxxCreateSystemThreads+0xa3 win32kfull!NtUserCallNoParam+0x6f nt!KiSystemServiceCopyEnd+0x28 (TrapFrame @ fffff20cb6217a80) 0x00007ffca6f81144 …… THREAD ffffca0574b8c080 Owning Process ???????????ffffca0574b59080 ??????Image: ????????csrss.exe Call Site nt!KiSwapContext+0x76 nt!KiSwapThread+0xbfd nt!KiCommitThreadWait+0x144 nt!KeWaitForMultipleObjects+0x287 win32kbase!LegacyInputDispatcher::WaitAndDispatch+0x8b win32kfull!RawInputThread+0x95e win32kbase!xxxCreateSystemThreads+0xa3 win32kfull!NtUserCallNoParam+0x6f nt!KiSystemServiceCopyEnd+0x28 (TrapFrame @ fffff20c`b6407a80) 0x00007ffc`a6f81144 |
3. 为什么hang witch dpc时,整个操作系统都挂起(卡死)了
因为线程切换的代码也是在dpc级别执行的。所以此时没有机会执行线程切换的代码。所以windows里大部分线程都无法工作。