当产生系统调用、中断或者异常,线程在返回用户空间前都会调用, _KiServiceExit函数,在_KiServiceExit会判断是否有要执行的用户APC,如果有则调用KiDeliverApc函数(第一个参数为1)进行处理。
执行用户APC时的堆栈操作
处理用户APC要比内核APC复杂的多,因为,用户APC函数要在用户空间执行的,这里涉及到大量换栈的操作:
当线程从用户层进入内核层时,要保留原来的运行环境,比如各种寄存 囚器,栈的位置等等(_Trap_Frame),然后切换成内核的堆栈,如果正常返回, 您恢复堆栈环境即可。
但如果有用户APC要执行的话,就意味着线程要提前返回到用户空间去,执行,而且返回的位置不是线程进入内核时的位置,而是返回到其他的位置,每处理一个用户APC都会涉及到:
内核->用户空间->再回到内核空间
堆栈的操作比较复杂,如果不了解堆栈的操作细节不可能理解用户APC是如何执行!
KiDeliverApc函数分析
无论是内核APC还是用户APC首先执行的都是这个函数
- 判断用户APC链表是否为空
- 判断第一个参数是为1
- 判断ApcState.UserApcPending是否为1
- 将ApcState.UserApcPending设置为0
- 链表操作将当前APC从用户队列中拆除
- 调用函数(KAPC.KernelRoutine)释放KAPC结构体内存空间
- 调用KilnitializeUserApc函数
取完内核APC后取用户APC,然后判断你用户APC是不是空的,不为空就跳转。
KilnitializeUserApc函数分析:备份CONTEXT
线程进0环时,原来的运行环境(寄存器栈顶等)保存到Trap_Frame结构体中,如果要提前返回3环去处理用户APC,就必须要修改Trap _Frame结构体:
比如:进0环时的位置存储在EIP中,现在要提前返回,而且返回的并不,是原来的位置,那就意味着必须要修改EIP为新的返回位置。还有堆栈ESP也要修改为处理APC需要的堆栈。那原来的值怎么办呢?处理完APC后该如何返回原来的, 位置呢?
KilnitializeUserApc要做的第一件事就是备份:
将原来Trap Frame的值备份到一个新的结构体中(CONTEXT),这个功能由其子函数KeContextFromKframes来完成。
kd> dt _CONTEXT
nt!_CONTEXT
+0x000 ContextFlags : Uint4B
+0x004 Dr0 : Uint4B
+0x008 Dr1 : Uint4B
+0x00c Dr2 : Uint4B
+0x010 Dr3 : Uint4B
+0x014 Dr6 : Uint4B
+0x018 Dr7 : Uint4B
+0x01c FloatSave : _FLOATING_SAVE_AREA
+0x08c SegGs : Uint4B
+0x090 SegFs : Uint4B
+0x094 SegEs : Uint4B
+0x098 SegDs : Uint4B
+0x09c Edi : Uint4B
+0x0a0 Esi : Uint4B
+0x0a4 Ebx : Uint4B
+0x0a8 Edx : Uint4B
+0x0ac Ecx : Uint4B
+0x0b0 Eax : Uint4B
+0x0b4 Ebp : Uint4B
+0x0b8 Eip : Uint4B
+0x0bc SegCs : Uint4B
+0x0c0 EFlags : Uint4B
+0x0c4 Esp : Uint4B
+0x0c8 SegSs : Uint4B
+0x0cc ExtendedRegisters : [512] UChar
0环备份的CONTEXT复制到3环堆栈
APC要执行的4个值放到图上面堆栈中
1
2.3.4
ntdll.KiUserApcDispatcher分析
1、当用户在3环调用QueueUserAPC函数来插入APC时,不需要提供 NormalRoutine,这个参数是在QueueUserAPC内部指定的:
BaseDispatchAPC
2、 ZwContinue函数的意义:
- 返回内核,如果还有用户APC,重复上面的执行过程。
- 如果没有需要执行的用户APC,会将CONTEXT赋值给Trap
Frame结构体。就像从来没有修改过一样。ZwContinue后面的代码不会执行,线程从哪里进0环仍然会从哪里回去。
总结
- 内核APC在线程切换时执行,不需要换栈,比较简单,一个循环执行完毕。
- 用户APC在系统调用、中断或异常返回3环前会进行判断,如果有要执行的用户APC,再执行。
- 用户APC执行前会先执行内核APC