windows系统里,为了保证系统内核的强壮和稳定,为了保证用户程序的强壮和稳定,提供了异常处理机制,来帮助程序员和系统使用人员处理异常。简单来说,当CPU执行代码时,发生异常,会把异常告知操作系统,操作系统首先会让程序自身处理这个异常,程序自身有能力(程序中注册的有异常处理函数)处理,程序就继续运行;程序自身没有能力处理(程序中没有注册异常处理函数),这个异常还没有被处理,就有操作系统来处理,就提示用户是调用调试器调试还是结束程序。如果在有调试器参与的情况下,程序出现异常,操作系统不会把异常处理权交给程序自身而是调试器,调试器可以把处理权继续转交给程序,也可以自己处理。为了完成前面讲的过程,Windwos信息提供了SEH和VEH两种处理机制。下面分别简单介绍。
一、SEH异常处理机制
SEH是windows操作系统异常处理机制,叫结构化异常处理,在程序源代码中使用__try,__except,__finally关键字来具体实现。
Windows操作系统(自Windows95起),对每个用户线程,都设立一个异常处理帧链表来处理异常事件。该链表的每个异常处理帧由两个成员组成,分别是链表上一项地址、当前异常处理器地址,组成了结构_EXCEPTION_REGISTRATION_RECORD。异常处理器是指一个处理异常的回调函数(callback function)。线程信息块(thread information block)的开始处(即FS:[0]指向的内存,FS是CPU的一个段寄存器)保存了异常处理帧链表的表头项的地址。程序执行遇到异常事件而中断时,操作系统的RtlDispatchException函数会从FS:[0]指向的链表表头依次调用每个节点包含异常处理回调函数,直到某个异常处理回调函数的返回值为0表示已经处理该异常,该线程可以恢复执行。链表最末一项是操作系统在装入线程时设置的指向kernel32!UnhandledExceptionFilter函数,该函数总是向用户显示“Application error”对话框。上述异常处理器程序及链表,是由用户程序自己安装的。链表各节点保存在程序调用栈(call stack)上。Windows异常处理机制支持嵌套异常的处理,即在执行异常处理回调函数时再次发生异常。这种情况下仍遵照普通异常处理机制,操作系统RtlDispatchException函数再入处理新出现的嵌套的异常。嵌套的异常的处理函数得到DispatcherContext参数值即为在执行时发生了新异常的异常帧的地址。
各种编程语言基于上述Windows异常处理机制,设计了各自的异常处理语句控制结构。Microsoft扩展了C语言语法,设计了结构化异常处理的try-except与try-finally语句。一个函数的所有在函数的的try-except与try-finally形成了一个基于包含(enclosing)关系的森林 (数据结构)。一个函数内如果有__try语句,则在函数的入口与结尾处,编译器插入了EH_prolog与EH_epilog代码,把函数内所有在try块中被保护的代码包了起来。 EH_prolog在调用栈上创建一个_EXCEPTION_REGISTRATION_RECORD,作为异常处理链表的新的表头,其中包含了Visual C++ 的运行时库msvcrt.dll的__except_handler4函数地址。在函数块的结尾处,EH_epilog把这项_EXCEPTION_REGISTRATION_RECORD从链表头移除,恢复其原来的表头。__except语句中的过滤表达式,由挂在链表中的异常处理回调函数MSVCR100D!__except_handler4来调用执行,返回值即为过滤表达式的求值结果。实际上,编译器实现结构化异常时,把链表每项的数据结构由2个成员扩展为5个成员,即在高地址方向追加了一个scopetable_entries类型结构体数组的指针、一个整型项表示执行点位于当前函数的哪个try块中、一个保存寄存器EBP的整数项。此后(低地址方向)紧接着是一个指向EXCEPTION_POINTERS结构的指针(前述的内在函数GetExceptionInformation即返回这个指针值)。
异常发生时,操作系统的异常处理机制的ntdll!RtlDispatchException函数会从FS:[0]指向的链表表头依次调用异常帧链表的每个节点所包含的异常处理回调函数MSVCR100D!__except_handler4,根据该回调函数的返回值来确定异常是否已经被处理,可以根据异常上下文(Exception context)恢复线程的执行。__except_handler4回调函数实际上只是调用了MSVCR100D!__except_handler4_common函数。__except_handler4_common函数是实际的workhorse,负责在当前异常帧所在的函数中查找那个try-except语句能够处理该异常(即过滤表达式的结果为1) 。如果不存在这样的try-except块,__except_handler4_common函数返回ExceptionContinueExecution(值0),由RtlDispatchException继续访问异常帧链表的下一个节点。如果找到了能处理当前异常的try-except块,__except_handler4_common函数首先调用全局展开函数_EH4_GlobalUnwind,把从异常帧链表表头所在的函数,直到能处理当前异常的函数的下一层函数,都做栈展开(unwinding);然后,对能处理当前异常的函数,从包含执行点的最内存__try语句,直到能处理异常的try-except块,调用局部展开函数_EH4_LocalUnwind;再执行能处理异常的try-except块的异常处理代码;最后,继续执行try-except之后的其他代码。全局展开,是由_EH4_GlobalUnwind调用ntdll!RtlUnwind,RtlUnwind遍历访问异常帧链表,把从表头帧到目标帧(不含)的所有异常处理回调函数用异常码(STATUS_UNWIND 即0C0000027H)、异常标志(EXCEPTION_UNWINDING即值2)调用。异常处理回调函数根据当前的异常码与异常标志,对当前异常帧所在函数,从包含了产生异常的执行点的最内层__try语句开始,直至函数内的最外层try块,依次调用try-finally的清理用途代码。这一步实际上是调用_EH4_LocalUnwind函数来完成。
局部展开,由_EH4_LocalUnwind函数实现,是从包含了产生异常的执行点的最内层__try语句开始,按着代码的包含关系向外直至目标try-except语句为止,依次调用try-finally的清理用途代码。
二、win32下的向量化异常处理
向量化异常处理(VEH)是结构化异常处理的一个扩展,它在Windows XP中被引入,为什么向量化异常要强调是win32下的呢,因为64位windows不支持这个特性。理解这个特性还是回到之前说的操作系统处理异常的顺序上面,首先会交给调试程序,然后再由用户程序处理,根据过滤表达式返回的值决定这个异常是否被处理,而这个向量化异常处理,就是将异常处理的代码添加到这个之前,它的代码会先于过滤表达式之前执行。我们知道异常是由内层向外层一层一层的查找,如果在内层已经处理完成,那么外层是永远没有机会处理的,这种情况在我们使用第三方库开发应用程序,而这个库又不提供源码,并且当发生异常时这个库只是简单的将线程终止,而我们想处理这个异常,但是由于内部处理了,外层的try根本捕获不到,这个时候就可以使用向量化异常处理了。这样我们可以编写异常处理代码先行处理并返回继续执行,这样库中就没有机会处理这个异常了。
你可以使用AddVectoredExceptionHandler()函数添加一个向量化异常处理器,VEH的缺点是它只能用在WinXP及其以后的版本,因此需要在运行时检查AddVectoredExceptionHandler()函数是否存在。
要移除先前安装的异常处理器,可以使用RemoveVectoredExceptionHandler()函数。
VEH允许查看或处理应用程序中所有的异常。为了保持后向兼容,当程序中的某些部分发生SEH异常时,系统依次调用已安装的VEH处理器,直到它找到有用的SEH处理器。
VEH的一个优点是能够链接异常处理器(chain exception handlers),因此如果有人在你之前安装了向量化异常处理器,你仍然能截获这些异常。
当你需要像调试器一样监事所有的异常时,使用VEH是很合适的。问题是你需要决定哪个异常需要处理,哪个异常需要跳过。 In program‘s code, some exceptions may be intentionally guarded by __try{}__except(){} construction, and handling such exceptions in VEH and not passing it to frame-based SEH handler, you may introduce bugs into application logics.
VEH目前没有被CrashRpt所使用。SetUnhandledExceptionFilter()更加适用,因为它是top-level SEH处理器。如果没有人处理异常,top-level SEH处理器就会被调用,并且你不用决定是否要处理这个异常。
Windows异常处理机制简介