结构化异常处理
结构化异常处理,是Windows操作系统上,Microsoft对C/C++程序语言做的语法扩展,用于处理异常事件的程序控制结构。
异常事件是打断程序正常执行流程的不在期望之中的硬件、软件事件。硬件异常是CPU抛出的如“除0”、数值溢出等;软件异常是操作系统与程序通过RaiseException语句抛出的异常。
Microsoft扩展了C语言的语法,用 try-except与try-finally语句来处理异常。[1]异常处理程序可以释放已经获取的资源、显示出错信息与程序内部状态供调试、从错误中恢复、尝试重新执行出错的代码或者关闭程序等等。
一个__try语句不能既有__except,又有__finally。但try-except与try-finally语句可以嵌套使用。
try-except
__try
{
// 受保护执行的代码
}
__except ( 过滤表达式 )
{
// 异常处理代码
}
首先,__try复合语句中的受保护的代码被执行。如果没有异常发生,则继续执行__except复合语句之后的代码。如果__try复合语句中的受保护执行的代码发生了异常,或受保护执行的代码调用的函数内部发生了异常并要求调用者来处理该异常,__except语句的过滤表达式(filter expression)被求值,根据其结果来决定如何处理异常:
- EXCEPTION_CONTINUE_EXECUTION (–1) : 导致异常的问题已经解决,在异常出现的现场重新执行操作。
- EXCEPTION_CONTINUE_SEARCH (0) :当前__except语句不能处理该异常,通知操作系统继续搜寻该线程其他的异常处理程序。
- EXCEPTION_EXECUTE_HANDLER (1):当前__except语句识别该异常,通过执行__except的复合语句来处理该异常。然后执行__except复合语句之后的代码。
内在函数GetExceptionCode返回一个32位整型值,表示异常的类型。内在函数GetExceptionInformation返回异常的详细信息及现场信息(如CPU寄存器的值) 。这两个函数可用于异常表达式中来判断是否处理该异常。这里说的内在函数(intrinsic function),是指编译器提供内联(inline)实现的函数。
Windows操作系统的应用程序的main()函数受到结构化异常的try-except语句保护,因此程序的未被处理的异常都会被捕获。这是因为,Windows操作系统在加载用户进程时,Kernel32.dll中的BaseProcessStart函数在__try块中调用了用户进程的入口函数mainCRTStartup,因此用户程序的所有异常都会被捕获、得到处理。
可以使用C++运行时库中的_set_se_translator()函数把结构化异常转为抛出一个C++对象的C++异常。此外,C++异常的catch(...)语句也能直接捕捉结构化异常。
可以使用操作系统运行时库kernel32中的SetUnhandledExceptionFilter()函数设置顶层未处理异常过滤器(top-level unhandled exception filter),捕获进程的各个线程中一切未被处理的结构化异常。该函数一般用于从特定的错误中恢复,如无效的调用栈(invalid stack)。
try-finally语句
__try {
// 受保护执行的代码
}
__finally {
// 清理用途的代码
}
__try复合语句中受保护的代码以任何方式执行结束后,不论__try复合语句是因为出现异常而非正常结束,还是没有出现异常而正常结束,__finally复合语句中的代码都会被执行。
如果__try复合语句中受保护的代码的执行没有出现异常,包括用goto语句或longjump系统函数跳出__try复合语句等情形,这时将执行__finally复合语句,然后执行try-finally语句之后的其他代码。
在__finally复合语句中使用内在函数AbnormalTermination判断是正常结束还是非正常结束__try复合语句。
执行顺序
如果__try复合语句中受保护的代码执行中出现了异常,首先按函数调用顺序从新向旧搜索所有包含了出现异常的执行点的try-except语句,执行每个except的过滤函数,直到某个try-except语句的过滤函数结果值为EXCEPTION_EXECUTE_HANDLER。这时,再从包含异常出现的执行点的最内层try-finally语句开始由内向外执行每个finally块中的代码,直至回退到那个过滤函数结果值为EXCEPTION_EXECUTE_HANDLER的try-except语句执行其except块,最后执行该try-except语句后面的其他代码。
__leave关键字用在 try-finally 语句的受保护节(try块)中,其效果是跳转到受保护节的结尾处。 与goto语句跳出受保护节不同,__leave语句更为有效,因为它不会导致堆栈展开。
例子
#include <stdio.h>
#include <windows.h> // for EXCEPTION_ACCESS_VIOLATION
#include <excpt.h>
int filter(unsigned int code, struct _EXCEPTION_POINTERS *ep)
{
printf("在異常表達式中\n");
if (code == EXCEPTION_ACCESS_VIOLATION) {
printf("接受處理訪問違例異常\n");
return EXCEPTION_EXECUTE_HANDLER;
}
else {
printf("其他異常都不處理\n");
return EXCEPTION_CONTINUE_SEARCH;
};
}
int main()
{
int* p = 0x00000000; // pointer to NULL
printf("開始主程序\n");
__try{
printf("進入外層的try\n");
__try{
printf("進入內層的try\n");
int *p=0; // 空指針
*p = 13; // 導致訪問衝突異常
}__finally{
printf("在finally內部。");
printf(AbnormalTermination() ? "非正常終止\n" : "正常終止\n");
}
}__except(filter(GetExceptionCode(), GetExceptionInformation())){
printf("在except內部\n");
}
printf("主函數結束\n");
}
輸出結果為:
開始主程序 進入外層的try 進入內層的try 在異常表達式中 接受處理訪問違例異常 在finally內部。非正常終止 在except內部 主函數結束
与Windows异常处理机制的关系
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) 。[2]如果不存在这样的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的清理用途代码。
SAFESEH
为了防止篡改异常处理函数的地址,自Windows XP SP2起引入了检查异常处理函数的地址是否有效。但这仅用于x86。而x64或ARM已经把所有异常处理函数放在了PDATA节中;该节叫做“.pdata”或者Exception Directory,用于存放处理异常的信息。[3]
链接器选项/SAFESEH[:NO]
用于设置是否需要开启。
如果/SAFESEH
被使用,链接器生成的可执行映像文件(image)包含它的安全的异常处理函数的清单。
如果/SAFESEH
未使用,链接器将检查如果所有的模块都兼容于安全的异常处理函数特性,则生成的可执行映像文件(image)包含它的安全的异常处理函数的清单;如果有模块不兼容于安全的异常处理函数特性,则生成的可执行映像文件(image)不包含安全的异常处理函数的清单。
如果/SAFESEH:NO
被使用,链接器生成的可执行映像文件(image)不包含安全的异常处理函数的清单。
相关的Windows API函数
函数 | 描述 |
---|---|
ExRaiseStatus | 用指定状态代码触发异常 |
ExRaiseAccessViolation | 触发STATUS_ACCESS_VIOLATION异常 |
ExRaiseDatatypeMisalignment | 触发STATUS_DATATYPE_MISALIGNMENT异常 |
Visual C++编译选项
Visual C++编译器的编译选项/EH设置异常处理模型 (Exception Handling Model):
- a 捕捉异步(asynchronous或structured)与同步(synchronous或C++)异常;C++的catch(...)语句也可捕捉异步异常。不论同步异常还是异步异常,所有局部变量对象都被自动析构。 /clr隐式包含了/EHa 。
- s 只捕捉同步(synchronous或C++)异常,并假定使用extern "C" 声明的函数可能抛出异常。 C++的catch(...)语句不捕捉异步异常。异步异常不会导致C++局部对象被自动析构。
- c
- 对于/EHsc, 只捕捉同步(synchronous或C++)异常,并假定使用extern "C" 声明的函数不会抛出C++异常。
- 对于/EHca, 等价于/EHa.
- r 编译器对所有noexcept函数总是产生运行时终止检查代码。
- 如果未指定/EH,则编译器将同时捕获异步结构化异常和 C++ 异常,但不会销毁由于异步异常超出范围的 C++ 对象。
参考文献
外部链接
- Igor Skochinsky. . OpenRCE. Monday, March 6, 2006 12:02.38 CST [2009-11-17]. (原始内容存档于2010-04-16).
- Microsoft Corp. . MSDN Library. 2009-11-12 [2009-11-17]. (原始内容存档于2009-12-08).
- Matt Pietrek. . MSJ. Jan 1997, 12 (1) [2013-06-30]. (原始内容存档于2015-09-29). Note that the examples given there do not work as-is on modern Windows systems (post XP SP2) due to the changes Microsoft made to address the security issues present in the early SEH design. The examples still work on later versions of Windows if compiled with
/link /safeseh:no
. - . Yasm manual. [2017-02-14]. (原始内容存档于2017-01-07).
- US patent 7,480,919 - Safe exceptions
- Johannes Passing. . May 20, 2008 [2017-02-14]. (原始内容存档于2017-02-15). Covers the obscure details needed to get low-level SEH (and particularly SafeSEH) code to work on more modern Windows.
- Igor Skochinsky. . OpenRCE. Monday, March 6, 2006 12:02.38 CST [2009-11-17]. (原始内容存档于2010-04-16).
- Matt Miller. . Technet. 2 Feb 2009 [2017-02-14]. (原始内容存档于2014-07-01).
- Stéfan Le Berre, Damien Cauquil. (PDF). Sysdream. 22 Dec 2009. (原始内容 (PDF)存档于2012-09-07).
- Joshua J. Drake. . 10 Jan 2012 [2017-02-14]. (原始内容存档于2017-01-09). An article explaining why Windows 7 SP1 ignores SafeSEH for some older binaries, while Windows XP SP3 honors it.
- MSDN:Structured Exception Handling Functions (页面存档备份,存于)