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

    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的流程返回到最初被打断执行的代码逻辑中。

你可能感兴趣的:(windows)