原文出处:A Crash Course on the Depths of Win32? Structured Exception Handling 下载源代码 本文假设你熟悉 C++,Win32 摘要 Win32 结构化异常处理其核心是操作系统提供的服务,你能找到的关于 SEH 的所有文档都是描述一个特定的编译器运行时库,这个运行库包装着操作系统实现。在本文中,我将一层一层对 SEH 进行剥离,以便展现其最基本的概念。 在 Win32 操作系统提供的所有功能中,使用最广泛但最缺乏文档描述的也许就是结构化异常处理了(SEH),当你考虑 Win32 结构化异常处理时,你也许会想到诸如 _try,_finally 以及 _except 这些术语。你能在任何有关 Win32 的书中发现对 SEH 很好的描述(即使是 remedial)。即便是 Win32 SDK 也具备有相当完整的使用 _try,_finally 和 _except 进行结构化异常处理的概述。 SEH 浅析 从整体来看,SEH 的可谓不可一世,绝对压倒一切,我将从细微之处开始,用我自己的方式一层一层研究。如果你是一张白纸,以前从没接触过结构化异常处理,那就最好不过了。如果你以前使用过 SEH。那就尝试清理你头脑中的 _try,GetExceptionCode 和 EXCEPTION_EXECUTE_HANDLER 等诸如此类的词,权当自己是个新手。做一个深呼吸,准备好了吗?好,我们开始。 该原型出自标准的 Win32 头文件 EXCPT.H,初看就有那么一点不同凡响。如果你慢慢研究,其实并没有那么糟。例如,忽略返回类型(EXCEPTION_DISPOSITION)。基本上你看到的就是一个叫做 _except_handler 的函数,这个函数带有四个参数。 ExceptionCode 参数是由操作系统赋值给异常的一个数。你可以在 WINNT.H 文件中搜一下“STATUS_”开始的 #defines 内容便可以得到一系列不同的异常编码。例如 STATUS_ACCESS_VIOLATION 是大家再熟悉不过的异常编码了,其值是 0xC0000005。更复杂的异常编码可以从 Windows NT DDK 的 NTSTATUS.H 文件中找到。EXCEPTION_RECORD 结构中的第四个元素是异常发生的地址。剩下的 EXCEPTION_RECORD 域现在可以忽略,不用管它。 此外,这个 CONTEXT 结构与 GetThreadContext 和 SetThreadContext API 函数使用的结构是相同的。 你还将看到该结构在 WINNT.H 文件中定义的 NT_TIB 结构中被引用为 _EXCEPTION_REGISTRATION_RECORD。唉,除此之外,没有什么地方能找到 _EXCEPTION_REGISTRATION_RECORD 的定义,所以我只能使用 EXSUP.INC 文件中定义的汇编语言结构。这也是我为什么在本文前述内容中说过的 SEH 缺乏文档的一个例证。 如果你想知道为什么我要在堆栈上建立这个 EXCEPTION_REGISTRATION 结构,而不是使用全局变量,有一个很好的理由。当你使用编译器的 _try/_except 时,编译器也会在堆栈上建立 EXCEPTION_REGISTRATION 结构。我只是向你简要地揭示你使用 _try/_except 时编译器所做的事情。让我们回到 main 函数,下一个 __asm 块是通过把 EAX 寄存器清零(MOV EAX,0),然后把此寄存器的值作为内存地址让下一条指令(MOV [EAX],1)向此地址写入数据而故意引发一个错误。最后一个 __asm 块是清除这个简单的异常处理例程:首先它恢复以前的 FS:[0] 内容,然后它将 EXCEPTION_REGISTRATION 结构记录从堆栈中弹出(ADD ESP,8)。 现在,假设你正在运行 MYSEH.EXE 并会看到所发生的事情。当 MOV [EAX],1 指令执行时,它导致一个数据访问违例。系统察看 TIB 中的 FS:[0] 并找到 EXCEPTION_REGISTRATION 结构指针。此结构中则有一个指向 MYSEH.CPP 中 _except_handler 函数的指针。系统则将四个必须的参数(我在前面描述过这四个参数)压入堆栈并调用 _except_handler 函数。 一旦进入 _except_handler,代码首先通过 printf 指示“哈!这里是我干的!”。接着,_except_handler 修复导致出错的问题。即 EAX 寄存器指向某个不能写入的内存地址(地址 0)。修复方法是在改变 CONTEXT 结构中的 EAX 的值,以便它指向某个允许进行写入操作的位置。在这个简单的程序中,DWORD 变量(scratch)是故意为此而设计的。_except_handler 函数最后一个动作时返回 ExceptionContinueExecution 值,它在标准的 EXCPT.H 文件中定义。 当操作系统看到返回值为 ExceptionContinueExecution。它就认为你已经修复了问题,并且引起错误的指令应该被重新执行。因为我的 _except_handler 函数强制 EAX 寄存器指向合法内存,MOV EAX,1 指令再次执行,函数 main 一切正常。看,这并不复杂,不是吗? 进一步深入 有了前面的最简单的例子,让我们再回过头去填补一些空白。虽然这个异常回调机制很棒,但它并不是一个完美的解决方案。对于稍微复杂一些的应用程序来说,仅用一个函数就能处理程序中任何地方都可能发生的异常是相当困难的。一个更实用的方案应该是有多个异常处理例程,每个例程针对程序的特定部分。不知你是否知道,实际上,操作系统提供的正是这个功能。 图四 查找处理异常的 EXCEPTION_REGISTRATION 结构 一旦系统找到一个处理该异常的某个回调函数,它就停止遍历结构链表。 这个异常处理回调函数,同样被称为_except_handler,却与前面的那个截然不同。它首先打印出 ExceptionRecord 结构中的异常代码和标志,这个结构的地址是作为一个指针参数被这个函数接收的。打印出异常标志的原因稍后就会明白。因为_except_handler 函数并没有打算修复出错的代码,因此它返回 ExceptionContinueSearch。这导致操作系统继续在 EXCEPTION_REGISTRATION 结构链表中搜索下一个 EXCEPTION_REGISTRATION结构。接下来安装的异常回调函数是针对 main 函数中的__try/__except块的。__except 块简单地打印出“Caught the exception in main()”。此时我们只是简单地忽略这个异常来表明我们已经处理了它。 以下是 MYSEH2.CPP: //================================================= // MYSEH2 - Matt Pietrek 1997 // Microsoft Systems Journal, January 1997 // FILE: MYSEH2.CPP // 使用命令行CL MYSEH2.CPP编译 //================================================= #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) { printf( "Home Grown handler: Exception Code: %08X Exception Flags %X", ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags ); if ( ExceptionRecord->ExceptionFlags & 1 ) printf( " EH_NONCONTINUABLE" ); if ( ExceptionRecord->ExceptionFlags & 2 ) printf( " EH_UNWINDING" ); if ( ExceptionRecord->ExceptionFlags & 4 ) printf( " EH_EXIT_UNWIND" ); if ( ExceptionRecord->ExceptionFlags & 8 ) // 注意这个标志 printf( " EH_STACK_INVALID" ); if ( ExceptionRecord->ExceptionFlags & 0x10 ) // 注意这个标志 printf( " EH_NESTED_CALL" ); printf( "\n" ); // 我们不想处理这个异常,让其它函数处理吧 return ExceptionContinueSearch; } void HomeGrownFrame( void ) { DWORD handler = (DWORD)_except_handler; __asm { // 创建EXCEPTION_REGISTRATION结构: push handler // handler函数的地址 push FS:[0] // 前一个handler函数的地址 mov FS:[0],ESP // 安装新的EXECEPTION_REGISTRATION结构 } *(PDWORD)0 = 0; // 写入地址0,从而引发一个错误 printf( "I should never get here!\n" ); __asm { // 移去我们的EXECEPTION_REGISTRATION结构 mov eax,[ESP] // 获取前一个结构 mov FS:[0], EAX // 安装前一个结构 add esp, 8 // 把我们EXECEPTION_REGISTRATION结构弹出堆栈 } } int main() { __try { HomeGrownFrame(); } __except( EXCEPTION_EXECUTE_HANDLER ) { printf( "Caught the exception in main()\n" ); } return 0; } 这里的关键是执行流程。当一个异常处理程序拒绝处理某个异常时,它实际上也就拒绝决定流程最终将从何处恢复。只有接受某个异常的异常处理程序才能决定待所有异常处理代码执行完毕之后流程将从何处继续执行。这个规则暗含的意义非常重大,虽然现在还不是显而易见。当使用结构化异常处理时,如果一个函数有一个异常处理程序但它却不处理某个异常,这个函数就有可能非正常退出。例如在 MYSEH2中 HomeGrownFrame 函数就不处理异常。由于在链表中后面的某个异常处理程序(这里是 main 函数中的)处理了这个异常,因此出错指令后面的 printf 就永远不会执行。从某种程度上说,使用结构化异常处理与使用 setjmp 和 longjmp 运行时库函数有些类似。 如果你运行 MYSEH2,会发现其输出有些奇怪。看起来好像调用了两次 _except_handler 函数。根据你现有的知识,第一次调用当然可以完全理解。但是为什么会有第二次呢? Home Grown handler: Exception Code: C0000005 Exception Flags 0 Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING Caught the Exception in main() 比较一下以“Home Grown Handler”开头的两行,就会看出它们之间有明显的区别。第一次异常标志是0,而第二次是2。这个问题说来话就长了。实际上,当一个异常处理回调函数拒绝处理某个异常时,它会被再一次调用。但是这次回调并不是立即发生的。这有点复杂。我需要把异常发生时的情形好好梳理一下。 帮帮我!没有人处理它! 迄今为止,我实际上一直在假设操作系统总是能在 EXCEPTION_REGISTRATION 结构链表中 的某个地方找到一个异常处理程序。如果找不到怎么办呢?实际上,这几乎不可能发生。因为操作系统暗中已经为每个线程都提供了一个默认的异常处理程序。这个默认的异常处理程序总是链表的最后一个结点,并且它总是选择处理异常。它进行的操作与其它正常的异常处理回调函数有些不同,下面我会说明。 在这段伪码中,注意对 lpfnEntryPoint 的调用被封装在一个__try 和 __except 块中。正是此__try 块安装了默认的、异常处理程序链表上的最后一个异常处理程序。所有后来注册的异常处理程序都被安装在此链表中这个结点的前面。如果 lpfnEntryPoint 函数返回,那么表明线程一直运行到完成并且没有引发异常。这时 BaseProcessStart 调用 ExitThread 使线程退出。 表面上看,UnhandledExceptionFilter 显示了一个对话框告诉你发生了一个错误。这时,你被给予了一个机会要么终止出错进程,要么调试它。但是幕后发生了许多事情,我会在文章最后详细讲述它。 编译器级的SEH 虽然我在前面偶尔也使用了__try 和__except,但迄今为止几乎我写的所有内容都是关于操作系统方面对 SEH 的实现。然而看一下我那两个使用操作系统的原始 SEH 的小程序别扭的样子,编译器对这个功能进行封装实在是非常有必要的。现在让我们来看一下 Visual C++ 是如何在操作系统对 SEH 功能实现的基础上来创建它自己的结构化异常处理支持的。 简单地说,某个函数__try 块中的所有代码是由 EXCEPTION_REGISTRATION 结构来保护的,该结构建立在此函数的堆栈帧上。在函数的入口处,这个新的 EXCEPTION_REGISTRATION 结构被放在异常处理程序链表的头部。在__try 块结束后,相应的 EXCEPTION_REGISTRATION 结构从这个链表的头部被移除。正如我前面所说,异常处理程序链表的头部被保存在 FS:[0] 处。因此,如果你在调试器中单步跟踪时能看到类似下面的指令 MOV DWORD PTR FS:[00000000],ESP 或者 MOV DWORD PTR FS:[00000000],ECX就能非常确定这段代码正在进入或退出一个__try/__except块。 Visual C++ 的 SEH 实现并没有使用原始的 EXCEPTION_REGISTRATION 结构。它在这个结构的末尾添加了一些附加数据。这些附加数据正是允许单个函数(__except_handler3)处理所有异常并将执行流程传递到相应的过滤器表达式和__except 块的关键。我在 Visual C++ 运行时库源代码中的 EXSUP.INC 文件中找到了有关 Visual C++ 扩展的 EXCEPTION_REGISTRATION 结构格式的线索。在这个文件中,你会看到以下定义(已经被注释掉了): ;struct _EXCEPTION_REGISTRATION{ ; struct _EXCEPTION_REGISTRATION *prev; ; void (*handler)( PEXCEPTION_RECORD, ; PEXCEPTION_REGISTRATION, ; PCONTEXT, ; PEXCEPTION_RECORD); ; struct scopetable_entry *scopetable; ; int trylevel; ; int _ebp; ; PEXCEPTION_POINTERS xpointers; ;}; 在前面你已经见过前两个域:prev 和 handler。它们组成了基本的 EXCEPTION_REGISTRATION 结构。后面三个域:scopetable(作用域表)、trylevel 和_ebp 是新增加的。scopetable 域指向一个 scopetable_entry 结构数组,而 trylevel 域实际上是这个数组的索引。最后一个域_ebp,是 EXCEPTION_REGISTRATION 结构创建之前栈帧指针(EBP)的值。 GetExceptionInformation 是一个编译器内联函数,与它相关的 GetExceptionCode 函数也是如此。此函数实际上只是返回 GetExceptionInformation 返回的数据结构(EXCEPTION_POINTERS)中的一个结构(EXCEPTION_RECORD)中的一个域(ExceptionCode)的值。当 Visual C++ 为 GetExceptionCode 函数生成下面的指令时,它到底是想干什么?我把这个问题留给读者。(现在就能理解为什么SDK文档提醒我们要注意这两个函数的使用范围了。) MOV EAX,DWORD PTR [EBP-14] ; 执行完毕,EAX指向EXCEPTION_POINTERS结构 MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX指向EXCEPTION_RECORD结构 MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX中是ExceptionCode的值 现在回到扩展的 EXCEPTION_REGISTRATION 结构上来。在这个结构开始前的8个字节处(即[EBP-18H]处),Visual C++ 保留了一个DWORD来保存所有prolog代码执行完毕之后的堆栈指针(ESP)的值(实际生成的指令为MOV DWORD PTR [EBP-18H],ESP)。这个DWORD中保存的值是函数执行时ESP寄存器的正常值(除了在准备调用其它函数时把参数压入堆栈这个过程会改变 ESP寄存器的值并在函数返回时恢复它的值外,函数在执行过程中一般不改变ESP寄存器的值)。 在操作系统看来,只存在组成原始 EXCEPTION_REGISTRATION 结构的两个域:即[EBP-10h]处的prev指针和[EBP-0Ch]处的handler函数指针。栈帧中的其它所有内容是针对于Visual C++的。把这个Visual C++生成的标准异常堆栈帧记到脑子里之后,让我们来看一下真正实现编译器层面SEH的这个Visual C++运行时库例程――__except_handler3。 __except_handler3 和 scopetable 我真的很希望让你看一看Visual C++运行时库源代码,让你自己好好研究一下__except_handler3函数,但是我办不到。因为 Microsoft并没有提供。在这里你就将就着看一下我为__except_handler3函数写的伪代码吧:。 __except_handler3一开始就在堆栈上创建了一个EXCEPTION_POINTERS结构,并用它的两个参数来对这个结构进行初始化。我在伪代码中把这个结构称为 exceptPrts,它的地址被放在[EBP-14h]处。你回忆一下前面我讲的编译器为 GetExceptionInformation和 GetExceptionCode 函数生成的汇编代码就会意识到,这实际上初始化了这两个函数使用的指针。 接着,__except_handler3从EXCEPTION_REGISTRATION帧中获取当前的trylevel(在[EBP-04h]处)。 trylevel变量实际是scopetable数组的索引,而正是这个数组才使得一个函数中的多个__try块和嵌套的__try块能够仅使用一个 EXCEPTION_REGISTRATION结构。每个scopetable元素结构如下: typedef struct _SCOPETABLE { DWORD previousTryLevel; DWORD lpfnFilter; DWORD lpfnHandler; } SCOPETABLE, *PSCOPETABLE; SCOPETABLE结构中的第二个成员和第三个成员比较容易理解。它们分别是过滤器表达式代码的地址和相应的__except块的地址。但是prviousTryLevel成员有点复杂。总之一句话,它用于嵌套的__try块。这里的关键是函数中的每个__try块都有一个相应的SCOPETABLE结构。 ShowSEHFrames 程序 如果你现在觉得已经被EXCEPTION_REGISTRATION、scopetable、trylevel、过滤器表达式以及展开等等之类的词搞得晕头转向的话,那和我最初的感觉一样。但是编译器层面的结构化异常处理方面的知识并不适合一点一点的学。除非你从整体上理解它,否则有很多内容单独看并没有什么意义。当面对大堆的理论时,我最自然的做法就是写一些应用我学到的理论方面的程序。如果它能够按照预料的那样工作,我就知道我的理解(通常)是正确的。 ShowSEHFrames程序中比较重要的函数是WalkSEHFrames和ShowSEHFrame。WalkSEHFrames函数首选打印出 __except_handler3的地址,打印它的原因很快就清楚了。接着,它从FS:[0]处获取异常链表的头指针,然后遍历该链表。此链表中每个结点都是一个VC_EXCEPTION_REGISTRATION类型的结构,它是我自己定义的,用于描述Visual C++的异常处理帧。对于这个链表中的每个结点,WalkSEHFrames都把指向这个结点的指针传递给ShowSEHFrame函数。 你可能想知道为什么明明 ShowSEHFrames 程序只有两个函数使用SEH,但是却有三个异常处理帧使用__except_handler3作为它们的异常回调函数。实际上第三个帧来自 Visual C++ 运行时库。Visual C++ 运行时库源代码中的 CRT0.C 文件清楚地表明了对 main 或 WinMain 的调用也被一个__try/__except 块封装着。这个__try 块的过滤器表达式代码可以在 WINXFLTR.C文 件中找到。 展开 在挖掘展开(Unwinding)的实现代码之前让我们先来搞清楚它的意思。我在前面已经讲过所有可能的异常处理程序是如何被组织在一个由线程信息块的第一个DWORD(FS:[0])所指向的链表中的。由于针对某个特定异常的处理程序可能不在这个链表的开头,因此就需要从链表中依次移除实际处理异常的那个异常处理程序之前的所有异常处理程序。 RtlUnwind 函数的伪代码: void _RtlUnwind( PEXCEPTION_REGISTRATION pRegistrationFrame, PVOID returnAddr, // 并未使用!(至少是在i386机器上) PEXCEPTION_RECORD pExcptRec, DWORD _eax_value) { DWORD stackUserBase; DWORD stackUserTop; PEXCEPTION_RECORD pExcptRec; EXCEPTION_RECORD exceptRec; CONTEXT context; // 从FS:[4]和FS:[8]处获取堆栈的界限 RtlpGetStackLimits( &stackUserBase, &stackUserTop ); if ( 0 == pExcptRec ) // 正常情况 { pExcptRec = &excptRec; pExcptRec->ExceptionFlags = 0; pExcptRec->ExceptionCode = STATUS_UNWIND; pExcptRec->ExceptionRecord = 0; pExcptRec->ExceptionAddress = [ebp+4]; // RtlpGetReturnAddress()―获取返回地址 pExcptRec->ExceptionInformation[0] = 0; } if ( pRegistrationFrame ) pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING; else // 这两个标志合起来被定义为EXCEPTION_UNWIND_CONTEXT pExcptRec->ExceptionFlags|=(EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND); context.ContextFlags =( CONTEXT_i486 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS); RtlpCaptureContext( &context ); context.Esp += 0x10; context.Eax = _eax_value; PEXCEPTION_REGISTRATION pExcptRegHead; pExcptRegHead = RtlpGetRegistrationHead(); // 返回FS:[0]的值 // 开始遍历EXCEPTION_REGISTRATION结构链表 while ( -1 != pExcptRegHead ) { EXCEPTION_RECORD excptRec2; if ( pExcptRegHead == pRegistrationFrame ) { NtContinue( &context, 0 ); } else { // 如果存在某个异常帧在堆栈上的位置比异常链表的头部还低 // 说明一定出现了错误 if ( pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead) ) { // 生成一个异常 excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; RtlRaiseException( &exceptRec2 ); } } PVOID pStack = pExcptRegHead + 8; // 8 = sizeof(EXCEPTION_REGISTRATION) // 确保pExcptRegHead在堆栈范围内,并且是4的倍数 if ( (stackUserBase <= pExcptRegHead ) && (stackUserTop >= pStack ) && (0 == (pExcptRegHead & 3)) ) { DWORD pNewRegistHead; DWORD retValue; retValue = RtlpExecutehandlerForUnwind(pExcptRec, pExcptRegHead, &context, &pNewRegistHead, pExceptRegHead->handler ); if ( retValue != DISPOSITION_CONTINUE_SEARCH ) { if ( retValue != DISPOSITION_COLLIDED_UNWIND ) { excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; RtlRaiseException( &excptRec2 ); } else pExcptRegHead = pNewRegistHead; } PEXCEPTION_REGISTRATION pCurrExcptReg = pExcptRegHead; pExcptRegHead = pExcptRegHead->prev; RtlpUnlinkHandler( pCurrExcptReg ); } else // 堆栈已经被破坏!生成一个异常 { excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; excptRec2.ExceptionCode = STATUS_BAD_STACK; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; RtlRaiseException( &excptRec2 ); } } // 如果执行到这里,说明已经到了EXCEPTION_REGISTRATION // 结构链表的末尾,正常情况下不应该发生这种情况。 //(因为正常情况下异常应该被处理,这样就不会到链表末尾) if ( -1 == pRegistrationFrame ) NtContinue( &context, 0 ); else NtRaiseException( pExcptRec, &context, 0 ); } RtlUnwind函数的伪代码到这里就结束了,以下是它调用的几个函数的伪代码: PEXCEPTION_REGISTRATION RtlpGetRegistrationHead( void ) { return FS:[0]; } RtlpUnlinkHandler( PEXCEPTION_REGISTRATION pRegistrationFrame ) { FS:[0] = pRegistrationFrame->prev; } void RtlpCaptureContext( CONTEXT * pContext ) { pContext->Eax = 0; pContext->Ecx = 0; pContext->Edx = 0; pContext->Ebx = 0; pContext->Esi = 0; pContext->Edi = 0; pContext->SegCs = CS; pContext->SegDs = DS; pContext->SegEs = ES; pContext->SegFs = FS; pContext->SegGs = GS; pContext->SegSs = SS; pContext->EFlags = flags; // 它对应的汇编代码为__asm{ PUSHFD / pop [xxxxxxxx] } pContext->Eip = 此函数的调用者的调用者的返回地址 // 读者看一下这个函数的 pContext->Ebp = 此函数的调用者的调用者的EBP // 汇编代码就会清楚这一点 pContext->Esp = pContext->Ebp + 8; } 虽然 RtlUnwind 函数的规模看起来很大,但是如果你按一定方法把它分开,其实并不难理解。它首先从FS:[4]和FS:[8]处获取当前线程堆栈的界限。它们对于后面要进行的合法性检查非常重要,以确保所有将要被展开的异常帧都在堆栈范围内。RtlUnwind 接着在堆栈上创建了一个空的EXCEPTION_RECORD结构并把STATUS_UNWIND赋给它的ExceptionCode域,同时把 EXCEPTION_UNWINDING标志赋给它的 ExceptionFlags 域。指向这个结构的指针作为其中一个参数被传递给每个异常回调函数。然后,这个函数调用RtlCaptureContext函数来创建一个空的CONTEXT结构,这个结构也变成了在展开阶段调用每个异常回调函数时传递给它们的一个参数。 RtlUnwind函数的其余部分遍历EXCEPTION_REGISTRATION结构链表。对于其中的每个帧,它都调用 RtlpExecuteHandlerForUnwind 函数,后面我会讲到这个函数。正是这个函数带 EXCEPTION_UNWINDING 标志调用了异常处理回调函数。每次回调之后,它调用RtlpUnlinkHandler 移除相应的异常帧。 RtlUnwind 函数的第一个参数是一个帧的地址,当它遍历到这个帧时就停止展开异常帧。上面所说的这些代码之间还有一些安全性检查代码,它们用来确保不出问题。如果出现任何问题,RtlUnwind 就引发一个异常,指示出了什么问题,并且这个异常带有EXCEPTION_NONCONTINUABLE 标志。当一个进程被设置了这个标志时,它就不允许再运行,必须终止。 未处理异常 在文章的前面,我并没有全面描述 UnhandledExceptionFilter 这个 API。通常情况下你并不直接调用它(尽管你可以这么做)。大多数情况下它都是由 KERNEL32 中进行默认异常处理的过滤器表达式代码调用。前面 BaseProcessStart 函数的伪代码已经表明了这一点。 图十三 UnHandledExceptionFilter 函数的伪代码 UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *pExceptionPtrs ) { PEXCEPTION_RECORD pExcptRec; DWORD currentESP; DWORD retValue; DWORD DEBUGPORT; DWORD dwTemp2; DWORD dwUseJustInTimeDebugger; CHAR szDbgCmdFmt[256]; // 从AeDebug这个注册表键值返回的字符串 CHAR szDbgCmdLine[256]; // 实际的调试器命令行参数(已填入进程ID和事件ID) STARTUPINFO startupinfo; PROCESS_INFORMATION pi; HARDERR_STRUCT harderr; // ??? BOOL fAeDebugAuto; TIB * pTib; // 线程信息块 pExcptRec = pExceptionPtrs->ExceptionRecord; if ( (pExcptRec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) && (pExcptRec->ExceptionInformation[0]) ) { retValue=BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]); if ( EXCEPTION_CONTINUE_EXECUTION == retValue ) return EXCEPTION_CONTINUE_EXECUTION; } // 查看这个进程是否运行于调试器下 retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &debugPort, sizeof(debugPort), 0 ); if ( (retValue >= 0) && debugPort ) // 通知调试器 return EXCEPTION_CONTINUE_SEARCH; // 用户调用SetUnhandledExceptionFilter了吗? // 如果调用了,那现在就调用他安装的异常处理程序 if ( _BasepCurrentTopLevelFilter ) { retValue = _BasepCurrentTopLevelFilter( pExceptionPtrs ); if ( EXCEPTION_EXECUTE_HANDLER == retValue ) return EXCEPTION_EXECUTE_HANDLER; if ( EXCEPTION_CONTINUE_EXECUTION == retValue ) return EXCEPTION_CONTINUE_EXECUTION; // 只有返回值为EXCEPTION_CONTINUE_SEARCH时才会继续执行下去 } // 调用过SetErrorMode(SEM_NOGPFAULTERRORBOX)吗? { harderr.elem0 = pExcptRec->ExceptionCode; harderr.elem1 = pExcptRec->ExceptionAddress; if ( EXCEPTION_IN_PAGE_ERROR == pExcptRec->ExceptionCode ) harderr.elem2 = pExcptRec->ExceptionInformation[2]; else harderr.elem2 = pExcptRec->ExceptionInformation[0]; dwTemp2 = 1; fAeDebugAuto = FALSE; harderr.elem3 = pExcptRec->ExceptionInformation[1]; pTib = FS:[18h]; DWORD someVal = pTib->pProcess->0xC; if ( pTib->threadID != someVal ) { __try { char szDbgCmdFmt[256]; retValue = GetProfileStringA( "AeDebug", "Debugger", 0, szDbgCmdFmt, sizeof(szDbgCmdFmt)-1 ); if ( retValue ) dwTemp2 = 2; char szAuto[8]; retValue = GetProfileStringA( "AeDebug", "Auto", "0", szAuto, sizeof(szAuto)-1 ); if ( retValue ) if ( 0 == strcmp( szAuto, "1" ) ) if ( 2 == dwTemp2 ) fAeDebugAuto = TRUE; } __except( EXCEPTION_EXECUTE_HANDLER ) { ESP = currentESP; dwTemp2 = 1; fAeDebugAuto = FALSE; } } if ( FALSE == fAeDebugAuto ) { retValue=NtRaiseHardError(STATUS_UNHANDLED_EXCEPTION | 0x10000000, 4, 0, &harderr,_BasepAlreadyHadHardError ? 1 : dwTemp2, &dwUseJustInTimeDebugger ); } else { dwUseJustInTimeDebugger = 3; retValue = 0; } if (retValue >= 0 && (dwUseJustInTimeDebugger == 3) && (!_BasepAlreadyHadHardError)&&(!_BaseRunningInServerProcess)) { _BasepAlreadyHadHardError = 1; SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE }; HANDLE hEvent = CreateEventA( &secAttr, TRUE, 0, 0 ); memset( &startupinfo, 0, sizeof(startupinfo) ); sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent); startupinfo.cb = sizeof(startupinfo); startupinfo.lpDesktop = "Winsta0\Default" CsrIdentifyAlertableThread(); // ??? retValue = CreateProcessA( 0, // 应用程序名称 szDbgCmdLine, // 命令行 0, 0, // 进程和线程安全属性 1, // bInheritHandles 0, 0, // 创建标志、环境 0, // 当前目录 &statupinfo, // STARTUPINFO π); // PROCESS_INFORMATION if ( retValue && hEvent ) { NtWaitForSingleObject( hEvent, 1, 0 ); return EXCEPTION_CONTINUE_SEARCH; } } if ( _BasepAlreadyHadHardError ) NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode); } return EXCEPTION_EXECUTE_HANDLER; } LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter ) { // _BasepCurrentTopLevelFilter是KERNEL32.DLL中的一个全局变量 LPTOP_LEVEL_EXCEPTION_FILTER previous= _BasepCurrentTopLevelFilter; // 设置为新值 _BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter; return previous; // 返回以前的值 } UnhandledExceptionFilter接下来的任务是确定进程是否运行于Win32调试器下。也就是进程的创建标志中是否带有标志DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS。它使用NtQueryInformationProcess函数来确定进程是否正在被调试,我在本月的Under the Hood专栏中讲解了这个函数。如果正在被调试,UnhandledExceptionFilter就返回 EXCEPTION_CONTINUE_SEARCH,这告诉系统去唤醒调试器并告诉它在被调试程序(debuggee)中产生了一个异常。UnhandledExceptionFilter接下来调用用户安装的未处理异常过滤器(如果存在的话)。通常情况下,用户并没有安装回调函数,但是用户可以调用 SetUnhandledExceptionFilter这个API来安装。上面我也提供了这个API的伪代码。这个函数只是简单地用用户安装的回调函数的地址来替换一个全局变量,并返回替换前的值。 有了初步的准备之后,UnhandledExceptionFilter就开始做它的主要工作:用一个时髦的应用程序错误对话框来通知你犯了低级的编程错误。有两种方法可以避免出现这个对话框。第一种方法是调用SetErrorMode函数并指定SEM_NOGPFAULTERRORBOX标志。另一种方法是将AeDebug子键下的Auto的值设为1。此时UnhandledExceptionFilter跳过应用程序错误对话框直接启动AeDebug 子键下的Debugger的值所指定的调试器。如果你熟悉“即时调试(Just In Time Debugging,JIT)”的话,这就是操作系统支持它的地方。接下来我会详细讲。 大多数情况下,上面的两个条件都为假。这样UnhandledExceptionFilter就调用NTDLL.DLL中的 NtRaiseHardError函数。正是这个函数产生了应用程序错误对话框。这个对话框等待你单击“确定”按钮来终止进程,或者单击“取消”按钮来调试它。(单击“取消”按钮而不是“确定”按钮来加载调试器好像有点颠倒了,可能这只是我个人的感觉吧。) 如果你单击“确定”,UnhandledExceptionFilter就返回EXCEPTION_EXECUTE_HANDLER。调用UnhandledExceptionFilter 的进程通常通过终止自身来作为响应(正像你在BaseProcessStart的伪代码中看到的那样)。这就产生了一个有趣的问题――大多数人都认为是系统终止了产生未处理异常的进程,而实际上更准确的说法应该是,系统进行了一些设置使得产生未处理异常的进程将自身终止掉了。 UnhandledExceptionFilter执行时真正有意思的部分是当你单击应用程序错误对话框中的“取消”按钮,此时系统将调试器附加(attach)到出错进程上。这段代码首先调用 CreateEvent来创建一个事件内核对象,调试器成功附加到出错进程之后会将此事件对象变成有信号状态。这个事件句柄以及出错进程的ID都被传到 sprintf函数,由它将其格式化成一个命令行,用来启动调试器。一切就绪之后,UnhandledExceptionFilter就调用 CreateProcess来启动调试器。如果CreateProcess成功,它就调用NtWaitForSingleObject来等待前面创建的那个事件对象。此时这个调用被阻塞,直到调试器进程将此事件变成有信号状态,以表明它已经成功附加到出错进程上。UnhandledExceptionFilter函数中还有一些其它的代码,我在这里只讲重要的。 进入地狱 如果你已经走了这么远,不把整个过程讲完对你有点不公平。我已经讲了当异常发生时操作系统是如何调用用户定义的回调函数的。我也讲了这些回调的内部情况,以及编译器是如何使用它们来实现__try和__except的。我甚至还讲了当某个异常没有被处理时所发生的情况以及系统所做的扫尾工作。剩下的就只有异常回调过程最初是从哪里开始的这个问题了。好吧,让我们深入系统内部来看一下结构化异常处理的开始阶段吧。 图十四 KiUserExceptionDispatcher 的伪代码: KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext ) { DWORD retValue; // 注意:如果异常被处理,那么 RtlDispatchException 函数就不会返回 if ( RtlDispatchException( pExceptRec, pContext ) ) retValue = NtContinue( pContext, 0 ); else retValue = NtRaiseException( pExceptRec, pContext, 0 ); EXCEPTION_RECORD excptRec2; excptRec2.ExceptionCode = retValue; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; RtlRaiseException( &excptRec2 ); } int RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext ) { DWORD stackUserBase; DWORD stackUserTop; PEXCEPTION_REGISTRATION pRegistrationFrame; DWORD hLog; // 从FS:[4]和FS:[8]处获取堆栈的界限 RtlpGetStackLimits( &stackUserBase, &stackUserTop ); pRegistrationFrame = RtlpGetRegistrationHead(); while ( -1 != pRegistrationFrame ) { PVOID justPastRegistrationFrame = &pRegistrationFrame + 8; if ( stackUserBase > justPastRegistrationFrame ) { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( stackUsertop < justPastRegistrationFrame ) { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( pRegistrationFrame & 3 ) // 确保堆栈按DWORD对齐 { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( someProcessFlag ) { hLog = RtlpLogExceptionHandler( pExcptRec, pContext, 0, pRegistrationFrame, 0x10 ); } DWORD retValue, dispatcherContext; retValue= RtlpExecuteHandlerForException(pExcptRec, pRegistrationFrame, pContext, &dispatcherContext, pRegistrationFrame->handler ); if ( someProcessFlag ) RtlpLogLastExceptionDisposition( hLog, retValue ); if ( 0 == pRegistrationFrame ) { pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL; // 关闭标志 } EXCEPTION_RECORD excptRec2; DWORD yetAnotherValue = 0; if ( DISPOSITION_DISMISS == retValue ) { if ( pExcptRec->ExceptionFlags & EH_NONCONTINUABLE ) { excptRec2.ExceptionRecord = pExcptRec; excptRec2.ExceptionNumber = STATUS_NONCONTINUABLE_EXCEPTION; excptRec2.ExceptionFlags = EH_NONCONTINUABLE; excptRec2.NumberParameters = 0; RtlRaiseException( &excptRec2 ); } else return DISPOSITION_CONTINUE_SEARCH; } else if ( DISPOSITION_CONTINUE_SEARCH == retValue ) {} else if ( DISPOSITION_NESTED_EXCEPTION == retValue ) { pExcptRec->ExceptionFlags |= EH_EXIT_UNWIND; if ( dispatcherContext > yetAnotherValue ) yetAnotherValue = dispatcherContext; } else // DISPOSITION_COLLIDED_UNWIND { excptRec2.ExceptionRecord = pExcptRec; excptRec2.ExceptionNumber = STATUS_INVALID_DISPOSITION; excptRec2.ExceptionFlags = EH_NONCONTINUABLE; excptRec2.NumberParameters = 0; RtlRaiseException( &excptRec2 ); } pRegistrationFrame = pRegistrationFrame->prev; // 转到前一个帧 } return DISPOSITION_DISMISS; } _RtlpExecuteHandlerForException: // 处理异常(第一次) MOV EDX,XXXXXXXX JMP ExecuteHandler RtlpExecutehandlerForUnwind: // 处理展开(第二次) MOV EDX,XXXXXXXX int ExecuteHandler( PEXCEPTION_RECORD pExcptRec, PEXCEPTION_REGISTRATION pExcptReg, CONTEXT * pContext, PVOID pDispatcherContext, FARPROC handler ) // 实际上是指向_except_handler()的指针 { // 安装一个EXCEPTION_REGISTRATION帧,EDX指向相应的handler代码 PUSH EDX PUSH FS:[0] MOV FS:[0],ESP // 调用异常处理回调函数 EAX = handler( pExcptRec, pExcptReg, pContext, pDispatcherContext ); // 移除EXCEPTION_REGISTRATION帧 MOV ESP,DWORD PTR FS:[00000000] POP DWORD PTR FS:[00000000] return EAX; } _RtlpExecuteHandlerForException使用的异常处理程序: { // 如果设置了展开标志,返回DISPOSITION_CONTINUE_SEARCH // 否则,给pDispatcherContext赋值并返回DISPOSITION_NESTED_EXCEPTION return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ? DISPOSITION_CONTINUE_SEARC : ( *pDispatcherContext = pRegistrationFrame->scopetable, DISPOSITION_NESTED_EXCEPTION ); } _RtlpExecuteHandlerForUnwind使用的异常处理程序: { // 如果设置了展开标志,返回DISPOSITION_CONTINUE_SEARCH // 否则,给pDispatcherContext赋值并返回DISPOSITION_COLLIDED_UNWIND return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ? DISPOSITION_CONTINUE_SEARCH : ( *pDispatcherContext = pRegistrationFrame->scopetable, DISPOSITION_COLLIDED_UNWIND ); } KiUserExceptionDispatcher 的核心是对 RtlDispatchException 的调用。这拉开了搜索已注册的异常处理程序的序幕。如果某个处理程序处理这个异常并继续执行,那么对 RtlDispatchException 的调用就不会返回。如果它返回了,只有两种可能:或者调用了NtContinue以便让进程继续执行,或者产生了新的异常。如果是这样,那异常就不能再继续处理了,必须终止进程。现在把目光对准 RtlDispatchException 函数的代码,这就是我通篇提到的遍历异常帧的代码。这个函数获取一个指向EXCEPTION_REGISTRATION 结构链表的指针,然后遍历此链表以寻找一个异常处理程序。由于堆栈可能已经被破坏了,所以这个例程非常谨慎。在调用每个EXCEPTION_REGISTRATION结构中指定的异常处理程序之前,它确保这个结构是按DWORD对齐的,并且是在线程的堆栈之中,同时在堆栈中比前一个EXCEPTION_REGISTRATION结构高。 RtlDispatchException并不直接调用EXCEPTION_REGISTRATION结构中指定的异常处理程序。相反,它调用 RtlpExecuteHandlerForException来完成这个工作。根据RtlpExecuteHandlerForException的执行情况,RtlDispatchException或者继续遍历异常帧,或者引发另一个异常。这第二次的异常表明异常处理程序内部出现了错误,这样就不能继续执行下去了。 RtlpExecuteHandlerForException的代码与RtlpExecuteHandlerForUnwind的代码极其相似。你可能会回忆起来在前面讨论展开时我提到过它。这两个“函数”都只是简单地给EDX寄存器加载一个不同的值然后就调用ExecuteHandler函数。也就是说,RtlpExecuteHandlerForException和RtlpExecuteHandlerForUnwind都是 ExecuteHanlder这个公共函数的前端。 ExecuteHandler查找EXCEPTION_REGISTRATION结构的handler域的值并调用它。令人奇怪的是,对异常处理回调函数的调用本身也被一个结构化异常处理程序封装着。在SEH自身中使用SEH看起来有点奇怪,但你思索一会儿就会理解其中的含义。如果在异常回调过程中引发了另外一个异常,操作系统需要知道这个情况。根据异常发生在最初的回调阶段还是展开回调阶段,ExecuteHandler或者返回DISPOSITION_NESTED_EXCEPTION,或者返回DISPOSITION_COLLIDED_UNWIND。这两者都是“红色警报!现在把一切都关掉!”类型的代码。 如果你像我一样,那不仅理解所有与SEH有关的函数非常困难,而且记住它们之间的调用关系也非常困难。为了帮助我自己记忆,我画了一个调用关系图(图十五)。 图十五 在SEH中是谁调用了谁 KiUserExceptionDispatcher() RtlDispatchException() RtlpExecuteHandlerForException() ExecuteHandler() // 通常到 __except_handler3 __except_handler3() scopetable filter-expression() __global_unwind2() RtlUnwind() RtlpExecuteHandlerForUnwind() scopetable __except block() 现在要问:在调用ExecuteHandler之前设置EDX寄存器的值有什么用呢?这非常简单。如果ExecuteHandler在调用用户安装的异常处理程序的过程中出现了什么错误,它就把EDX指向的代码作为原始的异常处理程序。它把EDX寄存器的值压入堆栈作为原始的 EXCEPTION_REGISTRATION结构的handler域。这基本上与我在MYSEH和MYSEH2中对原始的结构化异常处理的使用情况一样。 结构化异常处理是Win32一个非常好的特性。多亏有了像Visual C++之类的编译器的支持层对它的封装,一般的程序员才能付出比较小的学习代价就能利用SEH所提供的便利。但是在操作系统层面上,事情远比Win32文档说的复杂。 附录:关于 “prolog 和 epilog ” 在 Visual C++ 文档中,微软对 prolog 和 epilog 的解释是:“保护现场和恢复现场” 此附录摘自微软 MSDN 库,详细信息参见: http://msdn.microsoft.com/en-us/library/tawsa7cb(VS.80).aspx(英文) http://msdn.microsoft.com/zh-cn/library/tawsa7cb(VS.80).aspx(中文) 每个分配堆栈空间、调用其他函数、保存非易失寄存器或使用异常处理的函数必须具有 Prolog,Prolog 的地址限制在与各自的函数表项关联的展开数据中予以说明(请参见异常处理 (x64))。Prolog 将执行以下操作:必要时将参数寄存器保存在其内部地址中;将非易失寄存器推入堆栈;为局部变量和临时变量分配堆栈的固定部分;(可选)建立帧指针。关联的展开数据必须描述 Prolog 的操作,必须提供撤消 Prolog 代码的影响所需的信息。 此 Prolog 执行以下操作:将参数寄存器 RCX 存储在其标识位置;保存非易失寄存器 R13、R14、R15;分配堆栈帧的固定部分;建立帧指针,该指针将 128 字节地址指向固定分配区域。使用偏移量以后,便可以通过单字节偏移量对多个固定分配区域进行寻址。 .除了 R10、R11 和条件代码以外,此 __chkstk helper 函数不会修改任何寄存器。特别是,此函数将返回未更改的 RAX,并且不会修改所有非易失寄存器和参数传递寄存器。 如果函数中使用了帧指针,则在执行 Epilog 之前必须将堆栈修整为其固定分配。这在技术上不属于 Epilog。例如,下面的 Epilog 可用于撤消前面使用的 Prolog: lea RSP, -128[R13] ; epilogue proper starts here add RSP, fixed-allocation-size pop R13 pop R14 pop R15 ret在实际应用中,使用帧指针时,没有必要分两个步骤调整 RSP,因此应改用以下 Epilog: lea RSP, fixed-allocation-size �C 128[R13] pop R13 pop R14 pop R15 ret 以上是 Epilog 的唯一合法形式。它必须由 add RSP,constant 或 lea RSP,constant[FPReg] 组成,后跟一系列零或多个 8 字节寄存器 pop、一个 return 或一个 jmp。(Epilog 中只允许 jmp 语句的子集。仅限于具有 ModRM 内存引用的 jmp 类,其中 ModRM mod 字段值为 00。在 ModRM mod 字段值为 01 或 10 的 Epilog 中禁止使用 jmp。有关允许使用的 ModRM 引用的更多信息,请参见“AMD x86-64 Architecture Programmer’s Manual Volume 3: General Purpose and System Instructions”(AMD x86-64 结构程序员手册第 3 卷:通用指令和系统指令)中的表 A-15。)不能出现其他代码。特别是,不能在 Epilog 内进行调度,包括加载返回值。 |