ReactOS SYSCALL_PROLOG/TRAP_EPILOG及相关代码注释 (2) --ZwContinue

来源:互联网 发布:吊顶面积算法 编辑:程序博客网 时间:2024/06/05 20:37

    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实现:

VOIDNTAPIKiInitializeUserApc(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@4DontTest:    /* Return to previous context */    pop ebp    mov esp, ebp    jmp _KiServiceExit2Error:    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的流程返回到最初被打断执行的代码逻辑中。

0 0
原创粉丝点击