天天看點

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;
}      

繼續閱讀