在 Win32 操作系统提供的所有功能中,使用最广泛但最缺乏文档描述的也许就是结构化异常处理了(SEH),当你考虑 Win32 结构化异常处理时,你也许会想到诸如 _try,_finally 以及 _except 这些术语。你能在任何有关 Win32 的书中发现对 SEH 很好的描述(即使是 remedial)。即便是 Win32 SDK 也具备有相当完整的使用 _try,_finally 和 _except 进行结构化异常处理的概述。
有了这些文档,那为何还说 SEH 缺乏文档呢?其实,Win32 结构化异常处理是操作系统提供的一个服务。你能找到的关于 SEH 的所有文档都是描述特定编译器的运行时库,这个运行库对操作系统实现进行包装。_try,_finally 和 _except 这些关键字没有任何神奇的地方。微软的操作系统及其编译器系列定义这些关键字和用法。其他的编译器提供商则只是沿用这些语义。虽然借助编译器层的 SEH 可以挽回一些原始操作系统级 SEH 处理不良口碑,但在大众眼里对原始操作系统 SEH 细节的处理感觉依旧。
我收到人们大量的e-mail,都是想要实现编译器级的 SEH 处理,又无法找到操作系统功能提供的相关文档。通常我都是建议参考 Visual C++ 或者 Borland C++ 运行库源代码。唉,出于一些未知的原因,编译器级的 SEH 似乎是一个大的秘密,微软和 Borland 都不提供其对 SEH 支持的核心层源代码。
在本文中,我将一层一层对 SEH 进行解剖,以便展现其最基本的概念。我打算通过代码产生和运行时库支持将操作系统提供的功能和编译器提供的功能分开。当我深入代码考察关键的操作系统例程时,我将使用 Intel 平台上的 Windows NT4.0 作为基础。但我将要描述的大多数内容同样适用于其它处理器上运行的应用。
我打算避免涉及到真正的 C++ 异常处理,它们使用 catch(),而不是 _except。其实,真正的 C++ 异常处理实现非常类似于本文中描述的内容。但是 C++ 异常处理有一些额外的复杂性会影响我想要涉及的概念。
通过深入研究晦涩的 .H 和 .INC 文件来归纳 Win32 SEH 构成,我发现有一个信息源之一就是 IBM OS/2 头文件(尤其是 BSEXCPT.H)。为此你不要觉得大惊小怪。。此处描述的 SEH 机制在其源头被定义时,微软仍然开发 OS/2 平台(译注: OS/2 平台起初是IBM 和 微软共同研发的,后来由于种种原因两个公司没有再继续下去)。所以你会发现Win32 下的 SEH 和 OS/2 下的 SEH 极其相似。
SEH 浅析
从整体来看,SEH 的可谓不可一世,绝对压倒一切,我将从细微之处开始,用我自己的方式一层一层研究。如果你是一张白纸,以前从没接触过结构化异常处理,那就最好不过了。如果你以前使用过 SEH。那就尝试清理你头脑中的 _try,GetExceptionCode 和 EXCEPTION_EXECUTE_HANDLER 等诸如此类的词,权当自己是个新手。做一个深呼吸,准备好了吗?好,我们开始。
想象一下,我告诉你某个线程出错了,操作系统给你一个机会通知了这个线程错误,或者再具体一点,当线程出错后,操作系统调用某个用户定义的回调函数。这个回调函数可以所任何它想做的事情。例如,它可以修复任何原因导致的错误,或者播放一个 .wav 文件。不管回调函数做什么,其最后总是返回一个值,这个值告诉系统下一步做什么。(这里描述的情况不一定完全一样,但足够接近。)
假定当你的代码出现了混乱,你不得不回来,想看看回调函数是什么样子的?换句话说,你想知道什么样的异常信息呢?其实这无关紧要,因为 Win32 已经帮你决定了。一个异常回调函数就象下面这样:
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
);
该原型出自标准的 Win32 头文件 EXCPT.H,初看就有那么一点不同凡响。如果你慢慢研究,其实并没有那么糟。例如,忽略返回类型(EXCEPTION_DISPOSITION)。基本上你看到的就是一个叫做 _except_handler 的函数,这个函数带有四个参数。
第一个参数是指向 EXCEPTION_RECORD 结构指针,该结构在 WINNT.H 中定义如下:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
ExceptionCode 参数是由操作系统赋值给异常的一个数。你可以在 WINNT.H 文件中搜一下“STATUS_”开始的 #defines 内容便可以得到一系列不同的异常编码。例如 STATUS_ACCESS_VIOLATION 是大家再熟悉不过的异常编码了,其值是 0xC0000005。更复杂的异常编码可以从 Windows NT DDK 的 NTSTATUS.H 文件中找到。EXCEPTION_RECORD 结构中的第四个元素是异常发生的地址。剩下的 EXCEPTION_RECORD 域现在可以忽略,不用管它。
_except_handler 回调函数的第二个参数是指向建立者框架(establisher frame)结构的指针,在 SEH 中它是一个至关重要的参数,但现在可以不用关心它。
_except_handler 回调函数的第三个参数是 CONTEXT 结构的指针。CONTEXT 结构在 WINNT.H 中定义,它表示特定线程异常发生时寄存器的值:
typedef struct _CONTEXT
{
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
} CONTEXT;