作者:shenzi
链接:http://blog.csdn.net/shenzi
使用SEH,并不意味着可以完全忽略代码中可能出现的错误,但是我们可以将软件主要功能编写和软件异常情况处理这两个任务分离开。这样就可以先集中注意力完成手头上的工作,稍后再去处理软件可能会遇到的各种错误情况。
SEH实际上包含两方面的功能:终止处理(termination handling)和异常处理(exception handling)。
1.终止处理
终止处理程序确保不管一个代码块(被保护代码(the guarded body))是如何退出的,另一个代码块(终止处理程序)总能被调用和执行。终止处理的语法(当使用Microsoft Visual C++编译器时)如下所示:
__try {
// Guarded body
被保护代码
...
}
__finally {
// Termination handler
终止处理程序
...
}
在这段代码中,操作系统和编译器的协同工作保证了不管被保护代码部分是如何退出的——无论我们在被保护代码中使用了return,还是goto,又或者longjump语句(除非调用ExitProcess
, ExitThread
, TerminateProcess
, 或TerminateThread
来终止进程或线程)——终止处理程序都会被调用,即
__finally
代码块都能执行。
一条好的经验法则是,不要在终止处理程序里包含让try块提前退出的语句,这意味着从try块和finally块中移除return,continue,break,goto等语句,即把这些语句放在终止处理程序之外。
当系统因为try代码块中的代码提前退出而执行finally代码块时,就会发生局部展开。需要注意的是,应该避免在try代码块中使用return语句,因为这对应用程序性能是有害的。
局部展开:
从try块中的提前退出(由goto,longjump,continue,break,return等语句引起)将程序控制流程强制转入finally块。
BOOL AbnormalTermination();
我
们只能在finally块里调用这个内在函数,它将返回一个布尔值来表明一个与当前finally块相关的try块是否已经提前退出。如果代码执行从
try块正常流入finally块,函数的返回结果就是FALSE,如果控制流从try块中异常退出,或者因为try块中的代码抛出了访问内存访问违规或
其它异常引起全局展开,那么返回结果就是TRUE。
2.异常处理程序与软件异常
由CPU抛出的异常都是硬件异常
,操作系统和应用程序也可以抛出异常,这些异常通常被称为软件异常
。
当一个硬件或者软件异常被抛出时,操作系统会给我们的应用程序一个查看异常类型的机会,并允许应用程序逐级处理这个异常。下面的代码演示了异常处理程序的语法结构:
__try {
// Guarded body
...
}
__except (exception filter) {
// Exception handler
...
}
请注意__except
关键字
,
任
何时候创建一个try块,后面必须跟一个finally代码块或except代码块。但是try块后不能同时又finally块和except块,也不能
同时有多个finally块或except块。不过却可以将try-finally块嵌套try-except块中,反过来也可以。
尽管我们建议不要在终止处理程序的try块中使用return,goto,continue和break语句,但是在异常处理程序的try块中,这些语句不会导致程序性能损失或者增加代码量。换句话说,把这些语句置于try块中不会带来局部展开这样的额外开销。
表1:异常过滤程序的返回值
标识
|
值 |
---|---|
EXCEPTION_EXECUTE_HANDLER |
1 |
EXCEPTION_CONTINUE_SEARCH |
0 |
EXCEPTION_CONTINUE_EXECUTION |
–1 |
图1:系统处理异常的过程
EXCEPTION_EXECUTE_HANDLER
这个值基本上等于告诉系统,“我知道这个异
常,并预计这个异常在某种情况下会发生,同时已经写了一些代码来处理它,让这些代码现在就执行吧。”于是,系统执行全局展开
,让程序执行跳转到except代码块处(即异常处理程序代码)。当except代码块运行结束后,系统就会认为异常已经得到了处理,于是允许应用程序继续执行。从except代码块后开始继续执行。
全局展开
图2:系统如何执行全局展开
EXCEPTION_CONTINUE_SEARCH
系统在看到过滤程序返回值为
EXCEPTION_CONTINUE_SEARCH
后,将程序控制流跳转到导致异常的那条指令,并尝试重新执行这条指令。
在异常过滤程序中返回
EXCEPTION_CONTINUE_SEARCH
是需要非常谨慎的。
EXCEPTION_CONTINUE_EXECUTION
这个标示符让系统在调用栈中向上查找前一个带except块的try代码块,并调用这个try块对应的异常过滤程序。
GetExceptionCode
决定异常过滤程序返回什么标识符前,我们必须具体情况具体分析。
GetExceptionCode是内在函数,它的返回值表明刚刚发生的异常的类型。
DWORD GetExceptionCode();
内在函数GetExceptionCode只能在异常过滤程序里(即__except之后的括号里)或者异常处理程序的代码里调用。
GetExceptionInformation
当一个异常发生时,操作系统将向发生异常的线程的栈中压入三个结构:EXCEPTION_RECORD,CONTEXT和EXCEPTION_POINTERS。
EXCEPTION_RECORD结构包含关于抛出异常的信息,这些信息的内容与具体的CPU煤油关系。
CONTEXT结构则包含关于异常但与CPU有关的信息。
EXCEPTION_POINTERS结构仅包含两个数据成员,它们分别为指向被压入栈中的
EXCEPTION_RECORD结构的指针和指向
CONTEXT结构的指针。
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
如果想要得到这些信息,并在应用程序中使用它们,可以如下调用函数GetExceptionInformation
:
PEXCEPTION_POINTERS GetExceptionInformation();
这里有一个关于
GetExceptionInformation
的重要提示:这个函数只能在异常过滤程序中调用,这是因为
CONTEXT,
EXCEPTION_RECORD以及
EXCEPTION_POINTERS数据结构只有在系统计算异常过滤程序时才是有效的。一旦程序控制流被转移到异常处理程序或者别的地方,这些栈上的数据结构就被销毁。
void FuncSkunk() {
// Declare variables that we can use to save the exception
// record and the context if an exception should occur.
EXCEPTION_RECORD SavedExceptRec;
CONTEXT SavedContext;
...
__try {
...
}
__except (
SavedExceptRec =
*(GetExceptionInformation())->ExceptionRecord,
SavedContext =
*(GetExceptionInformation())->ContextRecord,
EXCEPTION_EXECUTE_HANDLER) {
// We can use the SavedExceptRec and SavedContext
// variables inside the handler code block.
switch (SavedExceptRec.ExceptionCode) {
...
}
}
...
}
软件异常
到目前为止,我们讨论了硬件异常,即由CPU捕获某一个事件并抛出的异常。其实,我们也可以在应用程序代码里强制抛出一个异常。这是函数将运行失败通知其调用者的另一种方式。
软件异常的捕获方式与硬件异常完全一样。因此,我们前面讨论的内容对软件异常同样适用。
抛出一个软件异常,只需要调用RaiseException函数:
VOID RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
CONST ULONG_PTR *pArguments);
C++异常与结构化异常的比较
SEH
是操作系统所提供的便利,它在任何语言中都是可以使用的。而C++异常处理只有在编写C++代码时才可以使用。如果读者在开发C++应用程序,那就应该使
用C++异常,而不是结构化异常(SEH)。理由是C++异常是语言的一部分,编译器知道什么是一个C++对象。这也就意味着编译器会自动生成代码来调用
C++对象的析构函数,保证对象的释放。
我们应该了解Microsoft的Visual
C++编译器使用操作系统的结构化异常机制来实现C++异常处理机制。所以,在创建一个C++ try块时,编译器会为我们生成一个SEH
__try块。C++的catch语句对应SEH异常过滤程序,catch块中的代码则对应SEH __except块中的代码。而编译器也会为C++
throw语句生成对Windows
RaiseException函数的调用。throw语句所使用的变量则成为RaiseException的附加参数。