通常,展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,就好像我们从来没有调用过这些函数一样。展开的另外一个效果就是 EXCEPTION_REGISTRATION 结构链表上处理异常的那个结构之前的所有 EXCEPTION_REGISTRATION 结构都被移除了。这很好理解,因为这些 EXCEPTION_REGISTRATION 结构通常都被创建在堆栈上。在异常被处理后,堆栈指针和栈帧指针在内存中比那些从 EXCEPTION_REGISTRATION 结构链表上移除的 EXCEPTION_REGISTRATION 结构高。图六显示了我说的情况。
图六 从异常展开
帮帮我!没有人处理它!
迄今为止,我实际上一直在假设操作系统总是能在 EXCEPTION_REGISTRATION 结构链表中 的某个地方找到一个异常处理程序。如果找不到怎么办呢?实际上,这几乎不可能发生。因为操作系统暗中已经为每个线程都提供了一个默认的异常处理程序。这个默认的异常处理程序总是链表的最后一个结点,并且它总是选择处理异常。它进行的操作与其它正常的异常处理回调函数有些不同,下面我会说明。
让我们来看一下系统是在什么时候插入了这个默认的、最后一个异常处理程序。很明显它需要在线程执行的早期,在任何用户代码开始执行之前。
下面是我为 BaseProcessStart 函数写的伪代码。它是 Windows NT KERNEL32.DLL 的一个内部例程。这个函数带一个参数——线程入口点函数的地址。BaseProcessStart 运行在新进程的上下文环境中,并且从该进程的第一个线程的入口点函数开始执行。
BaseProcessStart 伪码 BaseProcessStart( PVOID lpfnEntryPoint ) { DWORD retValue DWORD currentESP; DWORD exceptionCode; currentESP = ESP; _try { NtSetInformationThread( GetCurrentThread(), ThreadQuerySetWin32StartAddress, &lpfnEntryPoint, sizeof(lpfnEntryPoint) ); retValue = lpfnEntryPoint(); ExitThread( retValue ); } _except(// 过滤器-表达式代码 exceptionCode = GetExceptionInformation(), UnhandledExceptionFilter( GetExceptionInformation() ) ) { ESP = currentESP; if ( !_BaseRunningInServerProcess ) // 常规进程 ExitProcess( exceptionCode ); else // 服务 ExitThread( exceptionCode ); } }
在这段伪码中,注意对 lpfnEntryPoint 的调用被封装在一个__try 和 __except 块中。正是此__try 块安装了默认的、异常处理程序链表上的最后一个异常处理程序。所有后来注册的异常处理程序都被安装在此链表中这个结点的前面。如果 lpfnEntryPoint 函数返回,那么表明线程一直运行到完成并且没有引发异常。这时 BaseProcessStart 调用 ExitThread 使线程退出。
另一方面,如果线程引发了一个异常但是没有异常处理程序来处理它时,该怎么办呢?这时,执行流程转到 __except 关键字后面的括号中。在 BaseProcessStart 中,这段代码调用 UnhandledExceptionFilter 这个 API,稍后我会讲到它。现在对于我们来说,重要的是 UnhandledExceptionFilter 这个API包含了默认的异常处理程序。
如果 UnhandledExceptionFilter 返回 EXCEPTION_EXECUTE_HANDLER,这时 BaseProcessStart 中的__except 块开始执行。而__except块所做的只是调用 ExitProcess 函数去终止当前进程。稍微想一下你就会理解了。常识告诉我们,如果一个进程引发了一个错误而没有异常处理程序去处理它,这个进程就会被系统终止。你在伪代码中看到的正是这些。
对于上述内容我还有一点要补充。如果引发错误的线程是作为服务来运行的,并且是基于线程的服务,那么__except 块并不调用 ExitProcess,而是调用 ExitThread。不能仅仅因为一个服务出错就终止整个服务进程。
UnhandledExceptionFilter 中的默认异常处理程序都做了什么呢?当我在一个技术讲座上问起这个问题时,响应者寥寥无几。几乎没有人知道当未处理异常发生时,到底操作系统的默认行为是什么。简单地演示一下这个默认的行为也许会让很多人豁然开朗。我运行一个故意引发错误的程序,其结果如下(如图八)。
图八 未处理异常对话框
表面上看,UnhandledExceptionFilter 显示了一个对话框告诉你发生了一个错误。这时,你被给予了一个机会要么终止出错进程,要么调试它。但是幕后发生了许多事情,我会在文章最后详细讲述它。
正如我让你看到的那样,当异常发生时,用户写的代码可以(并且通常是这样)获得机会执行。同样,在操作过程中,用户写的代码可以执行。此用户编写的代码也可能有缺陷并可能引发另一个异常。由于这个原因,异常处理回调函数也可以返回另外两个值: ExceptionNestedException 和 ExceptionCollidedUnwind。很明显,它们很重要。但这是非常复杂的问题,我并不打算在这里详细讲述它们。要想理解其基本概念真的太困难了。
编译器级的SEH
虽然我在前面偶尔也使用了__try 和__except,但迄今为止几乎我写的所有内容都是关于操作系统方面对 SEH 的实现。然而看一下我那两个使用操作系统的原始 SEH 的小程序别扭的样子,编译器对这个功能进行封装实在是非常有必要的。现在让我们来看一下 Visual C++ 是如何在操作系统对 SEH 功能实现的基础上来创建它自己的结构化异常处理支持的。
在继续往下讨论之前,记住其它编译器可以使用原始的系统 SEH 来做一些完全不同的事情这一点是非常重要的。没有谁规定编译器必须实现 Win32 SDK 文档中描述的__try/__except 模型。例如 Visual Basic 5.0 在它的运行时代码中使用了结构化异常处理,但是那里的数据结构和算法与我这里要讲的完全不同。
如果你把 Win32 SDK 文档中关于结构化异常处理方面的内容从头到尾读一遍,一定会遇到下面所谓的“基于帧”的异常处理程序模型:
__try { // 这里是被保护的代码 } __except (过滤器表达式) { // 这里是异常处理程序代码 }
简单地说,某个函数__try 块中的所有代码是由 EXCEPTION_REGISTRATION 结构来保护的,该结构建立在此函数的堆栈帧上。在函数的入口处,这个新的 EXCEPTION_REGISTRATION 结构被放在异常处理程序链表的头部。在__try 块结束后,相应的 EXCEPTION_REGISTRATION 结构从这个链表的头部被移除。正如我前面所说,异常处理程序链表的头部被保存在 FS:[0] 处。因此,如果你在调试器中单步跟踪时能看到类似下面的指令
MOV DWORD PTR FS:[00000000],ESP 或者 MOV DWORD PTR FS:[00000000],ECX
就能非常确定这段代码正在进入或退出一个__try/__except块。
既然一个__try 块对应着堆栈上的一个 EXCEPTION_REGISTRATION 结构,那么 EXCEPTION_REGISTRATION 结构中的回调函数又如何呢?使用 Win32 的术语来说,异常处理回调函数对应的是过滤器表达式(filter-expression)代码。事实上,过滤器表达式就是__except 关键字后面的小括号中的代码。就是这个过滤器表达式代码决定了后面的大括号中的代码是否执行。
由于过滤器表达式代码是你自己写的,你当然可以决定在你的代码中的某个地方是否处理某个特定的异常。它可以简单的只是一句 “EXCEPTION_EXECUTE_HANDLER”,也可以先调用一个把p计算到20,000,000位的函数,然后再返回一个值来告诉操作系统下一步做什么。随你的便。关键是你的过滤器表达式代码必须是我前面讲的有效的异常处理回调函数。
我刚才讲的虽然相当简单,但那只不过是隔着有色玻璃看世界罢了。现实是非常复杂的。首先,你的过滤器表达式代码并不是被操作系统直接调用的。事实上,各个 EXCEPTION_REGISTRATION 结构的 handler 域都指向了同一个函数。这个函数在 Visual C++ 的运行时库中,它被称为__except_handler3。正是这个__except_handler3 调用了你的过滤器表达式代码,我一会儿再接着说它。
对我前面的简单描述需要修正的另一个地方是,并不是每次进入或退出一个__try 块时就创建或撤销一个 EXCEPTION_REGISTRATION 结构。相反,在使用 SEH 的任何函数中只创建一个 EXCEPTION_REGISTRATION 结构。换句话说,你可以在一个函数中使用多个 __try/__except 块,但是在堆栈上只创建一个 EXCEPTION_REGISTRATION 结构。同样,你可以在一个函数中嵌套使用 __try 块,但 Visual C++ 仍旧只是创建一个 EXCEPTION_REGISTRATION 结构。
如果整个 EXE 或 DLL 只需要单个的异常处理程序(__except_handler3),同时,如果单个的 EXCEPTION_REGISTRATION 结构就能处理多个__try 块的话,很明显,这里面还有很多东西我们不知道。这个技巧是通过一个通常情况下看不到的表中的数据来完成的。由于本文的目的就是要深入探索结构化异常处理,那就让我们来看一看这些数据结构吧。
扩展的异常处理帧
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)的值。
_ebp 域成为扩展的 EXCEPTION_REGISTRATION 结构的一部分并非偶然。它是通过 PUSH EBP 这条指令被包含进这个结构中的,而大多数函数开头都是这条指令(通常编译器并不为使用FPO优化的函数生成标准的堆栈帧,这样其第一条指令可能不是 PUSH EBP。但是如果使用了SEH的话,那么无论你是否使用了FPO优化,编译器一定生成标准的堆栈帧)。这条指令可以使 EXCEPTION_REGISTRATION 结构中所有其它的域都可以用一个相对于栈帧指针(EBP)的负偏移来访问。例如 trylevel 域在 [EBP-04]处,scopetable 指针在[EBP-08]处,等等。(也就是说,这个结构是从[EBP-10H]处开始的。)
紧跟着扩展的 EXCEPTION_REGISTRATION 结构下面,Visual C++ 压入了另外两个值。紧跟着(即[EBP-14H]处)的一个DWORD,是为一个指向 EXCEPTION_POINTERS 结构(一个标准的Win32 结构)的指针所保留的空间。这个指针就是你调用 GetExceptionInformation 这个API时返回的指针。尽管SDK文档暗示 GetExceptionInformation 是一个标准的 Win32 API,但事实上它是一个编译器内联函数。当你调用这个函数时,Visual C++ 生成以下代码:
MOV EAX,DWORD PTR [EBP-14]
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寄存器的值)。
看起来好像我一下子给你灌输了太多的信息,我承认。在继续下去之前,让我们先暂停,来回顾一下 Visual C++ 为使用结构化异常处理的函数生成的标准异常堆栈帧,它看起来像下面这个样子:
EBP-00 _ebp EBP-04 trylevel EBP-08 scopetable数组指针 EBP-0C handler函数地址 EBP-10指向前一个EXCEPTION_REGISTRATION结构 EBP-14 GetExceptionInformation EBP-18 栈帧中的标准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函数的伪代码:
int __except_handler3( struct _EXCEPTION_RECORD * pExceptionRecord, struct EXCEPTION_REGISTRATION * pRegistrationFrame, struct _CONTEXT *pContextRecord, void * pDispatcherContext ) { LONG filterFuncRet; LONG trylevel; EXCEPTION_POINTERS exceptPtrs; PSCOPETABLE pScopeTable; CLD // 将方向标志复位(不测试任何条件!) // 如果没有设置EXCEPTION_UNWINDING标志或EXCEPTION_EXIT_UNWIND标志 // 表明这是第一次调用这个处理程序(也就是说,并非处于异常展开阶段) if ( ! (pExceptionRecord->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) ) { // 在堆栈上创建一个EXCEPTION_POINTERS结构 exceptPtrs.ExceptionRecord = pExceptionRecord; exceptPtrs.ContextRecord = pContextRecord; // 把前面定义的EXCEPTION_POINTERS结构的地址放在比 // establisher栈帧低4个字节的位置上。参考前面我讲 // 的编译器为GetExceptionInformation生成的汇编代 // 码*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs; // 获取初始的“trylevel”值 trylevel = pRegistrationFrame->trylevel; // 获取指向scopetable数组的指针 scopeTable = pRegistrationFrame->scopetable; search_for_handler: if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE ) { if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter ) { PUSH EBP // 保存这个栈帧指针 // !!!非常重要!!!切换回原来的EBP。正是这个操作才使得 // 栈帧上的所有局部变量能够在异常发生后仍然保持它的值不变。 EBP = &pRegistrationFrame->_ebp; // 调用过滤器函数 filterFuncRet = scopetable[trylevel].lpfnFilter(); POP EBP // 恢复异常处理程序的栈帧指针 if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH ) { if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION return ExceptionContinueExecution; // 如果能够执行到这里,说明返回值为EXCEPTION_EXECUTE_HANDLER scopetable = pRegistrationFrame->scopetable; // 让操作系统清理已经注册的栈帧,这会使本函数被递归调用 __global_unwind2( pRegistrationFrame ); // 一旦执行到这里,除最后一个栈帧外,所有的栈帧已经 // 被清理完毕,流程要从最后一个栈帧继续执行 EBP = &pRegistrationFrame->_ebp; __local_unwind2( pRegistrationFrame, trylevel ); // NLG = "non-local-goto" (setjmp/longjmp stuff) __NLG_Notify( 1 ); // EAX = scopetable->lpfnHandler // 把当前的trylevel设置成当找到一个异常处理程序时 // SCOPETABLE中当前正在被使用的那一个元素的内容 pRegistrationFrame->trylevel = scopetable->previousTryLevel; // 调用__except {}块,这个调用并不会返回 pRegistrationFrame->scopetable[trylevel].lpfnHandler(); } } scopeTable = pRegistrationFrame->scopetable; trylevel = scopeTable->previousTryLevel; goto search_for_handler; } else // trylevel == TRYLEVEL_NONE { return ExceptionContinueSearch; } } else // 设置了EXCEPTION_UNWINDING标志或EXCEPTION_EXIT_UNWIND标志 { PUSH EBP // 保存EBP EBP = &pRegistrationFrame->_ebp; // 为调用__local_unwind2设置EBP __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE ) POP EBP // 恢复EBP return ExceptionContinueSearch; } }
虽然__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、过滤器表达式以及展开等等之类的词搞得晕头转向的话,那和我最初的感觉一样。但是编译器层面的结构化异常处理方面的知识并不适合一点一点的学。除非你从整体上理解它,否则有很多内容单独看并没有什么意义。当面对大堆的理论时,我最自然的做法就是写一些应用我学到的理论方面的程序。如果它能够按照预料的那样工作,我就知道我的理解(通常)是正确的。
下面是ShowSEHFrame.EXE的源代码。它使用__try/__except块设置了好几个 Visual C++ SEH 帧。然后它显示每一个帧以及Visual C++为每个帧创建的scopetable的相关信息。这个程序本身并不生成也不依赖任何异常。相反,我使用了多个__try块以强制Visual C++生成多个 EXCEPTION_REGISTRATION 帧以及相应的 scopetable。
//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: %08X Handler: %08X Prev: %08X Scopetable: %08X/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: %08X " "filter: %08X __except: %08X/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: %08X/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。
图十一是 ShowSEHFrames 的运行结果。首先检查以“Frame:”开头的每一行,你会发现它们显示的异常处理帧在堆栈上的地址呈递增趋势,并且在前三个帧中,它们的异常处理程序的地址是一样的(都是004012A8)。再看输出的开始部分,你会发现这个004012A8不是别的,它正是 Visual C++运行时库函数__except_handler3的地址。这证明了我前面所说的单个回调函数处理所有异常这一点。
图十一 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中的同名函数上。下面是我为此函数写的伪代码。
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 函数写的伪代码。这个API有点奇怪(至少在我看来是这样)。如果异常的类型是 EXCEPTION_ACCESS_VIOLATION,它就调用_BasepCheckForReadOnlyResource。虽然我没有提供这个函数的伪代码,但可以简要描述一下。如果是因为要对 EXE 或 DLL 的资源节(.rsrc)进行写操作而导致的异常,_BasepCurrentTopLevelFilter 就改变出错页面正常的只读属性,以便允许进行写操作。如果是这种特殊的情况,UnhandledExceptionFilter 返回 EXCEPTION_CONTINUE_EXECUTION,使系统重新执行出错指令。
转载于:https://blog.51cto.com/masefee/814027