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

 先看调用参数。异常纪录块ExceptionRecord就是前面准备好的;指针ExceptionFrame是NULL,这是个 KEXCEPTION_FRAME结构指针,但是从代码中看这只是为PowerPC芯片而设的,用于保存一些附加寄存器的内容,386架构的处理器芯片无 此要求;而陷阱框架指针Tf指向堆栈上因异常而形成的框架,我们在前面倒是称之为“异常框架”。此外,PreviousMode为KernelMode; 而FirstChance为TRUE,表示即将进行的是第一次努力。
    这个函数的代码中有很大一部分是用于用户空间异常的,因为KiUserTrapHandler()最后也是调用这个函数。但是我们现在要集中关注系统空间异常,所以把那些代码删去了。
    除前面准备好的异常纪录块外,对于异常的处理还需要用到陷阱框架中的许多信息,以及别的信息、例如有关浮点处理器的现场信息,所以在进入实质性的异常处理 前要通过KeTrapFrameToContext()将这些信息整理、收集到一个上下文数据结构中。完成了处理之后,如果要从 KiDispatchException()返回的话,则反过来通过KeContextToTrapFrame()更新有关的原始信息,因为在异常处理的 过程中可能会有所改变。
    有些异常可能发生在程序调试的过程中,如果是这样就要由调试程序先进行处理,有的是由调试程序直接采取措施,有的是由调试人员决定采取什么措施。例如,由 于程序断点所引起的异常(其实是自陷),就只能由调试程序和调试人员采取措施。所以,只要是处于Debug状态,异常处理的第一步就是交由调试程序 (debugger)处理。当然,对内核的调试不像对应用软件那么简单,但是道理是一样的。首先,所用的内核映像必须是可调试的版本,在编译、连接时就加 上了调试可选项。另一方面,还得有个调试工具,那一般就是kd、即“内核调试器(Kernel Debugger)”。这两个条件是缺一不可的。
    通过KdpEnterDebuggerException()把本次异常提交调试程序处理的结果有两种:
l 内核并不处在被调试的状态,或调试程序(以及调试人员)不能解决问题,本次异常需要由SEH机制加以处理,此时返回常数kdHandleException。
l 调试程序已经解决了问题,可以继续运行、而不需要SEH处理的介入,此时返回常数kdContinue。
    如果返回的是kdContinue,就跳转到程序标签Handled下面。此时数据结构Context中的内容可能已有改变,所以通过 KeContextToTrapFrame()对异常框架作相应修改,然后返回,本次异常就这样对付过去了。对于Context结构内容的改变,不妨设想 一下,调试人员可能会选择让程序的执行跳转到另一个点上,或者修改某一个寄存器的内容,这时候就要修改上下文中的返回地址或寄存器映像。
    从总体上说,对发生于系统空间的异常,内核分三步采取措施:
    1. 第一步、即“FirstChance”,是先把问题提交给调试程序,如不能解决(返回的不是kdContinue)就调用 RtlDispatchException()进行实质性的SEH处理。在实际运行中,处于调试状态的时候总是少数,所以在绝大部分的情况下第一步的核心 就是SEH的处理。SEH机制对异常的处理有三种可能的结果:
l 如果被某个SEH框架所认领,并实施长程跳转,程序就不返回了,而此刻所在的函数调用框架也随同别的一些框架一起被跨越和废弃。
l 被某个SEH框架所认领,但是认为可以不作长程跳转而继续运行(例如只需要执行一下善后函数就行)。这样,程序就会从RtlDispatchException()返回,并且返回的值是TRUE。此时问题已经解决,所以也是通过程序标签Handled下面的代码返回。
l 所有的SEH框架都拒绝认领,这意味着处理本次异常的第一步努力已经失败。
    2. 要是第一步失败,就进行第二步,再次通过KdpEnterDebuggerException()把问题提交给调试程序。注意此时的最后两个调用参 数都变成了FALSE,而第一次时都是TRUE。这两个参数,一个是FirstChance,其意义自明;第二个是Gdb,表示需要取得别的调试支持。如 果这一次取得了成功、问题解决了,那么返回值是kdContinue。否则就要采取第三步措施了。
3. 第三步,实际上此时已经无计可施,宏操作KEBUGCHECKWITHTF()的作用是显示出错信息并将出错的现场“转储(Dump)”到文件中以便事后分析,然后就使CPU进入停机状态。
    对于发生于系统空间的异常,这三步措施、或者说三次努力,都是在KiDispatchException()内部完成的,如果调用参数FirstChance为1就一气呵成,但是在调用这个函数时也可以使FirstChance为0而跳过第一步。
    而对于发生于用户空间的异常,则对于ExceptionList的扫描处理是在用户空间进行的,并且在用户空间也有一个类似于KiDispatchException();而有的措施却又需要通过系统空间才能进行,所以不一定能在同一个函数中一气呵成。
    显然,SEH处理的核心就是对ExceptionList的扫描处理,这是由RtlDispatchException()完成的,事实上绝大多数的异常都可以通过这个函数得到妥善的处理。
    下面就是由RtlDispatchException()实现的实质性的SEH处理了。这种处理在有些文献中称为“基于框架(Frame-Based)”的异常处理,其基础当然就是ExceptionList。

[_KiTrap14() > KiPageFaultHandler() > KiKernelTrapHandler() > KiDispatchException()
> RtlDispatchException()]

BOOLEAN
NTAPI
RtlDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
                     IN PCONTEXT Context)
{
PEXCEPTION_REGISTRATION_RECORD RegistrationFrame, NestedFrame = NULL;
. . . . . .

/* Get the current stack limits and registration frame */
RtlpGetStackLimits(&StackLow, &StackHigh);
RegistrationFrame = RtlpGetExceptionList();
DPRINT("RegistrationFrame is 0x%p/n", RegistrationFrame);

/* Now loop every frame */
while (RegistrationFrame != EXCEPTION_CHAIN_END)
{
    /* Find out where it ends */
    RegistrationFrameEnd = (ULONG_PTR)RegistrationFrame + sizeof(*RegistrationFrame);

    /* Make sure the registration frame is located within the stack */
    if ((RegistrationFrameEnd > StackHigh) ||
            ((ULONG_PTR)RegistrationFrame < StackLow) ||
            ((ULONG_PTR)RegistrationFrame & 0x3))
    {
       . . . . . .
       continue;
       . . . . . .
    }

    . . . . . .

    /* Call the handler */
    DPRINT("Executing handler: %p/n", RegistrationFrame->Handler);
    ReturnValue = RtlpExecuteHandlerForException(ExceptionRecord,
                                     RegistrationFrame, Context, &DispatcherContext,
                                     RegistrationFrame->Handler);
    DPRINT("Handler returned: %p/n", (PVOID)ReturnValue);

    /* Check if this is a nested frame */
    if (RegistrationFrame == NestedFrame)
    {
            /* Mask out the flag and the nested frame */
            ExceptionRecord->ExceptionFlags &= ~EXCEPTION_NESTED_CALL;
            NestedFrame = NULL;
    }

    /* Handle the dispositions */
    if (ReturnValue == ExceptionContinueExecution)
    {
            /* Check if it was non-continuable */
            if (ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
            {
                /* Set up the exception record */
                ExceptionRecord2.ExceptionRecord = ExceptionRecord;
                ExceptionRecord2.ExceptionCode =
                                STATUS_NONCONTINUABLE_EXCEPTION;
                ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
                ExceptionRecord2.NumberParameters = 0;

                /* Raise the exception */
                DPRINT("Non-continuable/n");
                RtlRaiseException(&ExceptionRecord2);
            }
            else
            {
                /* Return to caller */
                return TRUE;
            }
    }
    else if (ReturnValue == ExceptionNestedException)
    {
            /* Turn the nested flag on */
            ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL;

            /* Update the current nested frame */
            if (NestedFrame < DispatcherContext) NestedFrame = DispatcherContext;
    }
    else if (ReturnValue == ExceptionContinueSearch)
    {
            /* Do nothing */
    }
    else
    {
            /* Set up the exception record */
            ExceptionRecord2.ExceptionRecord = ExceptionRecord;
            ExceptionRecord2.ExceptionCode = STATUS_INVALID_DISPOSITION;
            ExceptionRecord2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
            ExceptionRecord2.NumberParameters = 0;

            /* Raise the exception */
            RtlRaiseException(&ExceptionRecord2);
    }

    /* Go to the next frame */
    RegistrationFrame = RegistrationFrame->Next;
}

/* Unhandled, return false */
DPRINT("FALSE/n");
return FALSE;
}

    SEH的基础就是异常处理链表,正是这个队列中的节点及其内容使长程跳转成为可能。所以这里一开始就通过RtlpGetExceptionList()找 到异常处理链表,这当然就是当前CPU的KPCR结构中的指针ExceptionList。这个指针指向链表中的第一个节点,其数据结构是 EXCEPTION_REGISTRATION_RECORD。这种数据结构的定义如下:

typedef struct _EXCEPTION_REGISTRATION_RECORD
{
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

    这和上一篇漫谈中所说的_SEHRegistration_t结构实质上是同一回事,只是结构和成分的名称不同。这里的Handler是个函数指针,其类 型为PEXCEPTION_ROUTINE,也跟_SEHRegistration_t结构中的_SEHFrameHandler_t相同,因而这里见到 的函数指针handler就是前面设置的函数指针SER_Handler。之所以如此,笔者猜想,是因为有关的代码重用于用户空间的异常处理,因为历史的 原因而使用了不同的名称。注意_SEHRegistration_t数据结构是另一种数据结构_SEHPortableFrame_t内部的第一个成分, 所以获得了一个_SEHRegistration_t结构指针,也就获得了指向其所在_SEHPortableFrame_t结构的指针。
    上一篇漫谈中还曾讲到,异常处理链表中的每一个数据结构都在堆栈上,都是相应函数调用框架中的局部量。所以这里还通过 RtlpGetStackLimits()获取当前线程的(系统空间)堆栈位置、即其StackLow和StackHigh两个端点;下面在处理队列中的 数据结构时先加以比对,以确认其位置上的合理性。不过也有个例外,就是倘若异常发生于DPC处理(Windows的DPC相当于Linux的bh、或“软 中断”)的过程中,那么由于DPC处理时使用的是一个独立的堆栈,所以就要对StackLow和StackHigh作出相应的调整,然后重新加以比对。当 然,如果比对的结果确实不符,那就无法继续下去了。
    然后就通过一个while循环来依次搜寻处理ExceptionList链表中的每一个节点,由 RtlpExecuteHandlerForException()加以尝试。链表中的每个节点都代表着一个局部SHE框架栈。前一篇漫谈中讲过,局部 SHE框架栈就是一组不仅实质上嵌套、而且(代码)形式上也嵌套的SEH域所形成的框架。但是,事实上在现有的ReactOS代码中还没有见到使用形式嵌 套的SEH域,所以ExceptionList中的每个节点实质上都只代表着单个的SEH域。因此,为叙述上的方便,下面只要不至于引起误解或与代码冲 突,就假定ExceptionList中的每个节点都只代表着一个SEH框架(而不是栈)。
    由于异常处理链表是个后进先出的队列,里面的第一个节点(数据结构)代表着最近进入的SEH框架。如果链表中有不止一个的节点,就说明有了SEH框架嵌 套。在嵌套的情况下,队列中的第一个节点代表着最底层的那个保护域,如果这个节点(执行过滤函数以后)拒绝认领本次异常,就说明并非这个SEH域所针对的 异常,那就应该往上跑一层,看是否为上一层SEH域所针对的异常。所以顺着异常处理链表考察下一个节点就是在逐层往上跑,直至有某个节点认领本次异常为 止。一个节点认领了实际发生的异常以后,一般就会执行预先规定的长程跳转,直接就跑到了那个框架中的if语句里面,而眼下这个函数的调用框架也就因为长程 跳转而被丢弃了。不过,后面读者将看到,在长程跳转之前还有个“展开(Unwinding)”的过程,那就是调用所有被跨越SEH框架的善后函数。
    所以,在正常条件下这个while循环注定是短命的。只有在队列中的每个SHE框架都拒绝认领所发生的异常、或者在处理中出错的情况下,这个while循环才可能以穷尽整个队列而告终。
    如果RtlpExecuteHandlerForException()返回,那么其返回值有这么几种可能:

#define ExceptionContinueExecution 0
#define ExceptionContinueSearch   1
#define ExceptionNestedException 2
#define ExceptionCollidedUnwind   3
    这个返回值实际上是对下一步应该如何进行的指示。所以此后的程序大体上相当于一个以此为条件的case语句。
l ExceptionContinueExecution 表示认领了、但是不作长程跳转。这说明或者是问题已经解决,或者是可以忽略本次异常的发生,总之是可以继续执行原来的程序了。但是这里还有个条件,就是被 异常所中断的程序还能够继续执行才行。这时候就要看异常纪录块ExceptionRecord中的EXCEPTION_NONCONTINUABLE标志 位。如果为1就表示无法继续执行,所以通过RtlRaiseException()引起一次类型为 STATUS_NONCONTINUABLE_EXCEPTION的“软异常”。而若可以继续,则直接结束循环而返回,并最终从本次异常返回(此时本次异 常的框架还在堆栈上)。注意此时函数返回的是TRUE,表示问题已经解决,而若返回FALSE则表示失败。
l ExceptionContinueSearch 表示不予认领,应该继续考察队列里面的下一个节点、即上升到更高一层的SEH框架。这是之所以需要ExceptionList、以及这里的while循环 的原因。此时在本轮循环中不需要再做什么,前进到ExceptionList队列中的下一个节点就是了。
l ExceptionNestedException 则表示RtlpExecuteHandlerForException()发现本次异常是一次嵌套异常,即发生于异常处理过程中的新的异常。下面读者将看 到,为了捕捉嵌套异常,在每考察/处理一个ExceptionList中的某个节点时先要在ExceptionList链表的头部插入一个临时的“保护节 点”,处理完目标节点后再将保护节点删去。这样,当嵌套异常发生时,原来对ExceptionList的扫描/处理被中断、而先要处理这新的异常,并因此 而再次进入这里的while循环。显然此时最先受到考察的是那个临时的保护节点,而ExceptionNestedException就是在这个时候返回 的,同时还通过参数DispatcherContext返回一个指针,指向该临时节点所保护的目标节点、即原来正在处理中的那个节点。在这样的情况下,代 码中使局部量NestedFrame指向所保护的目标节点,并使异常纪录块中的EXCEPTION_NESTED_CALL标志位设成1,然后就继续在 ExceptionList中往前搜索。一直到过了所保护的节点以后,才把这标志位以及NestedFrame清0。这样,只要异常纪录块中的这个标志位 为1,就表示目前处于在处理过程中发生了嵌套异常的那个SEH框架内部。注意对嵌套异常的处理可能在ExceptionList中跑得更远,也即属于更高 层的SEH框架。由于嵌套的深度有可能大于1,实际的代码比之上述还要更复杂一些。
l 如 果除此之外出现了别的返回值,包括ExceptionCollidedUnwind,那就是发生了严重的错误。就其本质而言,这样的错误也相当于异常, ExceptionList中理应也有相应的节点为此作好了准备,只是CPU的硬件不会因此而发生硬异常。所以,就通过 RtlRaiseException()引起一次类型为STATUS_INVALID_DISPOSITION的软异常。这当然是一次嵌套异常,因为原来 的异常框架还在。
    关于通过RtlRaiseException()引起软异常的问题,下一篇漫谈中还要详细介绍,这里先简单提一下。所谓软异常,就是以函数调用的手段来模 拟一次异常。在典型的情况下,每个SEH域都有一个过滤函数,过滤函数检查异常纪录块的类型以确定是否应该认领和处理本次异常。以上列的第一种情况为例, 假定原来是访问内存失败而引起的14号异常,类型是STATUS_ACCESS_VIOLATION,这已经得到了处理,结论是继续执行;然而状态标志位 又表明无法继续。这样,问题已不再是原来访问内存失败的问题,而变成了类型为STATUS_NONCONTINUABLE_EXCEPTION的另一个问 题,这就可能需要由针对此种异常的另一个SEH域来处理了,因而以此类型模拟一次异常,使SEH机制再次搜索ExceptionList。这就是发起软异 常的意图。
    最后,如果while()循环结束,那就说明异常处理链表中所有的节点都不是为本类异常准备的,也即事先并没有估计到本类异常的发生、也没有为此作出安排,所以就返回FALSE,让上一层KiDispatchException()采取其第二步措施。
    显然,关键在于RtlpExecuteHandlerForException(),就是对ExceptionList中具体节点的考察和处理。先看其调用界面:

EXCEPTION_DISPOSITION
RtlpExecuteHandlerForException(PEXCEPTION_RECORD ExceptionRecord,
            PEXCEPTION_REGISTRATION RegistrationFrame, PCONTEXT Context,
            PVOID DispatcherContext, PEXCEPTION_HANDLER ExceptionHandler);

    其中第一个参数指向异常纪录块,这是关于本次(实际发生的)异常的信息;第二个参数指向ExceptionList队列中的一个节点,这是关于当前所考察 SEH域的信息;第三个参数指向本次异常发生时的上下文数据结构。第四个参数DispatcherContext是个指针,仅对用于嵌套异常的临时保护节 点有效。最后一个参数是函数指针,指向由当前节点提供的框架处理函数。对于普通的节点这就是_SEHFrameHandler(),是在 _SEHEnterFrame_f()中设置好的。
    再看具体的实现,这是一段汇编代码。

[_KiTrap14() > KiPageFaultHandler() > KiKernelTrapHandler() > KiDispatchException()
> RtlpDispatchException() > RtlpExecuteHandlerForException()]

_RtlpExecuteHandlerForException@2 0:
    /* Copy the routine in EDX */
    mov edx, offset _RtlpExceptionProtector
    /* Jump to common routine */
    jmp _RtlpExecuteHandler@20

    首先让寄存器EDX指向一个函数RtlpExceptionProtector(),其作用下面就会看到。然后跳转到标签 _RtlpExecuteHandler下面。事实上,_RtlpExecuteHandler下面这一段代码是由 RtlpExecuteHandlerForException()与另一个函数RtlpExecuteHandlerForUnwind()共用的,所 不同的只是置入EDX的函数指针。

_RtlpExecuteHandlerForUnwind@20:
    /* Copy the routine in EDX */
    mov edx, offset _RtlpExceptionProtector
    /* Run the common routine */
_RtlpExecuteHandler@20:
    /* Save non-volatile */
    push ebx
    push esi
    push edi
    /* Clear registers */
    xor eax, eax
    xor ebx, ebx
    xor esi, esi
    xor edi, edi

    /* Call the 2nd-stage executer */
    push [esp+0x20]
    push [esp+0x20]
    push [esp+0x20]
    push [esp+0x20]
    push [esp+0x20]
    call _RtlpExecuteHandler2@20

    /* Restore non-volatile */
    pop edi
    pop esi
    pop ebx
    ret 0x14

    按理说,在RtlpExecuteHandlerForUnwind()里面置入EDX的指针应该指向RtlpUnwindProtector()、而不 是像在RtlpExecuteHandlerForException()里面那样指向RtlpExceptionProtector()。所以这很可能 是个错误。毕竟0.3.0版的ReactOS还很新,里面有些错误也不奇怪。而在0.2.6版的代码中则确实指向RtlpUnwindProtector (),那应该是正确的。
    显然,具体的处理是由_RtlpExecuteHandler2()实现的,这里只是为这个函数的调用进行事先的准备和事后的恢复。

[_KiTrap14() > KiPageFaultHandler() > KiKernelTrapHandler() > KiDispatchException()
> RtlDispatchException() > RtlpExecuteHandlerForException() > _RtlpExecuteHandler2()]

_RtlpExecuteHandler2@20:
    /* Set up stack frame */
    push ebp
    mov ebp, esp
    /* Save the Frame */
    push [ebp+0xC]       /* 指向原节点、这是要保护的目标节点 */
    /* Push handler address */
    push edx        /* 成为新节点中的Handler指针,指向保护函数 */

    /* Push the exception list */
    push [fs:TEB_EXCEPTION_LIST]   /* 成为新节点中的Next指针 */
    /* Link us to it */
    mov [fs:TEB_EXCEPTION_LIST], esp /* 让ExceptionList指向新节点 */

    /* Call the handler */
    push [ebp+0x14]
    push [ebp+0x10]
    push [ebp+0xC]
    push [ebp+8]
    mov ecx, [ebp+0x18]      /* 参数ExceptionHandler */
    call ecx          /* 调用ExceptionHandler,4个调用参数 */

    /* Unlink us */
    mov esp, [fs:TEB_EXCEPTION_LIST]
    /* Restore it */
    pop [fs:TEB_EXCEPTION_LIST]    /* 新节点已从ExceptionList中摘除 */

    /* Undo stack frame and return */
    mov esp, ebp        /* 新节点不复存在于堆栈上 */
    pop ebp
    ret 0x14

    这里的常数TEB_EXCEPTION_LIST定义为0,所以“fs:TEB_EXCEPTION_LIST”就是“fs:0”,即指向KPCR结构中 的第一个字段,即ExceptionList。但是在这里引用常数TEB_EXCEPTION_LIST有些误导,因为现在这是在系统空间、而不是在用户 空间。究其原因,则是这个函数同样也用于用户空间的异常处理(代码重用),而TEB的第一个字段确实同样也是ExceptionList。
    注意这里的call指令所引用的是ECX、而不是EDX。ECX的内容来自堆栈上的调用参数,就是前面的最后一个参数ExceptionHandler。对于普通的节点(下面就要讲到不普通的节点),这实际上是_SEHFrameHandler()。
    这段代码有点奥妙。注意这里先把作为参数的指针RegistrationFrame压入堆栈,再把寄存器EDX的内容、即指向 _RtlpExceptionProtector()或_RtlpUnwindProtector()的函数指针压入堆栈,然后又把[fs:0]、即指针 ExceptionList的内容压入堆栈,再把当前的堆栈指针写入ExceptionList。这样一来,ExceptionList所指处的内容是一 个_SEHRegistration_t结构指针,这个指针的上方是一个函数指针(再上方是指向目标节点的指针)。我们可以把这两个指针看成一个 _SEHPortableFrame_t数据结构,而这里对堆栈和[fs:0]的操作,则实际上是把又一个节点插入了异常处理队列的头部(逻辑上则是尾 部)。但是,与这个队列中原有的节点相比,这个新的节点又有所不同。原来的节点虽然同为_SEHRegistration_t数据结构,却都是 _SEHPortableFrame_t数据结构中的一个成分;而现在挂入队列的节点却是孤立地存在(外加一个指针)。但是这并不成为问题,因为现在这个 函数指针所指向的函数也不一样。毕竟,是这个函数决定了怎样去访问和使用有关的数据结构,只要二者配套就行。为区别于目标节点中的框架处理函数,我们称现 在这个函数为“保护函数(protector)”。与此相应,我们不妨称这样的节点为“保护节点”,而称原来的节点为“普通节点”。
    保护节点的存在是暂时的,从保护函数返回时下面的两条指令就把这个新的节点删除了,于是ExceptionList又恢复了原状。由于节点存在于堆栈上,有关的队列操作就很干净利索。后面读者就会看到,保护节点不会执行长程跳转,所以一定会返回。
    那么为什么要在ExceptionList中插入一个保护节点呢?这是因为,下面就要对一个普通节点执行_SEHFrameHandler()了,执行这 个函数的过程本身(例如对过滤函数的调用)有可能会引起新的异常,所以也应该把它保护起来、为可能发生的异常作好准备。而相应的保护函数,如果异常果真发 生的话,则是_RtlpExceptionProtector()。其实这个函数并不真的起什么保护作用,其目的只是用来表明发生了嵌套异常。
 在异常处理的过程中又发生新的异常,这就是嵌套异常。但是读者要注意嵌套异常和嵌套SEH域的区别,不要混淆。

你可能感兴趣的:(漫谈兼容内核)