__asm {
// 移去我们的EXECEPTION_REGISTRATION结构 mov eax,[ESP] // 获取前一个结构 mov FS:[0], EAX // 安装前一个结构
add esp, 8 // 将我们的EXECEPTION_REGISTRATION弹出堆栈 } return 0; }
如 果你想知道我为什么把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这条指令执行时,它引发一个访问违规。系统在FS:[0]处的TIB中查找,然后发现了一个指向 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结构吗?这个结构的第一个成员,称为prev,前面我们暂时把它忽略了。它 实际上是一个指向另外一个
EXCEPTION_REGISTRATION结构的指针。这第二个EXCEPTION_REGISTRATION结构可以有一 个完全不同的处理函数。它的prev域可以指向第三个EXCEPTION_REGISTRATION结构,依次类推。 简单地说,就是有一个EXCEPTION_REGISTRATION结构链表。线程信息块的第一个DWORD(在基于Intel CPU的机器上是FS:[0])指向这个链表的头部。
操作系统要这个 EXCEPTION_REGISTRATION 结构链表做 什么呢?原来,当异常发生时,系统遍历这个链表以查找一个(其异常处理程序)同意处理这个异常的EXCEPTION_REGISTRATION结构。在 MYSEH.CPP中,异常处理程序通过返回ExceptionContinueExecution表示它同意处理
这个异常。异常回调函数也可以拒绝处理 这个异常。在这种情况下,系统移向链表的下一个EXCEPTION_REGISTRATION结构并询问它的异常回调函数,看它是否同意处理这个异常。图 4显示了这个过程。一旦系统找到一个处理这个异常的回调函数,它就停止遍历链表。
图4 查找一个处理异常的EXCEPTION_REGISTRATION结构
图 5的MYSEH2.CPP就是一个异常处理函数不处理某个异常的例子。为了使代码尽量简单,我使用了编译器层面的异常处理。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()”。此时我们只是简单地忽略这个异常来表明我们已经处理了它。 图5 MYSEH2.CPP
//================================================= // MYSEH2 - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997 // FILE: MYSEH2.CPP
// 使用命令行CL MYSEH2.CPP编译
//================================================= #define WIN32_LEAN_AND_MEAN #include
struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) {
printf( \ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags ); if ( ExceptionRecord->ExceptionFlags & 1 )
printf( \ if ( ExceptionRecord->ExceptionFlags & 2 )
printf( \
if ( ExceptionRecord->ExceptionFlags & 4 )
printf( \
if ( ExceptionRecord->ExceptionFlags & 8 ) // 注意这个标志
printf( \
if ( ExceptionRecord->ExceptionFlags & 0x10 ) // 注意这个标志
printf( \ printf( \
// 我们不想处理这个异常,让其它函数处理吧 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( \ __asm {
// 移去我们的EXECEPTION_REGISTRATION结构 mov eax,[ESP] // 获取前一个结构 mov FS:[0], EAX // 安装前一个结构
add esp, 8 // 把我们EXECEPTION_REGISTRATION结构弹出堆栈 } }
int main() { __try {
HomeGrownFrame(); }
__except( EXCEPTION_EXECUTE_HANDLER ) {
printf( \ } return 0; }
这里的关键是执行流程。 当一个异常处理程序拒绝处理某个异常时,它实际上也就拒绝决定流程最终将从何处恢复。只有处理某个异常的异常处理程序才能决定待所有异常处理代码执行完毕之后流程将从何处恢复。 这个规则的意义非常重大,虽然现在还不明显。
当 使用结构化异常处理时,如果一个函数有一个异常处理程序但它却不处理某个异常,这个函数就有可能非正常退出。例如在MYSEH2中 HomeGrownFrame函数就不处理异常。由于在链表中后面的某个异常处理程序(这里是main函数中的)处理了这个异常,因此出错指令后面的 printf就永远不会执行。从某种程度上说,使用结构化异常处理与使用setjmp和longjmp运行时库函数有些类似。