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的清理用途代码。