ReactOS SYSCALL_PROLOG/TRAP_EPILOG及相关代码注释 (1) 一文中提到了KTRAP_FRAME:CPU从内核返回用户空间时,通过宏TRAP_EPILOG,恢复这个结构中的Eip返回到被中断的指令继续执行。一些Win API 如ReadFileEx提供了完成函数,当异步读取完成后调用的回调函数,执行完回调函数后再返回到原有流程继续执行;再换个思路,如果修改返回地址Eip,是否会跳转到其他地方执行?系统是否提供类似机制?查看ReactOS源码发现,APC请求实现了类似功能:当从内核返回,系统检查是否有APC请求,如果有先跳去请求中的回调函数,执行完成后通过ZwContinue重新返回内核空间,再通过KiServiceExit2函数返回到被中断的用户态代码继续执行。下面围绕这些函数及相关结构展开注释。
如上文所述,执行APC的时机是在内核返回到用户空间的途中,如下:
.func KiServiceExit _KiServiceExit: /* Disable interrupts */ cli /* Check for, and deliver, User-Mode APCs if needed */ CHECK_FOR_APC_DELIVER 1 //如果有APC,经过上面宏,会把KTRAP_FRAME中的中断返回地址eip改为指向_KiUserApcDispatcher, //随后执行TRAP_EPILOG中iret时,返回到_KiUserApcDispatcher /* Exit and cleanup */ TRAP_EPILOG FromSystemCall, DoRestorePreviousMode, DoNotRestoreSegments, DoNotRestoreVolatiles, DoRestoreEverything .endfunc
CHECK_FOR_APC_DELIVER的作用可参看毛德操的情节分析,源码中有一段:
/* Save pointer to Trap Frame */ mov ebx, ebp源码中注释为将ebp指向的KTRAP_FRAME保存至ebx中。ebp什么时候指向KTRAP_FRAME的?当发生中断等 进入SYSCALL_PROLOG后,经过为内核堆栈上分配保存寄存器值得空间后执行mov ebp,esp语句,之后ebp一直指向KTRAP_FRAME结构,如果期间进入其他内核函数也会保存/恢复ebp,因此在_KiServiceExit中ebp依然指向进入内核时的KTRAP_FRAME结构。
CHECK_FOR_APC_DELIVER中如果发现有用户APC请求就进入KiDeliverApc投递一个APC,即通过KiInitializeUserApc准备执行APC。接下来看看KiInitializeUserApc实现:
VOID NTAPI KiInitializeUserApc(IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME TrapFrame, IN PKNORMAL_ROUTINE NormalRoutine, IN PVOID NormalContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2) { CONTEXT Context; ULONG_PTR Stack, AlignedEsp; ULONG ContextLength; EXCEPTION_RECORD SehExceptRecord; _SEH_DECLARE_LOCALS(KiCopyInfo); /* Don't deliver APCs in V86 mode */ if (TrapFrame->EFlags & EFLAGS_V86_MASK) return; /* Save the full context */ Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context); /* Protect with SEH */ _SEH_TRY { /* Sanity check */ ASSERT((TrapFrame->SegCs & MODE_MASK) != KernelMode); /* Get the aligned size */ /* 扩充用户栈空间,用以创建Context变量。 1.KTRAP_FRAME保存了发生中断时,用户态寄存器。其中KTRAP_FRAME->HarewareEsp存放了 中断时用户空间的堆栈情况,即变量分布。调用KeTrapFrameToContext时,将这些 寄存器值保存到Context变量中。 2.修改Context.Esp,即在现有用户空间上添加若干变量。 */ AlignedEsp = Context.Esp & ~3; ContextLength = CONTEXT_ALIGNED_SIZE + (4 * sizeof(ULONG_PTR)); Stack = ((AlignedEsp - 8) & ~3) - ContextLength; /* Probe the stack */ ProbeForWrite((PVOID)Stack, AlignedEsp - Stack, 1); ASSERT(!(Stack & 3)); /* Copy data into it */ RtlCopyMemory((PVOID)(Stack + (4 * sizeof(ULONG_PTR))), &Context, sizeof(CONTEXT)); /* Run at APC dispatcher */ /* TrapFrame->Eip = (ULONG)KeUserApcDispatcher; 设置TrapFrame中eip域。后面TRAP_EPILOG中会用TrapFrame 中的域恢复中断/异常发生时的寄存器值,其中在TRAP_EPILOG .if syscall 部分有如下几句 pop edx pop ecx popf jmp edx 将TrapFrame->Eip恢复到edx中然后jmp ebx ,即跳转到KeUserApcDispatcher执行 _KiServiceExit用TrapFrame中保存的值恢复寄存器,然后从内核态 恢复到上次发生中断时的状态。_KiServiceExit认为上次是在KeUserApcDispatcher入口 发生中断,因此,退出时返回到Ring3:KeUserApcDispatcher */ TrapFrame->Eip = (ULONG)KeUserApcDispatcher; TrapFrame->HardwareEsp = Stack; /* Setup Ring 3 state */ TrapFrame->SegCs = Ke386SanitizeSeg(KGDT_R3_CODE, UserMode); TrapFrame->HardwareSegSs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode); TrapFrame->SegDs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode); TrapFrame->SegEs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode); TrapFrame->SegFs = Ke386SanitizeSeg(KGDT_R3_TEB, UserMode); TrapFrame->SegGs = 0; TrapFrame->ErrCode = 0; /* Sanitize EFLAGS */ TrapFrame->EFlags = Ke386SanitizeFlags(Context.EFlags, UserMode); /* Check if thread has IOPL and force it enabled if so */ if (KeGetCurrentThread()->Iopl) TrapFrame->EFlags |= 0x3000; /* Setup the stack */ *(PULONG_PTR)(Stack + 0 * sizeof(ULONG_PTR)) = (ULONG_PTR)NormalRoutine; *(PULONG_PTR)(Stack + 1 * sizeof(ULONG_PTR)) = (ULONG_PTR)NormalContext; *(PULONG_PTR)(Stack + 2 * sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument1; *(PULONG_PTR)(Stack + 3 * sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument2; } _SEH_EXCEPT(KiCopyInformation2) { /* Dispatch the exception */ _SEH_VAR(SehExceptRecord).ExceptionAddress = (PVOID)TrapFrame->Eip; KiDispatchException(&SehExceptRecord, ExceptionFrame, TrapFrame, UserMode, TRUE); } _SEH_END; }KiInitializeUserApc的入口参数TrapFrame依然是退出中断时ebp指向的KTRAP_FRAME结构,而指针ExceptionFrame传入的值是0。
查看代码逻辑,KiInitializeUserApc主要做两件事:1.修改TrapFrame->Eip从而达到修改返回到指定地址的目的。但是如果不把原Eip的值加以保存,在适当的契机予以恢复,就再也没有机会返回到原来被中断的指令上去执行。而KeTrapFrameToContext正提供了保存原TrapFrame中各域到Context的功能,以后借Context的尸还魂到被中断的代码中(Context是大蛇丸秽土转生用的活人了?)。2.既然KeTrapFrameToContext保存了各寄存器的值,也必然保存了栈顶寄存器esp的值。KiInitializeUserApc扩充原用户空间的堆栈,往其上新添加了几个变量,这几个变量是提供给KeUserApcDispatcher的参数。当从KiDeliverApc返回,遇到iret指令时,CPU执行到KeUserApcDispatcher的入口,就像是内核主动调用KeUserApcDispatcher似得。函数调用发生了,函数的参数一般在堆栈上,而自从内核返回到用户空间,堆栈空间也从内核栈切换到了用户栈。而之前KiInitializeUserApc修改了用户栈栈顶,因此现在栈顶内存中的变量就是提供给KeUserApcDispatcher的参数。
现在执行到KeUserApcDispatcher中,反正闲着也是闲着,跟过去看看它的实现:
.func KiUserApcDispatcher@16 .globl _KiUserApcDispatcher@16 _KiUserApcDispatcher@16: /*本函数在R3*/ /* Setup SEH stack */ lea eax, [esp+CONTEXT_ALIGNED_SIZE+16] mov ecx, fs:[TEB_EXCEPTION_LIST] mov edx, offset _KiUserApcExceptionHandler mov [eax], ecx mov [eax+4], edx /* Enable SEH */ mov fs:[TEB_EXCEPTION_LIST], eax /* Put the Context in EDI */ pop eax lea edi, [esp+12] /* Call the APC Routine */ call eax /* Restore exception list */ mov ecx, [edi+CONTEXT_ALIGNED_SIZE] mov fs:[TEB_EXCEPTION_LIST], ecx /* Switch back to the context */ push 1 //lea edi,[esp+12] edi指向KiInitializeUserApc中预留的Context,该Context保存了R3发生中断时真正的eip push edi //call ZwContinue,又发起一次系统调用,所有的系统调用有段共有的代码,会在内核栈上形成一个KTRAP_FRAME,同时ebp指向该KTRAP_FRAME //然后,进入ZwContinue进行该API自身操作---_NtContinue@8 call _ZwContinue@8 /* Save callback return value */ mov esi, eax /* Raise status */ StatusRaiseApc: push esi call _RtlRaiseStatus@4 jmp StatusRaiseApc ret 16 .endfunc进入_KiUserApcDispatcher时已经在R3,可以执行预留在R3 APC中的回调函数。call eax就是跳去执行回调函数。执行完回调函数,通过
call _ZwContinue@8重新进入内核空间,这次进入内核空间又会形成新的KTRAP_FRAME结构,看下ZwContinue的实现
如果是通过_KiUserApcDispatcher调用_NtContinue进入: _KiUserApcDispatcher本身是在R3,call _ZwContinue时,会形成一个KTRAP_FRAME,同时ebp指向该结构 */ .func NtContinue@8 _NtContinue@8: /* NOTE: We -must- be called by Zw* to have the right frame! */ /* Push the stack frame */ push ebp /* Get the current thread and restore its trap frame */ mov ebx, PCR[KPCR_CURRENT_THREAD] /* [ebp+KTRAP_FRAME_EDX]保存中断前的ebp框架,嵌套中断*/ mov edx, [ebp+KTRAP_FRAME_EDX] /* 这是恢复以前的框架? */ mov [ebx+KTHREAD_TRAP_FRAME], edx /* Set up stack frame */ /*入口处push ebp说是保存框架,mov ebp,esp又是保存框架*/ mov ebp, esp /* Save the parameters */ mov eax, [ebp+0] //堆栈框架 mov ecx, [ebp+8] //CONTEXT /* Call KiContinue */ push eax push 0 push ecx call _KiContinue@12 /* Check if we failed (bad context record) */ or eax, eax jnz Error /* Check if test alert was requested */ cmp dword ptr [ebp+12], 0 je DontTest /* Test alert for the thread */ mov al, [ebx+KTHREAD_PREVIOUS_MODE] push eax call _KeTestAlertThread@4 DontTest: /* Return to previous context */ pop ebp mov esp, ebp jmp _KiServiceExit2 Error: pop ebp mov esp, ebp jmp _KiServiceExit .endfunc上面说了,R3调用ZwContinue时会生成新的KTRAP_FRAME,可是纵观_NtContinue代码也没看到何处会生成KTRAP_FRAME结构。其实,所有的系统调用发生时,都会经过SYSCALL_PROLOG过程,然后由ShareCode进入具体API的实现函数,因此进入_NtContinue时实质会经历如下的过程:
SYSCALL_PROLOG->ShareCode->_NtContinue->KiContinue
KiContinue(IN PCONTEXT Context, IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME TrapFrame) { NTSTATUS Status = STATUS_SUCCESS; KIRQL OldIrql = APC_LEVEL; KPROCESSOR_MODE PreviousMode = KeGetPreviousMode(); /* Raise to APC_LEVEL, only if needed */ if (KeGetCurrentIrql() < APC_LEVEL) KeRaiseIrql(APC_LEVEL, &OldIrql); /* Set up SEH to validate the context */ _SEH_TRY { /* Check the previous mode */ if (PreviousMode != KernelMode) { /* Validate from user-mode */ KiContinuePreviousModeUser(Context, ExceptionFrame, TrapFrame); } else { /* Convert the context into Exception/Trap Frames */ KeContextToTrapFrame(Context, ExceptionFrame, TrapFrame, Context->ContextFlags, KernelMode); } } _SEH_HANDLE { /* Save the exception code */ Status = _SEH_GetExceptionCode(); } _SEH_END; /* Lower the IRQL if needed */ if (OldIrql < APC_LEVEL) KeLowerIrql(OldIrql); /* Return status */ return Status; }KiInitializeUserApc保存最开始一次中断/异常/自陷时的框架中各值于Context(注意,这里涉及2个KTRAP_FRAME,一个是真正的KTRAP_FRAME结构,是被打断执行的流程进入内核时形成的,于KiDeliverApc中被修改并保存在Context中。另一个是假返回R3时,通过ZwContinue系统调用再次进入内核时行成的),这个珍藏多年的变量终于在KiContinue派上了用处。KeContextToTrapFrame是KeTrapFrameToContext的逆操作,用Context的值恢复后一次KTRAP_FRAME结构,等所有语句执行完后,进入_KiServiceExit2,再走TRAP_EPILOG的流程返回到最初被打断执行的代码逻辑中。