从本篇文章开始,将全面阐述__try,__except,__finally,__leave异常模型机制,它也即是Windows系列操作系统平台上提供的SEH模型。主人公阿愚将在这里与大家分享SEH的学习过程和经验总结。
SEH有两项非常强大的功能。当然,首先是异常处理模型了,因此,这篇文章首先深入阐述SEH提供的异常处理模型。另外,SEH还有一个特别强大的功能,这将在下一篇文章中进行详细介绍。
try-except入门 SEH的异常处理模型主要由try-except语句来完成,它与标准C++所定义的异常处理模型非常类似,也都是可以定义出受监控的代码模块,以及定义异常处理模块等。还是老办法,看一个例子先,代码如下:
//seh-test.c
#include <stdio.h>
void main()
{
puts("hello");
// 定义受监控的代码模块
__try
{
puts("in try");
}
//定义异常处理模块
__except(1)
{
puts("in except");
}
puts("world");
}
呵呵!是不是很简单,而且与C++异常处理模型很相似。当然,为了与C++异常处理模型相区别,VC编译器对关键字做了少许变动。首先是在每个关键字加上两个下划线作为前缀,这样既保持了语义上的一致性,另外也尽最大可能来避免了关键字的有可能造成名字冲突而引起的麻烦等;其次,C++异常处理模型是使用catch关键字来定义异常处理模块,而SEH是采用__except关键字来定义。并且,catch关键字后面往往好像接受一个函数参数一样,可以是各种类型的异常数据对象;但是__except关键字则不同,它后面跟的却是一个表达式(可以是各种类型的表达式,后面会进一步分析)。
try-except进阶
与C++异常处理模型很相似,在一个函数中,可以有多个try-except语句。它们可以是一个平面的线性结构,也可以是分层的嵌套结构。例程代码如下:
// 例程1
// 平面的线性结构
#include <stdio.h>
void main()
{
puts("hello");
__try
{
puts("in try");
}
__except(1)
{
puts("in except");
}
// 又一个try-except语句
__try
{
puts("in try");
}
__except(1)
{
puts("in except");
}
puts("world");
}
// 例程2
// 分层的嵌套结构
#include <stdio.h>
void main()
{
puts("hello");
__try
{
puts("in try");
// 又一个try-except语句
__try
{
puts("in try");
}
__except(1)
{
puts("in except");
}
}
__except(1)
{
puts("in except");
}
puts("world");
}
// 例程3
// 分层的嵌套在__except模块中
#include <stdio.h>
void main()
{
puts("hello");
__try
{
puts("in try");
}
__except(1)
{
// 又一个try-except语句
__try
{
puts("in try");
}
__except(1)
{
puts("in except");
}
puts("in except");
}
puts("world");
}
try-except异常处理规则
try-except异常处理规则与C++异常处理模型有相似之处,例如,它们都是向上逐级搜索恰当的异常处理模块,包括跨函数的多层嵌套try- except语句。但是,它们的处理规则也有另外一些很大的不同之处,例如查找匹配恰当的异常处理模块的过程,在C++异常处理模型中,它是通过异常对象的类型来匹配;但是在try-except语句的异常处理规则中,则是通过__except关键字后面括号中的表达式的值来匹配查找正确的异常处理模块。还是看看MSDN中怎么说的吧!摘略如下:
The compound statement after the __try clause is the body or guarded section. The compound statement after the __except clause is the exception handler. The handler specifies a set of actions to be taken if an exception is raised during execution of the body of the guarded section. Execution proceeds as follows:
1. The guarded section is executed.
2. If no exception occurs during execution of the guarded section, execution continues at the statement after the __except clause.
3. If an exception occurs during execution of the guarded section or in any routine the guarded section calls, the __except expression is evaluated and the value determines how the exception is handled. There are three values:
EXCEPTION_CONTINUE_EXECUTION (–1) Exception is dismissed. Continue execution at the point where the exception occurred.
EXCEPTION_CONTINUE_SEARCH (0) Exception is not recognized. Continue to search up the stack for a handler, first for containing try-except statements, then for handlers with the next highest precedence.
EXCEPTION_EXECUTE_HANDLER (1) Exception is recognized. Transfer control to the exception handler by executing the __except compound statement, then continue execution at the assembly instruction that was executing when the exception was raised.
Because the __except expression is evaluated as a C expression, it is limited to a single value, the conditional-expression operator, or the comma operator. If more extensive processing is required, the expression can call a routine that returns one of the three values listed above.
对查找匹配恰当的异常处理模块的过程等几条规则翻译如下:
1. 受监控的代码模块被执行(也即__try定义的模块代码);
2. 如果上面的代码执行过程中,没有出现异常的话,那么控制流将转入到__except子句之后的代码模块中;
3. 否则,如果出现异常的话,那么控制流将进入到__except后面的表达式中,也即首先计算这个表达式的值,之后再根据这个值,来决定做出相应的处理。这个值有三种情况,如下:
EXCEPTION_CONTINUE_EXECUTION (–1) 异常被忽略,控制流将在异常出现的点之后,继续恢复运行。
EXCEPTION_CONTINUE_SEARCH (0) 异常不被识别,也即当前的这个__except模块不是这个异常错误所对应的正确的异常处理模块。系统将继续到上一层的try-except域中继续查找一个恰当的__except模块。
EXCEPTION_EXECUTE_HANDLER (1) 异常已经被识别,也即当前的这个异常错误,系统已经找到了并能够确认,这个__except模块就是正确的异常处理模块。控制流将进入到__except模块中。
上面的规则其实挺简单的,很好理解。当然,这个规则也非常的严谨,它能很好的满足开发人员的各种需求,满足程序员对异常处理的分类处理的要求,它能够给程序员提供一个灵活的控制手段。
其中比较特殊的就是__except关键字后面跟的表达式,它可以是各种类型的表达式,例如,它可以是一个函数调用,或是一个条件表达式,或是一个逗号表达式,或干脆就是一个整型常量等等。例如代码如下:
// seh-test.c
// 异常处理模块的查找过程演示
#include <stdio.h>
int seh_filer()
{
return 0;
}
void test()
{
__try
{
int* p;
puts("test()函数的try块中");
// 下面将导致一个异常
p = 0;
*p = 45;
}
// 注意,__except关键字后面的表达式是一个函数表达式
// 而且这个函数将返回0,所以控制流进入到上一层
// 的try-except语句中继续查找
__except(seh_filer())
{
puts("test()函数的except块中");
}
}
void main()
{
puts("hello");
__try
{
puts("main()函数的try块中");
// 注意,这个函数的调用过程中,有可能出现一些异常
test();
}
// 注意,这个表达式是一个逗号表达式
// 它前部分打印出一条message,后部分是
// 一个常量,所以这个值也即为整个表达式
// 的值,因此系统找到了__except定义的异
// 常处理模块,控制流进入到__except模块里面
__except(puts("in filter"), 1)
{
puts("main()函数的except块中");
}
puts("world");
}
上面的程序运行结果如下:
hello
main()函数的try块中
test()函数的try块中
in filter
main()函数的except块中
world
Press any key to continue
这种运行结果应该是在意料之中吧!为了对它的流程进行更清楚的分析,下图描述出了程序的运行控制流转移过程,如下。
http://byfiles.storage.msn.com/x1pN1mp8dKYgTFQGzKRebME6105or51BGbjDrskKQ5x3cw-RNCRExH00cuzP8U6qAybuiCE9msw4yGhML-hVWfFOK8DnWPKA9WtlamUEAPnYIJs4c-bXtKEVQ
另外,对于__except关键字后面表达式的值,上面的规则中已经做了详细规定。它们有三种值,其中如果为0,那么系统继续查找;如果为1,表示系统已经找到正确的异常处理模块。其实这两个值都很好理解,可是如果值为-1的话,那么处理将比较特殊,上面也提到了,此种情况下,“异常被忽略,控制流将在异常出现的点之后,继续恢复运行。”实际上,这就等同于说,程序的执行过程将不受干扰,好像异常从来没有发生一样。看一个例程吧!代码如下:
#include <stdio.h>
void main()
{
int j, zero;
puts("hello");
__try
{
puts("main()函数的try块中");
zero = 0;
j = 10;
// 下面将导致一个异常
j = 45 / zero;
// 注意,异常出现后,程序控制流又恢复到了这里
printf("这里会执行到吗?值有如何呢?j=%d \n", j);
}
// 注意,这里把zero变量赋值为1,试图恢复错误,
// 当控制流恢复到原来异常点时,避免了异常的再次发生
__except(puts("in filter"), zero = 1, -1)
{
puts("main()函数的except块中");
}
puts("world");
}
上面的程序运行结果如下:
hello
main()函数的try块中
in filter
这里会执行到吗?值有如何呢?j=45
world
Press any key to continue
呵呵!厉害吧!要知道C++异常处理模型可没有这样的能力。但是请注意,一般这项功能不能轻易采用,为什么呢?因为它会导致不稳定,再看下面一个示例,代码如下:
#include <stdio.h>
void main()
{
int* p, a;
puts("hello");
__try
{
puts("main()函数的try块中");
// 下面将导致一个异常
p = 0;
*p = 45;
printf("这里会执行到吗?值有如何呢?p=%d \n", *p);
}
// 注意,这里把p指针赋了一个合法的值,也即说,
// 当控制流恢复到原来异常点时,异常将不会再次发生
__except(puts("in filter"), p = &a, -1)
{
puts("main()函数的except块中");
}
puts("world");
}
呵呵!大家猜猜上面的程序的运行结果如何呢?是不是和刚才的那个例子一样,异常也得以被恢复了。朋友们!还是亲自运行测试一把。哈哈!程序运行结果是死了,进行一个无限循环当中,并且控制终端内不断输出“in filter”信息。为什么会出现这种情况,难道MSDN中有关的阐述的有问题吗?或这个异常处理模型实现上存在BUG?NO!不是这样的,实际上这就是由于表达式返回-1值时,给程序所带来的不稳定性。当然,MSDN中有关的阐述也没有错,那么究竟具体原因是为何呢?这是因为,表达式返回-1值时,系统将把控制流恢复到异常出现点之后继续运行。这意味着什么呢?也许大家都明白了,它这里的异常恢复点是基于一条机器指令级别上的。这样就有很大的风险,因为上面的例程中,所谓的异常恢复处理,也即p = &a语句,它实际上的确改变了p指针值,但是这个指针值是栈上的某个内存区域,而真正出现异常时,代表p指针值的很有可能是某个寄存器。呵呵!是不是挺费解的,没关系!还是看看调试界图吧!如下:
http://byfiles.storage.msn.com/x1pN1mp8dKYgTFQGzKRebME62KovOI2zIOz3rCTVXO99-ku0JJm6Gg--r6FEmfo7NKpWBwPGnL6pvfxPZ2zpd1rP8MhDhEmzvQiib6XZ1TEJqu_HREcahE28g
try-except深入
上面的内容中已经对try-except进行了全面的了解,但是有一点还没有阐述到。那就是如何在__except模块中获得异常错误的相关信息,这非常关键,它实际上是进行异常错误处理的前提,也是对异常进行分层分级别处理的前提。可想而知,如果没有这些起码的信息,异常处理如何进行?因此获取异常信息非常的关键。Windows提供了两个API函数,如下:
LPEXCEPTION_POINTERS GetExceptionInformation(VOID);
DWORD GetExceptionCode(VOID);
其中GetExceptionCode()返回错误代码,而GetExceptionInformation()返回更全面的信息,看它函数的声明,返回了一个LPEXCEPTION_POINTERS类型的指针变量。那么EXCEPTION_POINTERS结构如何呢?如下,
typedef struct _EXCEPTION_POINTERS { // exp
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS;
呵呵!仔细瞅瞅,这是不是和上一篇文章中,用户程序所注册的异常处理的回调函数的两个参数类型一样。是的,的确没错!其中 EXCEPTION_RECORD类型,它记录了一些与异常相关的信息;而CONTEXT数据结构体中记录了异常发生时,线程当时的上下文环境,主要包括寄存器的值。因此有了这些信息,__except模块便可以对异常错误进行很好的分类和恢复处理。不过特别需要注意的是,这两个函数只能是在 __except后面的括号中的表达式作用域内有效,否则结果可能没有保证(至于为什么,在后面深入分析异常模型的实现时候,再做详细阐述)。看一个例程吧!代码如下:
#include <windows.h>
#include <stdio.h>
int exception_access_violation_filter(LPEXCEPTION_POINTERS p_exinfo)
{
if(p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
printf("存储保护异常\n");
return 1;
}
else return 0;
}
int exception_int_divide_by_zero_filter(LPEXCEPTION_POINTERS p_exinfo)
{
if(p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
printf("被0除异常\n");
return 1;
}
else return 0;
}
void main()
{
puts("hello");
__try
{
__try
{
int* p;
// 下面将导致一个异常
p = 0;
*p = 45;
}
// 注意,__except模块捕获一个存储保护异常
__except(exception_access_violation_filter(GetExceptionInformation()))
{
puts("内层的except块中");
}
}
// 注意,__except模块捕获一个被0除异常
__except(exception_int_divide_by_zero_filter(GetExceptionInformation()))
{
puts("外层的except块中");
}
puts("world");
}
上面的程序运行结果如下:
hello
存储保护异常
内层的except块中
world
Press any key to continue
呵呵!感觉不错,大家可以在上面的程序基础之上改动一下,让它抛出一个被0除异常,看程序的运行结果是不是如预期那样。
最后还有一点需要阐述,在C++的异常处理模型中,有一个throw关键字,也即在受监控的代码中抛出一个异常,那么在SEH异常处理模型中,是不是也应该有这样一个类似的关键字或函数呢?是的,没错!SEH异常处理模型中,对异常划分为两大类,第一种就是上面一些例程中所见到的,这类异常是系统异常,也被称为硬件异常;还有一类,就是程序中自己抛出异常,被称为软件异常。怎么抛出呢?还是Windows提供了的API函数,它的声明如下:
VOID RaiseException(
DWORD dwExceptionCode, // exception code
DWORD dwExceptionFlags, // continuable exception flag
DWORD nNumberOfArguments, // number of arguments in array
CONST DWORD *lpArguments // address of array of arguments
);
很简单吧!实际上,在C++的异常处理模型中的throw关键字,最终也是对RaiseException()函数的调用,也即是说,throw是 RaiseException的上层封装的更高级一类的函数,这以后再详细分析它的代码实现。这里还是看一个简单例子吧!代码如下:
#include <windows.h>
#include <stdio.h>
int seh_filer(int code)
{
switch(code)
{
case EXCEPTION_ACCESS_VIOLATION :
printf("存储保护异常,错误代码:%x\n", code);
break;
case EXCEPTION_DATATYPE_MISALIGNMENT :
printf("数据类型未对齐异常,错误代码:%x\n", code);
break;
case EXCEPTION_BREAKPOINT :
printf("中断异常,错误代码:%x\n", code);
break;
case EXCEPTION_SINGLE_STEP :
printf("单步中断异常,错误代码:%x\n", code);
break;
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED :
printf("数组越界异常,错误代码:%x\n", code);
break;
case EXCEPTION_FLT_DENORMAL_OPERAND :
case EXCEPTION_FLT_DIVIDE_BY_ZERO :
case EXCEPTION_FLT_INEXACT_RESULT :
case EXCEPTION_FLT_INVALID_OPERATION :
case EXCEPTION_FLT_OVERFLOW :
case EXCEPTION_FLT_STACK_CHECK :
case EXCEPTION_FLT_UNDERFLOW :
printf("浮点数计算异常,错误代码:%x\n", code);
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO :
printf("被0除异常,错误代码:%x\n", code);
break;
case EXCEPTION_INT_OVERFLOW :
printf("数据溢出异常,错误代码:%x\n", code);
break;
case EXCEPTION_IN_PAGE_ERROR :
printf("页错误异常,错误代码:%x\n", code);
break;
case EXCEPTION_ILLEGAL_INSTRUCTION :
printf("非法指令异常,错误代码:%x\n", code);
break;
case EXCEPTION_STACK_OVERFLOW :
printf("堆栈溢出异常,错误代码:%x\n", code);
break;
case EXCEPTION_INVALID_HANDLE :
printf("无效句病异常,错误代码:%x\n", code);
break;
default :
if(code & (1<<29))
printf("用户自定义的软件异常,错误代码:%x\n", code);
else
printf("其它异常,错误代码:%x\n", code);
break;
}
return 1;
}
void main()
{
puts("hello");
__try
{
puts("try块中");
// 注意,主动抛出一个软异常
RaiseException(0xE0000001, 0, 0, 0);
}
__except(seh_filer(GetExceptionCode()))
{
puts("except块中");
}
puts("world");
}
上面的程序运行结果如下:
hello
try块中
用户自定义的软件异常,错误代码:e0000001
except块中
world
Press any key to continue
上面的程序很简单,这里不做进一步的分析。我们需要重点讨论的是,在__except模块中如何识别不同的异常,以便对异常进行很好的分类处理。毫无疑问,它当然是通过GetExceptionCode()或GetExceptionInformation ()函数来获取当前的异常错误代码,实际也即是DwExceptionCode字段。异常错误代码在winError.h文件中定义,它遵循 Windows系统下统一的错误代码的规则。每个DWORD被划分几个字段,如下表所示:
http://byfiles.storage.msn.com/x1pN1mp8dKYgTFQGzKRebME6105or51BGbjDrskKQ5x3cw-RNCRExH00cuzP8U6qAybuiCE9msw4yGhML-hVWfFOK8DnWPKA9WtlamUEAPnYIJs4c-bXtKEVQ
例如我们可以在winbase.h文件中找到EXCEPTION_ACCESS_VIOLATION的值为0 xC0000005,将这个异常代码值拆开,来分析看看它的各个bit位字段的涵义。
C 0 0 0 0 0 0 5 (十六进制)
1100 0000 0000 0000 0000 0000 0000 0101 (二进制)
第3 0位和第3 1位都是1,表示该异常是一个严重的错误,线程可能不能够继续往下运行,必须要及时处理恢复这个异常。第2 9位是0,表示系统中已经定义了异常代码。第2 8位是0,留待后用。第1 6 位至2 7位是0,表示是FACILITY_NULL设备类型,它代表存取异常可发生在系统中任何地方,不是使用特定设备才发生的异常。第0位到第1 5位的值为5,表示异常错误的代码。
如果程序员在程序代码中,计划抛出一些自定义类型的异常,必须要规划设计好自己的异常类型的划分,按照上面的规则来填充异常代码的各个字段值,如上面示例程序中抛出一个异常代码为0xE0000001软件异常。
总结
(1) C++异常模型用try-catch语法定义,而SEH异常模型则用try-except语法;
(2) 与C++异常模型相似,try-except也支持多层的try-except嵌套。
(3) 与C++异常模型不同的是,try-except模型中,一个try块只能是有一个except块;而C++异常模型中,一个try块可以有多个catch块。
(4)与C++异常模型相似,try-except模型中,查找搜索异常模块的规则也是逐级向上进行的。但是稍有区别的是,C++异常模型是按照异常对象的类型来进行匹配查找的;而try-except模型则不同,它通过一个表达式的值来进行判断。如果表达式的值为1 (EXCEPTION_EXECUTE_HANDLER),表示找到了异常处理模块;如果值为0 (EXCEPTION_CONTINUE_SEARCH),表示继续向上一层的try-except域中继续查找其它可能匹配的异常处理模块;如果值为- 1(EXCEPTION_CONTINUE_EXECUTION),表示忽略这个异常,注意这个值一般很少用,因为它很容易导致程序难以预测的结果,例如,死循环,甚至导致程序的崩溃等。
(5) __except关键字后面跟的表达式,它可以是各种类型的表达式,例如,它可以是一个函数调用,或是一个条件表达式,或是一个逗号表达式,或干脆就是一个整型常量等等。最常用的是一个函数表达式,并且通过利用GetExceptionCode()或GetExceptionInformation ()函数来获取当前的异常错误信息,便于程序员有效控制异常错误的分类处理。
(6) SEH异常处理模型中,异常被划分为两大类:系统异常和软件异常。其中软件异常通过RaiseException()函数抛出。RaiseException()函数的作用类似于C++异常模型中的throw语句。
本篇文章已经对SEH的异常处理进行了比较全面而深入的阐述,相信大家现在已经对SEH的异常处理机制胸有成竹了。但是SEH的精华仅只如此吗?非也,朋友们!继续到下一篇的文章中,主人公阿愚将和大家一起共同探讨SEH模型的另一项重要的机制,那就是“有效保证资源的清除”。这对于C程序可太重要了,因为在C++程序中,至少还有对象的析构函数来保证资源的有效清除,避免资源泄漏,但C语言中则没有一个有效的机制,来完成此等艰巨的任务。呵呵!SEH 雪中送炭,它提供了完美的解决方案,所以千万不要错过,一起去看看吧!Let’s go!