上一篇文章阿愚对结构化异常处理(Structured Exception Handling,SEH)有了初步的认识,而且也知道了SEH是__try,__except,__finally,__leave异常模型机制和try,catch,throw方式的C++异常模型的奠基石。 为了更进一步认识SEH机制,更深刻的理解SEH与__try,__except,__finally,__leave异常模型机制的区别。本篇文章特别对狭义上的SEH进行一些极为细致的讲解。SEH设计思路 SEH机制大致被设计成这样一种工作流程:用户应用程序对一些可能出现异常错误的代码模块,创建一个相对应的监控函数(也即回调函数),并向操作系统注册;这样应用程序在执行过程中,如果什么异常也没出现的话,那么程序将按正常的执行流顺序来完成相对应的工作任务;否则,如果受监控的代码模块中,在运行时出现一个预知或未预知的异常错误,那么操作系统将捕获住这个异常,并暂停程序的执行过程(实际上是出现异常的工作线程被暂停了),然后,操作系统也收集一些有关异常的信息(例如,异常出现的地点,线程的工作环境,异常的错误种类,以及其它一些特殊的字段等),接着,操作系统根据先前应用程序注册的监控性质的回调函数,来查询当前模块所对应的监控函数,找到之后,便立即来回调它,并且传递一些必要的异常的信息作为监控函数的参数。此时,用户注册的监控函数便可以根据异常错误的种类和严重程度来进行分别处理,例如终止程序,或者试图恢复错误后,再使程序正常运行。 细心的朋友们现在可能想到,用户应用程序如何来向操作系统注册一系列的监控函数呢?其实SEH设计的巧妙之处就在与此,它这里有两个关键之处。其一,就是每个线程为一个完全独立的注册主体,线程间互不干扰,也即每个线程所注册的所有监控回调函数会连成一个链表,并且链表头被保存在与线程本地存储数据相关的区域(也即FS数据段区域,这是Windows操作系统的设计范畴,FS段中的数据一般都是一些线程相关的本地数据信息,例如FS:[0]就是保存监控回调函数数据结构体的链表头。有关线程相关的本地数据,这里不再详细赘述,感兴趣的朋友可以参考其它更为详细地资料);其二,那就是每个存储监控回调函数指针的数据结构体,实际上它们一般并不是被存储在堆(Heap)中,而是被存储在栈(Stack)中。大家还记得在《第9集 C++的异常对象如何传送》中,有关“函数调用栈”的布局,呵呵!那只是比较理想化的栈布局,实际上,无论是C++还是C程序中,如果函数模块中,存在异常处理机制的情况下,那么栈布局都会略有些变化,会变得更为复杂一些,因为在栈中,需要插入一些“存储监控回调函数指针的数据结构体”数据信息。例如典型的带SEH机制的栈布局如下图所示。 上图中,注意其中绿线部分所连成链表数据结构,这就是用户应用程序向操作系统注册的一系列的监控函数。如果某个函数中声明了异常处理机制,那么在函数帧栈中将分配一个数据结构体(EXCEPTION_REGISTRATION),这个数据结构体有点类似与局部变量的性质,它包含两个字段,其中一个是指向监控函数的指针(handler function address);另一个就是链表指针(previous EXCEPTION_REGISTRATION)。特别需要注意的是,并不是每个函数帧栈中都有EXCEPTION_REGISTRATION数据结构。另外链表头指针被保存到FS:[0]中,这样无论是操作系统,还是应用程序都能够很好操纵这个链表数据体变量。EXCEPTION_REGISTRATION的定义如下:typedef struct _EXCEPTION_REGISTRATION{struct _EXCEPTION_REGISTRATION *prev;DWORD handler;}EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION;通过一个简单例子,来理解SEH机制 也许上面的论述过于抽象化和理论化了,还是看一个简单的例子吧!这样也很容易来理解SEH的工作机制原来是那么的简单。示例代码如下: //seh.c#include <windows.h>#include <stdio.h>#include <stdlib.h> typedef struct _EXCEPTION_REGISTRATION{struct _EXCEPTION_REGISTRATION *prev;DWORD handler;}EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION; // 异常监控函数EXCEPTION_DISPOSITION myHandler(EXCEPTION_RECORD *ExcRecord,void * EstablisherFrame, CONTEXT *ContextRecord,void * DispatcherContext){printf("进入到异常处理模块中\n");printf("不进一步处理异常,程序直接终止退出\n");abort();return ExceptionContinueExecution;} int main(){DWORD prev;EXCEPTION_REGISTRATION reg, *preg;// 建立异常结构帧(EXCEPTION_REGISTRATION)reg.handler = (DWORD)myHandler;// 把异常结构帧插入到链表中__asm{mov eax, fs:[0]mov prev, eax}reg.prev = (EXCEPTION_REGISTRATION*) prev;// 注册监控函数preg = ®__asm{mov eax, pregmov fs:[0], eax} {int* p;p = 0; // 下面的语句被执行,将导致一个异常*p = 45;} printf("这里将不会被执行到.\n");return 0;} 上面的程序运行结果如下: 通过上面的演示的简单例程,现在应该非常清楚了Windows操作系统提供的SEH机制的基本原理和控制流转移的具体过程。另外,这里分别详细介绍一下exception_handler回调函数的各个参数的涵义。其中第一个参数为EXCEPTION_RECORD类型,它记录了一些与异常相关的信息。它的定义如下:typedef struct _EXCEPTION_RECORD {DWORD ExceptionCode;DWORD ExceptionFlags;struct _EXCEPTION_RECORD *ExceptionRecord;PVOID ExceptionAddress;DWORD NumberParameters;UINT_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];} EXCEPTION_RECORD; 第二个参数为PEXCEPTION_REGISTRATION类型,既当前的异常帧指针。第三个参数为指向CONTEXT数据结构的指针,CONTEXT数据结构体中记录了异常发生时,线程当时的上下文环境,主要包括寄存器的值,这一点有点类似于setjmp函数的作用。第四个参数DispatcherContext,它也是一个指针,表示调度的上下文环境,这个参数一般不被用到。最后再来看一看exception_handler回调函数的返回值有何意义?它基本上有两种返回值,一种就是返回ExceptionContinueExecution,表示异常已经被恢复了,程序可以正常继续执行。另一种就是ExceptionContinueSearch,它表示当前的异常回调处理函数不能有效处理这个异常错误,系统将会根据EXCEPTION_REGISTRATION数据链表,继续查找下一个异常处理的回调函数。上面的例程的详细分析如下图所示: 来一个稍微复杂一点例子,来更深入理解SEH机制 现在,相信大家已经对SEH机制,既有了非常理性的理解,也有非常感性的认识。实际上,从用户角度上来分析,SEH机制确是比较简单。它首先是用户注册一系列的异常回调函数(也即监控函数),操作系统为每个线程维护一个这样的链表,每当程序中出现异常的时候,操作系统便获得控制权,并纪录一些与异常相关的信息,接着系统便依次搜索上面的链表,来查找并调用相应的异常回调函数。 说到这里,也许朋友们有点疑惑了?上一篇文章中讲述到,“无论是__try,__except,__finally,__leave异常模型机制,或是try,catch,throw方式的C++异常模型,它们都是在SEH基础上来实现的”。但是从这里看来,好像上面描述的SEH机制与try,catch,throw方式的C++异常模型不太相关。是的,也许表面上看起来区别是比较大的,但是SEH机制,它的的确确是上面讲到的其它两种异常处理模型的基础。这一点,在深入分析C++异常模型的实现时,会再做详细的叙述。这里为了更深入理解SEH机制,主人公阿愚设计了一个稍微复杂一点例子。它仍然只有SEH机制,没有__try,__except,__finally,__leave异常模型的任何影子,但是它与真实的__try,__except,__finally,__leave异常模型的实现却有几分相似之处。 // seh.c#include <windows.h>#include <stdio.h> typedef struct _EXCEPTION_REGISTRATION{struct _EXCEPTION_REGISTRATION *prev;DWORD handler;}EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION; #define SEH_PROLOGUE(pFunc_exception) \{ \DWORD pFunc = (DWORD)pFunc_exception; \_asm mov eax, FS:[0] \_asm push pFunc \_asm push eax \_asm mov FS:[0], esp \} #define SEH_EPILOGUE() \{ \_asm pop FS:[0] \_asm pop eax \} void printfErrorMsg(int ex_code){char msg[20]; memset(msg, 0, sizeof(msg));switch (ex_code){case EXCEPTION_ACCESS_VIOLATION :strcpy(msg, "存储保护异常");break;case EXCEPTION_ARRAY_BOUNDS_EXCEEDED :strcpy(msg, "数组越界异常");break;case EXCEPTION_BREAKPOINT :strcpy(msg, "断点异常");break;case EXCEPTION_FLT_DIVIDE_BY_ZERO :case EXCEPTION_INT_DIVIDE_BY_ZERO :strcpy(msg, "被0除异常");break;default :strcpy(msg, "其它异常");} printf("\n");printf("%s,错误代码为:0x%x\n", msg, ex_code);}EXCEPTION_DISPOSITION my_exception_Handler(EXCEPTION_RECORD *ExcRecord,void * EstablisherFrame, CONTEXT *ContextRecord,void * DispatcherContext){int _ebp; printfErrorMsg(ExcRecord->ExceptionCode);printf("跳过出现异常函数,返回到上层函数中继续执行\n");printf("\n"); _ebp = ContextRecord->Ebp;_asm{// 恢复上一个异常帧mov eax, EstablisherFramemov eax, [eax]mov fs:[0], eax // 返回到上一层的调用函数mov esp, _ebppop ebpmov eax, -1ret}// 下面将绝对不会被执行到exit(0);return ExceptionContinueExecution; } EXCEPTION_DISPOSITION my_RaiseException_Handler(EXCEPTION_RECORD *ExcRecord,void * EstablisherFrame, CONTEXT *ContextRecord,void * DispatcherContext){int _ebp; printfErrorMsg(ExcRecord->ExceptionCode);printf("跳过出现异常函数,返回到上层函数中继续执行\n");printf("\n"); _ebp = ContextRecord->Ebp;_asm{// 恢复上一个异常帧mov eax, EstablisherFramemov eax, [eax]mov fs:[0], eax // 返回到上一层的调用函数mov esp, _ebppop ebpmov esp, ebppop ebpmov eax, -1ret}// 下面将绝对不会被执行到exit(0);return ExceptionContinueExecution; } void test1(){SEH_PROLOGUE(my_exception_Handler); {int zero;int j;zero = 0; // 下面的语句被执行,将导致一个异常j = 10 / zero; printf("在test1()函数中,这里将不会被执行到.j=%d\n", j);} SEH_EPILOGUE();} void test2(){SEH_PROLOGUE(my_exception_Handler);{int* p;p = 0; printf("在test2()函数中,调用test1()函数之前\n");test1();printf("在test2()函数中,调用test1()函数之后\n");printf("\n"); // 下面的语句被执行,将导致一个异常*p = 45; printf("在test2()函数中,这里将不会被执行到\n");} SEH_EPILOGUE();} void test3(){SEH_PROLOGUE(my_RaiseException_Handler); {// 下面的语句被执行,将导致一个异常RaiseException(0x999, 0x888, 0, 0); printf("在test3()函数中,这里将不会被执行到\n");} SEH_EPILOGUE();} int main(){printf("在main()函数中,调用test1()函数之前\n");test1();printf("在main()函数中,调用test1()函数之后\n"); printf("\n"); printf("在main()函数中,调用test2()函数之前\n");test2();printf("在main()函数中,调用test2()函数之后\n"); printf("\n"); printf("在main()函数中,调用test3()函数之前\n");test3();printf("在main()函数中,调用test3()函数之后\n"); return 0;} 上面的程序运行结果如下:在main()函数中,调用test1()函数之前 被0除异常,错误代码为:0xc0000094跳过出现异常函数,返回到上层函数中继续执行 在main()函数中,调用test1()函数之后 在main()函数中,调用test2()函数之前在test2()函数中,调用test1()函数之前 被0除异常,错误代码为:0xc0000094跳过出现异常函数,返回到上层函数中继续执行 在test2()函数中,调用test1()函数之后 存储保护异常,错误代码为:0xc0000005跳过出现异常函数,返回到上层函数中继续执行 在main()函数中,调用test2()函数之后 在main()函数中,调用test3()函数之前 其它异常,错误代码为:0x999跳过出现异常函数,返回到上层函数中继续执行 在main()函数中,调用test3()函数之后Press any key to continue总结 本文所讲到的异常处理机制,它就是狭义上的SEH,虽然它很简单,但是它是Windows系列操作系统平台上其它所有异常处理模型实现的奠基石。有了它就有了基本的物质保障,另外,通常一般所说的SEH,它都是指在本篇文章中所阐述的狭义上的SEH机制基础之上,实现的__try,__except,__finally,__leave异常模型,因此从下一篇文章中,开始全面介绍__try,__except,__finally,__leave异常模型,实际上,它也即广义上的SEH。此后所有的文章内容中,如没有特别注明,SEH机制都表示__try,__except,__finally,__leave异常模型,这也是为了与try,catch,throw方式的C++异常模型相区分开。 朋友们!有点疲劳了吧!可千万不要放弃,继续到下一篇的文章中,可要知道,__try,__except,__finally,__leave异常模型,它可以说是最优先的异常处理模型之一,甚至比C++的异常模型还好,功能还强大!即便是JAVA的异常处理模型也都从它这里继承了许多优点,所以不要错过呦,Let’s go!

评论