漫谈兼容内核之二十六:Windows的结构化异常处理(三)

  用户空间的异常机制是对于系统空间的异常机制的模拟。在内核中,并非所有的异常都是一来就进入“基于SEH框架(Frame-Based)”的异常处理, 而是先进入_KiTrap14()等等类似于向量中断的入口,在那里可以被拦截进行一些优先的处理,例如页面换入和对Copy-On-Write页面的处 理等等。这些处理是全局性质的,而不属于某个SEH域。这相当于是一层全局性的过滤。只有不属于这个层次的异常才会进入基于SEH框架的异常处理。
    为此,用户空间的每个进程还有一个“向量式异常处理”程序入口的队列RtlpVectoredExceptionHead,队列中的每个节点都指向一个具 体的异常处理函数。就像内核中“登记”中断处理程序一样,在用户空间也可以通过RtlAddVectoredExceptionHandler()登记向 量式的异常处理程序,这样就可以在异常的源头上加以拦截(不过在Windows的资料中并未见到有用户空间向量式异常处理的存在,但是在Wine的代码中 倒是有)。这里RtlpExecuteVectoredExceptionHand lers()的作用就是先扫描这个队列,让队列中的各个节点依次认领,如果被认领并且得到了处理,就不再进入基于SEH框架、即 ExceptionList的处理了(代码中if语句的条件“!=”疑为“==”之误,待考),所以处理完之后就通过系统调用NtContinue()返 回内核,而进入内核以后又会返回用户空间当初因异常而被中断的地方。所以,系统调用NtContinue()实质上就是对“中断返回”的模拟。在讲述 APC机制的时候,读者已经看到用户空间的APC函数也是通过NtContinue()实行“中断返回”的(相当于Linux的系统调用 sigreturn())。
    要是在向量式异常处理队列中没有得到处理,那就要通过RtlDispatchException()进行基于SEH框架的异常处理了。
    读者在上一篇漫谈中已经看到过RtlDispatchException()的代码,大致如下:

[KiUserExceptionDispatcher() > RtlDispatchException()]

RtlDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
                                                 IN PCONTEXT Context)
{
    . . . . . .

    /* Get the current stack limits and registration frame */
    RtlpGetStackLimits(&StackLow, &StackHigh);
    RegistrationFrame = RtlpGetExceptionList();

    /* Now loop every frame */
    while (RegistrationFrame != EXCEPTION_CHAIN_END)
    {
        . . . . . .    /* 扫描ExceptionList,直至有节点认领/处理本次异常 */
    }
    /* Unhandled, return false */
    return FALSE;
}

    但是,那是在内核中,作为异常处理的第一步措施(FirstChance),由KiDispatchException()针对发生于系统空间的异常而加 以调用的。而现在,则显然是在用户空间调用这个函数。那莫非在用户空间就可以直接调用内核中的函数吗?不是的,这个 RtlDispatchException()并非那个RtlDispatchException(),这只是“代码重用”而已。这个 RtlDispatchException()编译以后连接在Ntdll.dll的映像中(不过并不导出),并且所处理的ExceptionList是在 用户空间,所使用的堆栈也是用户空间堆栈。
    ExceptionList是通过RtlpGetExceptionList()获取的。

_RtlpGetExceptionList@0:

    /* Return the exception list */
    mov eax, [fs:TEB_EXCEPTION_LIST]
    ret

    常数TEB_EXCEPTION_LIST定义为0,所以总之就是[fs:0]。当CPU运行于系统空间时,其段寄存器FS指向KPCR;而当运行于用户 空间时则指向TEB。所以,不管是在系统空间还是用户空间,RtlpGetExceptionList()总能取到各自的ExceptionList指 针。
    RtlpGetStackLimits()也是一样:

_RtlpGetStackLimits@8:
    /* Get the stack limits */
    mov eax, [fs:TEB_STACK_LIMIT]
    mov ecx, [fs:TEB_STACK_BASE]
    /* Return them */
    mov edx, [esp+4]
    mov [edx], eax
    mov edx, [esp+8]
    mov [edx], ecx
    /* return */
    ret 8

    常数TEB_STACK_LIMIT定义为8,TEB_STACK_BASE定义为4,分别是StackLimit和StackBase两个字段相对于 TEB起点的位移。而在KPCR数据结构中同样也有这两个字段,而且也在相同的位置上。当然,前者用于用户空间堆栈,而后者用于系统空间堆栈。由此可见, 这些数据结构确实是经过精心设计的。另一方面,在处理ExceptionList的过程中所涉及的各种数据结构也都既用于系统空间又用于用户空间。
    这样,同一个函数RtlDispatchException()的代码就既可用于内核,也可用于用户空间的程序库。既然如此,我们也就不必再看一遍RtlDispatchException()的代码了。
    回到KiUserExceptionDispatcher(),执行RtlDispatchException()的结果主要有三种可能。
    一种是当前异常为ExceptionList中的某个节点所认领、即顺利通过了其过滤函数的过滤,并执行了长程跳转。显然,在这种情况下 RtlDispatchException()不会返回,包括KiUserExceptionDispatcher()在内的函数调用框架均因长程跳转而 被跨越和丢弃。不仅如此,复制到用户空间堆栈上的两个数据结构也因为长程跳转而被丢弃。
    第二种是荒掣鼋诘闳狭炝耍沧髁四承┐恚残砘怪葱辛苏飧鼋诘愕纳坪蠛遣⑽粗葱谐こ烫耸盧tlDispatchException() 返回TRUE,于是便通过系统调用NtContinue()完成“中断返回”。由于堆栈上的上下文数据结构含有当初因异常而进入系统空间时所保留的现场, 最后就返回到了(用户空间中)当初因异常而被中断了的地方。
    第三种是没有任何节点认领,RtlDispatchException()中的while循环穷尽了所有的节点,所以返回FALSE。这一定是出了什么问 题,例如ExceptionList被损坏了,因为在正常情况下这是不应该发生的。前面已经提及,在用户空间,整个线程的代码都是放在一个SEH域里执行 的,所以ExceptionList一定是非空,并且其最后一个节点(最早进入的)就是前面在BaseProcessStartup()中进入的那个 SEH域。这个SEH域的过滤函数一般都返回_SEH_EXECUTE_HANDLER,所以是来者不拒。因此,既然穷尽了ExceptionList, 就一定是发生了严重的问题,所以要通过系统调用NtRaiseException()引起一次软异常,以进入针对当前异常的第二步措施。注意这里调用 NtRaiseException()时的第三个参数为FALSE,表示这已经不是第一次尝试。
    此外还有一种可能,就是在RtlDispatchException()内部就已调用了RtlRaiseException(),例如针对某个节点的 RtlpExecuteHandlerForException()返回ExceptionContinueExecution,而异常纪录块中 ExceptionFlags的标志位EXCEPTION_NONCONTINUABLE却又是1,此时就要通过RtlRaiseException() 引起原因为STATUS_NONCONTINUABLE_EXCEPTION的软异常。
    在正常的情况下,对于调用点而言,系统调用NtContinue()和NtRaiseException()是不返回的,返回就说明系统调用本身(而不是 对异常的处理)出了错,例如参数有问题。因此,要是果真从这两个系统调用返回,那么正常的处理已经山穷水尽,只能又求助于软异常了,这就是下面对于 RtlRaiseException()的调用。
    调用RtlRaiseException()的目的在于模拟一次异常,这“异常”已经不是原来所要处理的异常,而是对原来的异常处理不下去了。此时需要解 决的已经不是引起原来那个异常的问题,而是为什么处理不下去的问题。所以原来的“异常代码”ExceptionCode可能是 STATUS_ACCESS_VIOLATION,而现在的ExceptionCode则是从NtRaiseException()或 NtContinue()返回的出错代码Status。同样的道理,通过RtlDispatchException()发起的则是 STATUS_NONCONTINUABLE_EXCEPTION软异常。正因为现在要发起的异常并非原来要处理的异常,所以要为 RtlRaiseException()准备一个新的异常纪录块。
    另一方面,之所以通过RtlRaiseException()发起各种不同类型的软异常,是建立在一个前提上的,那就是相信这个异常最终总会得到处理。这 包括两个方面,首先是ExceptionList中应该有能够认领/处理此类异常的节点,即已经准备好了应对此类问题所需的程序。其次,即使 ExceptionList中没有这样的节点,终归也有应对的办法,那就是前面讲的三个步骤,包括Debug、结束当前线程、甚至“死机”在内。注意“死 机”也有受控和失控之分,使CPU进入停机状态是有控制的死机,那也比任由CPU在程序中乱跳一气要好。
    然而,尽管现在要发起的是与原来不同的异常,但是这毕竟是在处理原来异常的过程中出的问题,与原来的异常是有关系的,应该让认领新发起异常的处理者知道这 一点。所以异常纪录块中有个指针ExceptionRecord,遇到像这样的情况就让新的异常纪录块通过这个指针指向原来的异常纪录块。所以,软异常的 纪录块可以成串,每个纪录块都可以指向本次异常的祸端,只有硬异常的纪录块不会指向别的纪录块。

    显然,RtlRaiseException()是个重要的函数。同样,这个函数的代码也是内核和用户空间两栖的。注意下列的调用路线只是许多情景中的一种,实际上调用这个函数的地方很多。

[KiUserExceptionDispatcher() > RtlRaiseException()]

VOID NTAPI RtlRaiseException(PEXCEPTION_RECORD ExceptionRecord)
{
    CONTEXT Context;
    . . . . . .

    /* Capture the context */
    RtlCaptureContext(&Context);
    /* Save the exception address */
    ExceptionRecord->ExceptionAddress = RtlpGetExceptionAddress();
    /* Write the context flag */
    Context.ContextFlags = CONTEXT_FULL;

    /* Check if we're being debugged (user-mode only) */
    if (!RtlpCheckForActiveDebugger(TRUE))
    {
        /* Raise an exception immediately */
        Status = ZwRaiseException(ExceptionRecord, &Context, TRUE);
    }
    else
    {
        /* Dispatch the exception and check if we should continue */
        if (RtlDispatchException(ExceptionRecord, &Context))
        {
            /* Raise the exception */
            Status = ZwRaiseException(ExceptionRecord, &Context, FALSE);
        }
        else
        {
            /* Continue, go back to previous context */
            Status = ZwContinue(&Context, FALSE);
        }
    }

    /* If we returned, raise a status */
    RtlRaiseStatus(Status);
}

    调用参数ExceptionRecord指向一个异常纪录块。如上所述,这个纪录块中记载着异常的性质,并且一般都通过指针指向另一个异常纪录块。
    首先通过RtlCaptureContext()获取当时的上下文,并通过RtlpGetExceptionAddress()获取RtlRaiseException()的返回地址,以此作为发生本次异常的地址。
    具体的操作视RtlpCheckForActiveDebugger()的结果而异。这个函数有系统空间和用户空间两个不同的版本。目前系统空间的版本只 是返回调用参数,所以在这里总是返回TRYE。而用户空间的版本则返回NtCurrentPeb()->BeingDebugged,所以只在受调 试时返回TRUE。
    所以,如果是在用户空间调用RtlRaiseException(),而又不是在受调试,就启动系统调用ZwRaiseException()。注意此时 的第三个参数为TRUE,表示这是第一次努力。可见,在这种情况下RtlRaiseException()是通过系统调用 ZwRaiseException()实现的。而且所用的函数名是ZwRaiseException(),在用户空间和系统空间都可以调用。
    反之如果是在系统空间,或者虽在用户空间但是在受调试,那就先直接调用RtlDispatchException(),看看本空间的 ExceptionList中是否有节点可以认领和处理。如果有、并且实施了长程跳转,那就不返回了。而如果返回的话,则视返回值为TRUE或FALSE 而分别启动系统调用ZwRaiseException()或ZwContinue()。如果RtlDispatchException()返回TRUE, 就说明已经得到ExceptionList中某个节点的认领、但是并未执行长程跳转而返回ExceptionContinueExecution,此时通 过ZwRaiseException()进行第二次努力,注意这里调用ZwRaiseException()时的第3个参数为FALSE,表示对 ExceptionList的搜索已经失败,下面该采取第二步措施了。而如果RtlDispatchException()返回FALSE,则说明 ExceptionList中根本就没有节点可以认领这次异常,所以就直接通过ZwContinue()返回到刚才获取的那个上下文中,那就是 RtlRaiseException()本次被调用的地方。
    在我们现在所考察的情景中,RtlRaiseException()是在KiUserExceptionDispatcher()中受到调用的,并且在此 之前已经有过对ZwRaiseException()或ZwContinue()的调用。那么现在又来调用ZwRaiseException()或 ZwContinue()是否重复呢?不重复,因为使用的是不同的异常纪录块,不同的异常代码,这是为不同原因而发起的软异常。
    还有,为什么在受调试的情况下反倒要直接调用RtlDispatchException(),避开调试处理呢?因为调试工具所安排的都是针对正常条件下的 异常,例如页面访问异常、除数为0等等,而现在发生的是在处理异常的过程中本来不应该发生的问题,是属于系统的问题,那不是属于应该由调试工具来处理的问 题。而如果直接通过ZwRaiseException()发起一次软中断,则必将首先进入调试程序。
    如果这一次对ZwRaiseException()或ZwContinue()的调用又失败了(居然返回了),那么问题就又更严重了,所以此时再以 ZwRaiseException()或ZwContinue()返回的出错代码为参数调用RtlRaiseStatus()。

[KiUserExceptionDispatcher() > RtlRaiseException() > RtlRaiseStatus()]

VOID NTAPI RtlRaiseStatus(NTSTATUS Status)
{
    EXCEPTION_RECORD ExceptionRecord;
    CONTEXT Context;
    DPRINT1("RtlRaiseStatus(Status 0x%.08lx)/n", Status);

     /* Capture the context */
    RtlCaptureContext(&Context);
    /* Add one argument to ESP */
    Context.Esp += sizeof(PVOID);

    /* Create an exception record */
    ExceptionRecord.ExceptionAddress = RtlpGetExceptionAddress();
    ExceptionRecord.ExceptionCode = Status;
    ExceptionRecord.ExceptionRecord = NULL;
    ExceptionRecord.NumberParameters = 0;
    ExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
    /* Write the context flag */
    Context.ContextFlags = CONTEXT_FULL;
    /* Check if we're being debugged (user-mode only) */
    if (!RtlpCheckForActiveDebugger(TRUE))
    {
        /* Raise an exception immediately */
        ZwRaiseException(&ExceptionRecord, &Context, TRUE);
    }
    else
    {
        /* Dispatch the exception */
        RtlDispatchException(&ExceptionRecord, &Context);
        /* Raise exception if we got here */
        Status = ZwRaiseException(&ExceptionRecord, &Context, FALSE);
    }
    /* If we returned, raise a status */
    RtlRaiseStatus(Status);
}

    这个函数的代码与RtlRaiseException()其实很相似,只是新的异常纪录块创建在本函数框架内部,并且排除了对ZwContinue()的 调用。特别值得注意的是,这个函数有可能递归调用其自身,如果对ZwRaiseException()又返回了,就又要调用RtlRaiseStatus ()。显然,这里的希望寄托在每次对RtlRaiseStatus()的调用参数、即ZwRaiseException()的失败原因不同,而总有一次得 到了妥善的处理,就不再从ZwRaiseException()返回了。注意这里所谓“妥善的处理”也包括在异常处理的第三步中结束线程的运行或者停机。 如果ZwRaiseException()老是返回,那就说明连这也出了问题。
    读者也许因而对异常处理的结局产生一种黯淡的印象,怎么老是失败又失败?但是须知这是在应付最坏的情况,实际的情况不会有这么糟糕。因为如前所述每个线程 的运行都是在一个SEH域中进行,并且这个SEH域的过滤函数实际上是来者不拒。再说,只要没有很特殊的情况,即使基于ExceptionList的处理 失败,后面也还有第二步、第三步措施。在正常的条件下,对用户空间异常的最后一手就是通过ZwTerminateThread()结束线程的运行,对系统 空间异常的最后一手则是停机。

    如前所述,ZwRaiseException()就是系统调用NtRaiseException()。一般在用户空间程序中使用 NtRaiseException(),而在内核中使用ZwRaiseException()。但是在用户空间也可以用ZwRaiseException (),这就是为像RtlRaiseException()这样“两栖”的代码而设的,在Ntdll.dll所导出的函数表中, NtRaiseException()和ZwRaiseException()实际上指向同一段代码。
    这个系统调用的作用就是模拟一次异常。在用户空间调用就模拟发生于用户空间的异常,在系统空间调用就模拟发生于系统空间的异常。读者也许会问,我们前面在 KiUserExceptionDispatcher()中看到对RtlDispatchException()的调用,带着一个新的异常纪录块扫描 ExceptionList,这不也是在模拟一次异常码?这里面的区别还是不小的。扫描ExceptionList只是异常处理的几个步骤之一,整个异常 处理的过程可以包含三次努力。另一方面,无论是真实发生的硬异常、还是由这个系统调用模拟的软异常,都会在系统空间堆栈上形成一个异常框架(由系统调用形 成的陷阱框架与此相同)。而直接调用RtlDispatchException()则并不形成新的异常框架。
    系统调用NtRaiseException()的调用界面如下:

NTSYSCALLAPI NTSTATUS NTAPI
NtRaiseException(IN PEXCEPTION_RECORD ExceptionRecord,
                IN PCONTEXT Context, IN BOOLEAN SearchFrames);

    前两个参数分别是指向异常纪录块和上下文结构的指针。第三个参数表示是否需要搜索ExceptionList,其实就是以前见到过的FirstChance,如果为FALSE就表示处理该次异常的第一次努力已经失败。
    异常纪录块中的字段ExceptionCode说明本次异常的性质、即种类。这是一个32位无符号异常代码,其意义有如中断向量,例如 STATUS_INTEGER_DIVIDE_BY_ZERO、STATUS_SINGLE_STEP等等都是。但是,由于不受硬件限制,异常代码的取值 范围比中断向量要广泛得多,就其本质而言这只是异常的发起者与处理者(过滤函数)之间的约定。所以,除微软已经定义的异常代码、即状态代码之外,应用软件 的开发者也可以按需要自行定义新的代码。微软为此定下了规则:
l Bit30-31表示严重性: 0 = 成功, 1 = 提示, 2 = 警告,3 = 出错。
l Bit29   表示定义/使用者:0 = 由微软定义, 1 = 非微软定义。
l Bit28   保留不用,必须为0。
l Bit16-27表示类型。
l Bit0-15 为异常编号、即产生异常的具体原因。
    例如,STATUS_NETWORK_SESSION_EXPIRED定义为0xC000035C,那就是:严重性为“出错”,由微软定义,类型代码为3,具体原因的代码为0x5C。
    所以,在异常纪录块中,异常代码表示异常的性质和原因。而ExceptionFlags,则以标志位的形式给出有关的状态信息。当然,对于不同编码的异常,对这些标志位可以作不同的解释。
    至于上下文Context,则是为NtContinue()准备的。
    不管是NtRaiseException()还是ZwRaiseException(),内核中实现这个系统调用的函数总是NtRaiseException()。在0.3.0版ReactOS的代码中,这是一段汇编程序:

[NtRaiseException()]

_NtRaiseException@12:
    /* 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, [fs:KPCR_CURRENT_THREAD]
    mov edx, [ebp+KTRAP_FRAME_EDX]
    mov [ebx+KTHREAD_TRAP_FRAME], edx

    /* Set up stack frame */
    mov ebp, esp

    /* Get the Trap Frame in EBX */
    mov ebx, [ebp+0]

    /* Get the exception list and restore */
    mov eax, [ebx+KTRAP_FRAME_EXCEPTION_LIST]
    mov [fs:KPCR_EXCEPTION_LIST], eax

    /* Get the parameters */
    mov edx, [ebp+16] /* Search frames */
    mov ecx, [ebp+12] /* Context */
    mov eax, [ebp+8] /* Exception Record */

    /* Raise the exception */
    push edx
    push ebx
    push 0
    push ecx
    push eax
    call _KiRaiseException@20

    /* Restore trap frame in EBP */
    pop ebp
    mov esp, ebp

    /* Check the result */
    or eax, eax
    jz _KiServiceExit2

    /* Restore debug registers too */
    jmp _KiServiceExit

    在进入NtRaiseException()之前,在系统空间堆栈上已经因为系统调用而形成了一个陷阱框架,“框架指针”EBP就指向这个框架。这里先通 过KPCR结构获取指向当前线程KTHREAD数据结构的指针, 然后使这KTHREAD结构中的指针TrapFrame指向当前的异常框架。这样,以后通过ExGetPreviousMode()就能获取正确的“先前 模式”。
    但是这里有个问题值得一说。常数KPCR_CURRENT_THREAD定义为0x124,而KPCR结构的大小只是0x54,所以指向当前线程 KTHREAD结构的指针显然不在KPCR中。原来,除KPCR以外,每个处理器还有个KPRCB数据结构(Processor Region Control Block)。KPCR结构中有个指针Prcb,指向与其配对的KPRCB数据结构。可是,KPRCB与KPCR两个数据结构起点之间的距离却是固定的, 那就是0x120。而KPRCB结构中的第二个长字,就是指针CurrentThread。所以,KPCR的位置一经确定,相应KPRCB的位置也就定 了。既然如此,为什么不干脆把两个数据结构合二为一呢?这就不得而知了,也许是历史遗留的问题。
    实际的处理是由KiRaiseException()完成的。处理完以后,如果返回的话,就根据返回值是否为STATUS_SUCCESS、即0而分别经由_KiServiceExit2或_KiServiceExit从异常返回。

[NtRaiseException() > KiRaiseException()]

NTSTATUS NTAPI
KiRaiseException(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT Context,
                 PKEXCEPTION_FRAME ExceptionFrame,
                 PKTRAP_FRAME TrapFrame, BOOLEAN SearchFrames)
{
    KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
    CONTEXT LocalContext;
    EXCEPTION_RECORD LocalExceptionRecord;
    ULONG ParameterCount, Size;
    NTSTATUS Status = STATUS_SUCCESS;
    DPRINT1("KiRaiseException/n");

    /* Set up SEH */
    _SEH_TRY
    {
        /* Check the previous mode */
        if (PreviousMode != KernelMode)
        {
            /* Validate the maximum parameters */
            if ((ParameterCount = ExceptionRecord->NumberParameters) >
                                EXCEPTION_MAXIMUM_PARAMETERS)
            {
                /* Too large */
                Status = STATUS_INVALID_PARAMETER;
                _SEH_LEAVE;
            }

            /* Probe the entire parameters now*/
            Size = (sizeof(EXCEPTION_RECORD) -
        ((EXCEPTION_MAXIMUM_PARAMETERS - ParameterCount) * sizeof(ULONG)));
            ProbeForRead(ExceptionRecord, Size, sizeof(ULONG));

            /* Now make copies in the stack */
            RtlMoveMemory(&LocalContext, Context, sizeof(CONTEXT));
            RtlMoveMemory(&LocalExceptionRecord, ExceptionRecord, Size);
            Context = &LocalContext;
            ExceptionRecord = &LocalExceptionRecord;

            /* Update the parameter count */
            ExceptionRecord->NumberParameters = ParameterCount;
        }
    }
    _SEH_HANDLE
    {
        Status = _SEH_GetExceptionCode();
    }
    _SEH_END;

    if (NT_SUCCESS(Status))
    {
        /* Convert the context record */
        KeContextToTrapFrame(Context,
                             ExceptionFrame,
                             TrapFrame,
                             Context->ContextFlags,
                             PreviousMode);

        /* Dispatch the exception */
        KiDispatchException(ExceptionRecord,
                            ExceptionFrame,
                            TrapFrame,
                            PreviousMode,
                            SearchFrames);
    }

    /* Return the status */
    return Status;
}

    参数ExceptionFrame是个指向KEXCEPTION_FRAME结构的指针,仅用于PowerPC处理器,所以从上面传下来的实参是0;另一个参数TrapFrame则是指向堆栈上陷阱框架的指针。其余参数不言自明。
    如果本次系统调用来自用户空间,那么作为参数的异常纪录块和上下文数据结构都在用户空间,所以需要先通过RtlMoveMemory()把它们复制到系统空间的临时缓冲区里来。然而这恰恰又是可能引起异常的,所以又要有个SEH域将其保护起来。
    异常纪录块和上下文数据结构都存在于系统空间以后,就可以调用KiDispatchException()了,参数SearchFrames变成了KiDispatchException()的参数FirstChance。
    KiDispatchException()的代码当然不用再看了。总之,就是前述的三步措施、三次努力,主要就是搜索ExceptionList。但是 搜索的是哪一个ExceptionList,就取决于“先前模式”,即调用ZwRaiseException()之时所处的空间:
l 如果“先前模式”是系统模式,那主要就是搜索系统空间的ExceptionList,在正常的情况下会作长程跳转而不再返回,因此自然也就不会从本次系统调用返回了。如果返回,那就是返回到调用ZwRaiseException()的地方。
l 如 果“先前模式”是用户模式,那主要就是修改堆栈上异常框架中的返回地址,然后返回,使得一旦返回到用户空间就进入 KiUserExceptionDispatcher(),并在那里扫描用户空间的ExceptionList。在正常的情况下也会因长程跳转而不再返回 到调用ZwRaiseException()的地方。或者,倘若不作长程跳转而要回到调用ZwRaiseException()的地方,则可以通过系统调 用ZwContinue()实现。
    从KiRaiseException()返回后,NtRaiseException()根据返回值是否为0而分别经由_KiServiceExit2或_KiServiceExit:

_KiServiceExit:
    /* Disable interrupts */
    cli
    /* Check for, and deliver, User-Mode APCs if needed */
    CHECK_FOR_APC_DELIVER 1
    . . . . . .
    /* Exit and cleanup */
    TRAP_EPILOG FromSystemCall, DoRestorePreviousMode, /
                    DoNotRestoreSegments, DoNotRestoreVolatiles, DoRestoreEverything

_KiServiceExit2:
    /* Disable interrupts */
    cli
    /* Check for, and deliver, User-Mode APCs if needed */
    CHECK_FOR_APC_DELIVER 0
    /* Exit and cleanup */
    TRAP_EPILOG NotFromSystemCall, DoRestorePreviousMode, /
                    DoRestoreSegments, DoRestoreVolatiles, DoNotRestoreEverything

    由于是在.S汇编代码文件中,定义和引用宏操作的格式有所不同。这里的FromSystemCall和NotFromSystemCall都是常数,前者定义为1,后者定义为0,余类推。
    而TRAP_EPILOG,则显然是与TRAP_PROLOG相对应,类似于中断返回,具体的代码就留给读者自己去看了。

你可能感兴趣的:(漫谈兼容内核之二十六:Windows的结构化异常处理(三))