- 虽然这个异常回调机制很好,但它并不是一个完美的解决方案。对于稍微复杂一些的应用程序来说,仅用一个 函数就能处理程序中任何地方都可能发生的异常是相当困难的。一个更实用的方案应该是有多个异常处理例程,每个例程针对程序中的一部分。实际上,操作系统提 供的正是这个功能。
- 还 记得系统用来查找异常回调函数的EXCEPTION_REGISTRATION结构吗?这个结构的第一个成员,称为prev,前面我们暂时把它忽略了。它 实际上是一个指向另外一个EXCEPTION_REGISTRATION结构的指针。这第二个EXCEPTION_REGISTRATION结构可以有一 个完全不同的处理函数。它的prev域可以指向第三个EXCEPTION_REGISTRATION结构,依次类推。简单地说,就是有一个EXCEPTION_REGISTRATION结构链表。线程信息块的第一个DWORD(在基于Intel CPU的机器上是FS:[0])指向这个链表的头部。
- 操作系统要这个EXCEPTION_REGISTRATION结构链表做 什么呢?原来,当异常发生时,系统遍历这个链表以查找一个(其异常处理程序)同意处理这个异常的EXCEPTION_REGISTRATION结构。在 MYSEH.CPP中,异常处理程序通过返回ExceptionContinueExecution表示它同意处理这个异常。异常回调函数也可以拒绝处理 这个异常。在这种情况下,系统移向链表的下一个EXCEPTION_REGISTRATION结构并询问它的异常回调函数,看它是否同意处理这个异常。下图 显示了这个过程。一旦系统找到一个处理这个异常的回调函数,它就停止遍历链表。
- 为了使代码尽量简单,我使用了编译器层面的异常处理。main函数只设置了一个 __try/__except块。在__try块内部调用了HomeGrownFrame函数。这个函数与前面的MYSEH程序非常相似。它也是在堆栈上 创建一个EXCEPTION_REGISTRATION结构,并且让FS:[0]指向此结构。在建立了新的异常处理程序之后,这个函数通过向一个NULL 指针所指向的内存处写入数据而故意引发一个错误:
- *(PDWORD)0 = 0;
- 这个异常处理回调函 数,同样被称为_except_handler,却与前面的那个截然不同。它首先打印出ExceptionRecord结构中的异常代码和标志,这个结构 的地址是作为一个指针参数被这个函数接收的。打印出异常标志的原因一会儿就清楚了。因为_except_handler函数并没有打算修复出错的代码,因 此它返回ExceptionContinueSearch。这导致操作系统继续在EXCEPTION_REGISTRATION结构链表中搜索下一个 EXCEPTION_REGISTRATION结构。接下来安装的异常回调函数是针对main函数中的__try/__except块的。 __except块简单地打印出“Caught the exception in main()”。此时我们只是简单地忽略这个异常来表明我们已经处理了它。
//================================================= // 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。这把我们带入到了展开(Unwinding)的世界中。实际上,当一个异常处理回调函数拒绝处理某个异常时,它会被再一次调用。但是这次回调并不是立即发生的。这有点复杂。我需要把异常发生时的情形好好梳理一下。
- 当 异常发生时,系统遍历EXCEPTION_REGISTRATION结构链表,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次遍历这个链 表,直到处理这个异常的结点为止。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义 为EH_UNWINDING。(EH_UNWINDING的定义在Visual C++ 运行时库源代码文件EXCEPT.INC中,但Win32 SDK中并没有与之等价的定义。)
- EH_UNWINDING表 示什么意思呢?原来,当一个异常处理回调函数被第二次调用时(带EH_UNWINDING标志),操作系统给这个函数一个最后清理的机会。什么样的清理 呢?一个绝好的例子是C++类的析构函数。当一个函数的异常处理程序拒绝处理某个异常时,通常执行流程并不会正常地从那个函数退出。现在,想像一个定义了 一个C++类的实例作为局部变量的函数。C++规范规定析构函数必须被调用。这带EH_UNWINDING标志的第二次回调就给这个函数一个机会去做一些 类似于调用析构函数和__finally块之类的清理工作。
- 在 异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程从处理异常的那个回调函数决定的地方开始继续执行。一定要记住,仅仅把指令指针设置到所 需的代码处就开始执行是不行的。流程恢复执行处的代码的堆栈指针和栈帧指针(在Intel CPU上是ESP和EBP)也必须被恢复成它们在处理这个异常的函数的栈帧上的值。因此,这个处理异常的回调函数必须负责把堆栈指针和栈帧指针恢复成它们 在包含处理这个异常的SEH代码的函数的堆栈上的值。
- 通 常,展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,就好像我们从来没有调用过这些函数一样。展开的另外一个效果就是 EXCEPTION_REGISTRATION结构链表上处理异常的那个结构之前的所有EXCEPTION_REGISTRATION结构都被移除了。这 很好理解,因为这些EXCEPTION_REGISTRATION结构通常都被创建在堆栈上。在异常被处理后,堆栈指针和栈帧指针在内存中比那些从 EXCEPTION_REGISTRATION结构链表上移除的EXCEPTION_REGISTRATION结构高。
时间: 2024-10-27 05:38:40