虽然__except_handler3的代码看起来很多,但是记住一点:它只是一个我在文章开头讲过的异常处理回调函数。它同MYSEH.EXE和 MYSEH2.EXE中的异常回调函数都带有同样的四个参数。__except_handler3大体上可以由第一个if语句分为两部分。这是由于这个函数可以在两种情况下被调用,一次是正常调用,另一次是在展开阶段。其中大部分是在非展开阶段的回调。 __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结构。正如我前面所说,当前的trylevel指定了要使用的scopetable数组的哪一个元素,最终也就是指定了过滤器表达式和__except块的地址。现在想像一下两个__try块嵌套的情形。如果内层__try块的过滤器表达式不处理某个异常,那外层__try块的过滤器表达式就必须处理它。那现在要问,__except_handler3是如何知道SCOPETABLE数组的哪个元素相应于外层的__try块的呢?答案是:外层__try块的索引由 SCOPETABLE结构的previousTryLevel域给出。利用这种机制,你可以嵌套任意层的__try块。previousTryLevel 域就好像是一个函数中所有可能的异常处理程序构成的线性链表中的结点一样。如果trylevel的值为0xFFFFFFFF(实际上就是-1,这个值在 EXSUP.INC中被定义为TRYLEVEL_NONE),标志着这个链表结束。 回到__except_handler3的代码中。在获取了当前的trylevel之后,它就调用相应的SCOPETABLE结构中的过滤器表达式代码。如果过滤器表达式返回EXCEPTION_CONTINUE_SEARCH,__exception_handler3 移向SCOPETABLE数组中的下一个元素,这个元素的索引由previousTryLevel域给出。如果遍历完整个线性链表(还记得吗?这个链表是由于在一个函数内部嵌套使用__try块而形成的)都没有找到处理这个异常的代码,__except_handler3返回DISPOSITION_CONTINUE_SEARCH(原文如此,但根据_except_handler函数的定义,这个返回值应该为ExceptionContinueSearch。实际上这两个常量的值是一样的。我在伪代码中已经将其改正过来了),这导致系统移向下一个EXCEPTION_REGISTRATION帧(这个链表是由于函数嵌套调用而形成的)。 如果过滤器表达式返回EXCEPTION_EXECUTE_HANDLER,这意味着异常应该由相应的__except块处理。它同时也意味着所有前面的EXCEPTION_REGISTRATION帧都应该从链表中移除,并且相应的__except块都应该被执行。第一个任务通过调用__global_unwind2来完成的,后面我会讲到这个函数。跳过这中间的一些清理代码,流程离开__except_handler3转向__except块。令人奇怪的是,流程并不从__except块中返回,虽然是 __except_handler3使用CALL指令调用了它。 当前的trylevel值是如何被设置的呢?它实际上是由编译器隐含处理的。编译器非常机灵地修改这个扩展的EXCEPTION_REGISTRATION 结构中的trylevel域的值(实际上是生成修改这个域的值的代码)。如果你检查编译器为使用SEH的函数生成的汇编代码,就会在不同的地方都看到修改这个位于[EBP-04h]处的trylevel域的值的代码。 __except_handler3是如何做到既通过CALL指令调用__except块而又不让执行流程返回呢?由于CALL指令要向堆栈中压入了一个返回地址,你可以想象这有可能破坏堆栈。如果你检查一下编译器为__except块生成的代码,你会发现它做的第一件事就是将EXCEPTION_REGISTRATION结构下面8个字节处(即[EBP-18H]处)的一个DWORD值加载到ESP寄存器中(实际代码为MOV ESP,DWORD PTR [EBP-18H]),这个值是在函数的prolog代码中被保存在这个位置的(实际代码为MOV DWORD PTR [EBP-18H],ESP)。 ShowSEHFrames程序 如果你现在觉得已经被EXCEPTION_REGISTRATION、scopetable、trylevel、过滤器表达式以及展开等等之类的词搞得晕头转向的话,那和我最初的感觉一样。但是编译器层面的结构化异常处理方面的知识并不适合一点一点的学。除非你从整体上理解它,否则有很多内容单独看并没有什么意义。当面对大堆的理论时,我最自然的做法就是写一些应用我学到的理论方面的程序。如果它能够按照预料的那样工作,我就知道我的理解(通常)是正确的。 图 10是ShowSEHFrame.EXE的源代码。它使用__try/__except块设置了好几个Visual C++ SEH帧。然后它显示每一个帧以及Visual C++为每个帧创建的scopetable的相关信息。这个程序本身并不生成也不依赖任何异常。相反,我使用了多个__try块以强制Visual C++生成多个EXCEPTION_REGISTRATION帧以及相应的scopetable。 图10 ShowSEHFrames.CPP //=========================================================// ShowSEHFrames - Matt Pietrek 1997// Microsoft Systems Journal, February 1997// FILE: ShowSEHFrames.CPP// 使用命令行CL ShowSehFrames.CPP进行编译//===============================================, ========== #define WIN32_LEAN_AND_MEAN #include <windows.h>#include <stdio.h> #pragma hdrstop //-------------------------------------------------------------------// 本程序仅适用于Visual C++,它使用的数据结构是特定于Visual C++的//------------------------------------------------------------------- #ifndef _MSC_VER#error Visual C++ Required (Visual C++ specific information is displayed)#endif //-------------------------------------------------------------------// 结构定义//------------------------------------------------------------------- // 操作系统定义的基本异常帧struct EXCEPTION_REGISTRATION{ EXCEPTION_REGISTRATION* prev; FARPROC handler;}; // Visual C++扩展异常帧指向的数据结构struct scopetable_entry{ DWORD previousTryLevel; FARPROC lpfnFilter; FARPROC lpfnHandler;}; // Visual C++使用的扩展异常帧struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION{ scopetable_entry * scopetable; int trylevel; int _ebp;}; //----------------------------------------------------------------// 原型声明//---------------------------------------------------------------- // __except_handler3是Visual C++运行时库函数,我们想打印出它的地址// 但是它的原型并没有出现在任何头文件中,所以我们需要自己声明它。extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *, PCONTEXT, PEXCEPTION_RECORD); //-------------------------------------------------------------// 代码//------------------------------------------------------------- //// 显示一个异常帧及其相应的scopetable的信息//void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec ){ printf( "Frame: X Handler: X Prev: X Scopetable: X/n", pVCExcRec, pVCExcRec->handler, pVCExcRec->prev, pVCExcRec->scopetable ); scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable; for ( unsigned i = 0; i <= pVCExcRec->trylevel; i++ ){ printf( " scopetable[%u] PrevTryLevel: X " "filter: X __except: X/n", i, pScopeTableEntry->previousTryLevel, pScopeTableEntry->lpfnFilter, pScopeTableEntry->lpfnHandler ); pScopeTableEntry++;} printf( "/n" ); } //// 遍历异常帧的链表,按顺序显示它们的信息//void WalkSEHFrames( void ){ VC_EXCEPTION_REGISTRATION * pVCExcRec; // 打印出__except_handler3函数的位置printf( "_except_handler3 is at address: X/n", _except_handler3 );printf( "/n" ); // 从FS:[0]处获取指向链表头的指针__asm mov eax, FS:[0]__asm mov [pVCExcRec], EAX // 遍历异常帧的链表。0xFFFFFFFF标志着链表的结尾while ( 0xFFFFFFFF != (unsigned)pVCExcRec ){ ShowSEHFrame( pVCExcRec ); pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);} } void Function1( void ){ // 嵌套3层__try块以便强制为scopetable数组产生3个元素 __try { __try { __try { WalkSEHFrames(); // 现在显示所有的异常帧的信息 } __except( EXCEPTION_CONTINUE_SEARCH ) {} } __except( EXCEPTION_CONTINUE_SEARCH ) {} } __except( EXCEPTION_CONTINUE_SEARCH ) {} } int main() {int i; // 使用两个__try块(并不嵌套),这导致为scopetable数组生成两个元素__try{ i = 0x1234;} __except( EXCEPTION_CONTINUE_SEARCH ){ i = 0x4321;} __try{ Function1(); // 调用一个设置更多异常帧的函数} __except( EXCEPTION_EXECUTE_HANDLER ){ // 应该永远不会执行到这里,因为我们并没有打算产生任何异常 printf( "Caught Exception in main/n" );} return 0; } ShowSEHFrames程序中比较重要的函数是WalkSEHFrames和ShowSEHFrame。WalkSEHFrames函数首选打印出 __except_handler3的地址,打印它的原因很快就清楚了。接着,它从FS:[0]处获取异常链表的头指针,然后遍历该链表。此链表中每个结点都是一个VC_EXCEPTION_REGISTRATION类型的结构,它是我自己定义的,用于描述Visual C++的异常处理帧。对于这个链表中的每个结点,WalkSEHFrames都把指向这个结点的指针传递给ShowSEHFrame函数。 ShowSEHFrame函数一开始就打印出异常处理帧的地址、异常处理回调函数的地址、前一个异常处理帧的地址以及scopetable的地址。接着,对于每个 scopetable数组中的元素,它都打印出其priviousTryLevel、过滤器表达式的地址以及相应的__except块的地址。我是如何知道scopetable数组中有多少个元素的呢?其实我并不知道。但是我假定VC_EXCEPTION_REGISTRATION结构中的当前trylevel域的值比scopetable数组中的元素总数少1。 图 11是ShowSEHFrames的运行结果。首先检查以“Frame:”开头的每一行,你会发现它们显示的异常处理帧在堆栈上的地址呈递增趋势,并且在前三个帧中,它们的异常处理程序的地址是一样的(都是004012A8)。再看输出的开始部分,你会发现这个004012A8不是别的,它正是 Visual C++运行时库函数__except_handler3的地址。这证明了我前面所说的单个回调函数处理所有异常这一点。图11 ShowSEHFrames运行结果 你可能想知道为什么明明ShowSEHFrames程序只有两个函数使用SEH,但是却有三个异常处理帧使用__except_handler3作为它们的异常回调函数。实际上第三个帧来自Visual C++运行时库。Visual C++运行时库源代码中的CRT0.C文件清楚地表明了对main或WinMain的调用也被一个__try/__except块封装着。这个__try 块的过滤器表达式代码可以在WINXFLTR.C文件中找到。 回到ShowSEHFrames程序,注意到最后一个帧的异常处理程序的地址是77F3AB6C,这与其它三个不同。仔细观察一下,你会发现这个地址在 KERNEL32.DLL中。这个特别的帧就是由KERNEL32.DLL中的BaseProcessStart函数安装的,这在前面我已经说过。 展开 在挖掘展开(Unwinding)的实现代码之前让我们先来搞清楚它的意思。我在前面已经讲过所有可能的异常处理程序是如何被组织在一个由线程信息块的第一个DWORD(FS:[0])所指向的链表中的。由于针对某个特定异常的处理程序可能不在这个链表的开头,因此就需要从链表中依次移除实际处理异常的那个异常处理程序之前的所有异常处理程序。 正如你在Visual C++的__except_handler3函数中看到的那样,展开是由__global_unwind2这个运行时库(RTL)函数来完成的。这个函数只是对RtlUnwind这个未公开的API进行了非常简单的封装。(现在这个API已经被公开了,但给出的信息极其简单,详细信息可以参考最新的Platform SDK文档。) __global_unwind2(void * pRegistFrame){ _RtlUnwind( pRegistFrame, &__ret_label, 0, 0 ); __ret_label:} 虽然从技术上讲RtlUnwind是一个KERNEL32函数,但它只是转发到了NTDLL.DLL中的同名函数上。图12是我为此函数写的伪代码。 图12 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函数的伪代码已经表明了这一点。 图 13是我为UnhandledExceptionFilter函数写的伪代码。这个API有点奇怪(至少在我看来是这样)。如果异常的类型是 EXCEPTION_ACCESS_VIOLATION,它就调用_BasepCheckForReadOnlyResource。虽然我没有提供这个函数的伪代码,但可以简要描述一下。如果是因为要对EXE或DLL的资源节(.rsrc)进行写操作而导致的异常,_BasepCurrentTopLevelFilter就改变出错页面正常的只读属性,以便允许进行写操作。如果是这种特殊的情况,UnhandledExceptionFilter返回EXCEPTION_CONTINUE_EXECUTION,使系统重新执行出错指令。 图13 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]; elseharderr.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, // bInheritHandles0, 0, // 创建标志、环境0, // 当前目录&statupinfo, // STARTUPINFO&pi); // 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_FILTERSetUnhandledExceptionFilter( 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的。我甚至还讲了当某个异常没有被处理时所发生的情况以及系统所做的扫尾工作。剩下的就只有异常回调过程最初是从哪里开始的这个问题了。好吧,让我们深入系统内部来看一下结构化异常处理的开始阶段吧。 图 14是我为KiUserExceptionDispatcher函数和一些相关函数写的伪代码。这个函数在NTDLL.DLL中,它是异常处理执行的起点。为了绝对准确起见,我必须指出:刚才说的并不是绝对准确。例如在Intel平台上,一个异常导致CPU将控制权转到ring 0(0特权级,即内核模式)的一个处理程序上。这个处理程序由中断描述符表(Interrupt Descriptor Table,IDT)中的一个元素定义,它是专门用来处理相应异常的。我跳过所有的内核模式代码,假设当异常发生时CPU直接将控制权转到了 KiUserExceptionDispatcher函数。 图14 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,XXXXXXXXJMP 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有关的函数非常困难,而且记住它们之间的调用关系也非常困难。为了帮助我自己记忆,我画了一个调用关系图(图15)。 现在要问:在调用ExecuteHandler之前设置EDX寄存器的值有什么用呢?这非常简单。如果ExecuteHandler在调用用户安装的异常处理程序的过程中出现了什么错误,它就把EDX指向的代码作为原始的异常处理程序。它把EDX寄存器的值压入堆栈作为原始的 EXCEPTION_REGISTRATION结构的handler域。这基本上与我在MYSEH和MYSEH2中对原始的结构化异常处理的使用情况一样。 图15 在SEH中是谁调用了谁 结论 结构化异常处理是Win32一个非常好的特性。多亏有了像Visual C++之类的编译器的支持层对它的封装,一般的程序员才能付出比较小的学习代价就能利用SEH所提供的便利。但是在操作系统层面上,事情远比Win32文档说的复杂。 不幸的是,由于人人都认为系统层面的SEH是一个非常困难的问题,因此至今这方面的资料都不多。在本文中,我已经向你指出了系统层面的SEH就是围绕着简单的回调在打转。如果你理解了回调的本质,在此基础上分层理解,系统层面的结构化异常处理也不是那么难掌握。 附录:关于prolog和epilog 美国英语中的“prolog”实际上就是“prologue”。从这个词的意思“序幕、序言”就能大致猜出它的作用。一个函数的prolog代码主要是为这个函数的执行做一些准备工作,例如设置堆栈帧、设置局部变量所使用的堆栈空间以及保存相关的寄存器等。标准的prolog代码开头一般为以下三条指令: PUSH EBPMOV EBP, ESPSUB ESP, XXX 上面的三条指令为使用EBP寄存器来访问函数的参数(正偏移)和局部变量(负偏移)做好了准备。例如按照__stdcall调用约定,调用者(caller)将被调函数(callee)的参数从右向左压入堆栈,然后用CALL指令调用这个函数。CALL指令将返回地址压入堆栈,然后流程就转到了被调函数的prolog代码。此时[ESP]中是返回地址,[ESP+4]中是函数的第一个参数。本来可以就这样使用ESP寄存器来访问参数,但由于 PUSH和POP指令会隐含修改ESP寄存器的值,这样同一个参数在不同时刻可能需要通过不同的指令形式来访问(例如,如果现在向堆栈中压入一个值的话,那访问第一个参数就需要使用[ESP+8]了)。为了解决这个问题,所以使用EBP寄存器。EBP寄存器被称为栈帧(frame)指针,它正是用于此目的。当上述prolog指令中的前两条指令执行后,就可以使用EBP来访问参数了,并且在整个函数中都不会改变此寄存器的值。在前面的例子中, [EBP+8]处就是第一个参数的值,[EBP+0Ch]处是第二个参数的值,依次类推。 大多数C/C++编译器都有“栈帧指针省略(Frame-Pointer Omission)”这个选项(在Microsoft C/C++编译器中为/Oy),它导致函数使用ESP来访问参数,从而可以空闲出一个寄存器(EBP)用于其它目的,并且由于不需要设置堆栈帧,从而会稍微提高运行速度。但是在某些情况下必须使用堆栈帧。作者在前面也提到过,Microsoft已经在其MSDN文档中指明:结构化异常处理是基于帧的异常处理。也就是说,它必须使用堆栈帧。当你查看编译器为使用SEH的函数生成的汇编代码时就会清楚这一点。无论你是否使用/Oy选项,它都设置堆栈帧。 可能有的读者在调试应用程序时偶然进入到了系统DLL(例如NTDLL.DLL)中,但是却意外地发现许多函数的prolog代码的第一条指令并不是上面所说的“PUSH EBP”,而是一条“垃圾”指令——“MOV EDI, EDI”(这条指令占两个字节)。Microsoft C/C++编译器被称为优化编译器,它怎么可能生成这么一条除了占用空间之外别无它用的指令呢?实际上,如果你比较细心的话,会发现以这条指令开头的函数的前面有5条NOP指令(它们一共占5个字节),如下图所示。 考虑一下使用JMP指令进行近跳转和远跳转分别需要几个字节?他们正好分别是2个字节和5个字节!这难道是巧合?熟悉API拦截的读者可能已经猜到了,它们是供拦截API时使用的。实际上,这是Microsoft对系统打“热补丁”(Hot Patching)时拦截API用的。在打“热补丁“时,修补程序在5条NOP指令处写入一个远跳转指令,以跳转到被修补过的代码处。而“MOV EDI, EDI”处用一个近跳转指令覆盖,它跳转到5个NOP指令所在的位置。使用“MOV EDI, EDI”而不是直接使用两个NOP指令是出于性能考虑。 第三条指令用于为局部变量保留空间,其中的XXX就是需要保留的字节数。不使用局部变量的函数没有这条指令。另外,如果局部变量比较少的话——例如2个,为了性能考虑,编译器往往会使用类似于两条“PUSH ECX”这样的指令来为局部变量保留空间。这三条指令后面一般还有几条PUSH指令用于保存函数使用的寄存器(一般是EBX、ESI和EDI)。 与prolog代码相对的就是epilog代码。与prolog类似,从它的意思“尾声、结尾”也能猜出它的作用。它主要做一些清理工作。标准的epilog代码如下: MOV ESP, EBP POP EBP RET XXX 这三条指令前面可能还有几条POP指令用于恢复在prolog代码中保存的寄存器(如果存在的话)。有了前面的分析,epilog代码不言自明。需要说明的一点是,最后的RET指令用于返回调用者,并从堆栈中弹出无用信息,XXX指定了弹出的字节数。它一般用于将参数弹出堆栈。因此从这个值就可以知道函数的参数个数(每个参数均为4字节)。 为了简化这种操作,Intel引入了ENTER和LEAVE指令。其中ENTER相当于前面所说的prolog代码的前两条指令,而LEAVE相当于上面的 epilog代码的前两条指令。但由于实现上ENTER指令比前面所说的两条指令执行速度慢,因此编译器都不使用这条指令。这样,你实际看到的情况就是:prolog代码就是前面所说的那样,但epilog代码使用了LEAVE指令。