SEH(structured exception handling)结构化异常处理
Windows提供的异常处理机制,与语言无关。SEH使用关键字: __try, __except, __finally, __leave。
- __try语句,定义受监控的代码模块。
- __except语句,定义的异常处理模块。
- 执行过程:
- 受监控的代码模块被执行。
- 如果没有出现异常,控制流转入__except子句之后的代码模块中。
- 否则出现异常,控制流转入__except后面的表达式中,计算表达式值,再根据这个值来做相应的处理。
- EXCEPTION_CONTINUE_EXECUTION (–1) 异常被忽略,控制流将在异常出现的点之后,继续恢复运行。
- EXCEPTION_CONTINUE_SEARCH (0) 异常不被识别,也即当前的这个__except模块不是这个异常错误所对应的正确的异常处理模块。系统将继续到上一层的try-except域中继续查找一个恰当的__except模块。
- EXCEPTION_EXECUTE_HANDLER (1) 异常已经被识别,也即当前的这个异常错误,系统已经找到了并能够确认,这个__except模块就是正确的异常处理模块。控制流将进入到__except模块中。
- __finally 语法的含义就是无论如何,此句总是会执行,常用于资源释放。_finally与__try必须配对,不能同时有__except和__finally。因为两者都必须同时和__try配对使用,否则会报编译错误。
- Windows提供了两个相关的API:
- LPEXCEPTION_POINTERS GetExceptionInformation(VOID);
1 try 2 { 3 // try block 4 } 5 except ( FilterFunction(GetExceptionInformation() ) 6 { 7 // exception handler block 8 } 9 10 typedef struct _EXCEPTION_POINTERS {11 PEXCEPTION_RECORD ExceptionRecord; 12 PCONTEXT ContextRecord; 13 } EXCEPTION_POINTERS;
ExceptionRecord记录了异常的相关信息,如错误码、标志位等。 ContextRecord记录了异常发生时,线程当时的上下文环境,主要包括寄存器的值,因此有了这些信息,__except模块便可以对异常错误进行很好的分类和恢复处理。不过特别需要注意的是,这两个函数只能是在__except后面的括号中的表达式作用域内有效,否则结果可能没有保证。
-
DWORD GetExceptionCode(VOID);
-
1 try 2 { 3 // try block 4 } 5 except ( FilterFunction(GetExceptionCode() ) 6 { 7 // exception handler block 8 }
- SEH使用 RaiseException函数来引发异常
1 VOID RaiseException(2 DWORD dwExceptionCode,3 DWORD dwExceptionFlags,4 DWORD nNumberOfArguments,5 CONST ULONG_PTR* pArguments);
C++异常处理
C++也实现了异常处理机制,即我们常见的try-catch语句
1 try 2 { 3 包含可能抛出异常的语句; 4 } 5 catch(类型名[形参名]) // 捕获特定类型的异常 6 { 7 8 } 9 catch(类型名[形参名]) // 捕获特定类型的异常 10 { 11 12 } 13 catch(...) // 三个点则表示捕获所有类型的异常 14 { 15 16 }
在有些情况下,我们捕获异常并不是为了对这个异常进行处理,而是修改异常的信息,然后将之抛出。C++专门提供了这样的操作方式,即我们可以直接使用throw关键字,后面无需跟随具体错误对象,这样可以将捕捉到的异常抛出。
1 int func(int x, int y) //定义函数 2 { 3 if(y == 0) 4 { 5 throw y; //除数为0,抛出异常 6 } 7 return x/y; //否则返回两个数的商 8 } 9 10 void main() 11 { 12 int res; 13 try //定义异常 14 { 15 res = func(2, 3); 16 cout << "The result of x/y is : " << res << endl; 17 res = func(6, 0); //出现异常,函数内部会抛出异常 18 } 19 catch(int) //捕获并处理异常 20 { 21 cerr << "error of dividing zero.\n"; 22 exit(1); //异常退出程序 23 } 24 }
使用筛选器处理异常。当发生异常时,比如内存访问违规时,CPU硬件会发现此问题,并产生一个异常(你可以把它理解为中断),然后CPU会把代码流程切换到异常处理服务例程。操作系统异常处理服务例程会查看当前进程是否处于调试状态,如果是则通知调试器发生了异常,如果不是则操作系统会查看当前线程是否安装了异常帧链(FS[0]),如果安装了SEH(try.... catch....),则调用SEH,并根据返回结果决定是否全局展开或局部展开。如果异常链中所有的SEH都没有处理此异常,而且此进程还处于调试状态,则操作系统会再次通知调试器发生异常(二次异常)。如果还没人处理,则调用操作系统的默认异常处理代码UnhandledExceptionHandler,不过操作系统允许你Hook这个函数,就是通过SetUnhandledExceptionFilter函数来设置。大部分异常通过此种方法都能捕获,不过栈溢出、覆盖的有可能捕获不到。
1 LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(2 LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter3 );
参数是一个函数指针,定义如下:
typedef LONG (*PTOP_LEVEL_EXCEPTION_FILTER)(STRUCT _EXCEPTION_POINTERS *ExceptionInfo );typedef PTOP_LEVEL_EXCEPTION_FILTER LPTOP_LEVEL_EXCEPTION_FILTER;
返回值有三种:
EXCEPTION_EXECUTE_HANDLER:表明异常处理完毕,程序可以退出。
EXCEPTION_CONTINUE_EXECUTION:忽略此异常,从异常点继续运行。如果此时再发生异常,还会调用异常处理函数。
EXCEPTION_CONTINUE_SEARCH:异常没被识别,交由上一级处理函数处理。
MiniDump文件
有了强大的SetUnhandledExceptionFilter 这个Windows API,就使得记录异常快照变得可行。只需要在通过这个API注册的异常处理函数中记录下当前上下文信息,就可以通过这些信息来分析并弥补程序上的缺陷。通常,在异常处理函数中生成MiniDump文件来供我们对异常进行分析和处理。
Microsoft提供了一个API函数,用以生成MiniDump:MiniDumpWriteDump,该API需要加载dbghelp.dll模块,并在dbghelp.h头文件里声明。
1 BOOL WINAPI MiniDumpWriteDump(2 _In_ HANDLE hProcess,3 _In_ DWORD ProcessId,4 _In_ HANDLE hFile,5 _In_ MINIDUMP_TYPE DumpType,6 _In_ PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,7 _In_ PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,8 _In_ PMINIDUMP_CALLBACK_INFORMATION CallbackParam9 );
有了MiniDump文件以及Release编译时生成的pdb文件,我们就可以通过VS或WinDbg等工具来分析和调试异常代码了。