23.2 SEH之异常处理--《Windows核心编程》结构化异常处理
(structured exception handing)SEH 包含终止处理和异常处理。本章讨论异常处理。
当一个硬件或者软件异常被抛出的时候,操作系统会给我们程序一个查看异常类型的机会,并允许程序自己处理这个异常。下面演示了异常处理程序的语法结构:
_try{
// Guarded body
}
_except(EXCEPTION_EXECUTE_HANDLER) // 也可以是EXCEPTION_CONTINUE_SEARCH或者EXCEPTION_CONTINUE_EXECUTE
{
// Exception handler
}
请注意:try 语句后必须但只能跟一个 finally 或者 except 块。不过可以将 try-finally 嵌套于 try-except 中。
异常过滤程序的返回值:
EXCEPTION_EXECUTE_HANDLER(1):执行except块,执行完except块以后,接着执行except块之后的代码,而不会返回到出现异常的代码
EXCEPTION_CONTINUE_SEARCH(0):搜索更高层的except进行执行
EXCEPTION_CONTINUE_EXECUTION(-1):系统将程序控制流跳转到导致异常那条指令,尝试重新执行
EXCEPTION_EXECUTE_HANDLER 之 全局展开
我们知道,一个线程离开 try-finally 结果的 try 块后,系统需要确保 finally 块的代码得到执行。发生异常时,系统用来确保这条规则成立的机制就是全局展开。
当异常过滤程序的计算结果为 EXCEPTION_EXECUTE_HANDLER 时,系统必须执行全局展开。全局展开导致所有已经开始执行但尚未完成的 try-finally 块得以继续执行,在调用栈中,这些 try-finally 块位于对异常进行了处理的 try-except 块的下方。
实例程序:
#include<iostream>
#include<Windows.h>
using namespace std;
DWORD g_dwProtectData = 10;
void FuncRen1()
{
DWORD dwTemp = 0;
__try
{
__try
{
g_dwProtectData = 5 / dwTemp; // 除0,产生异常
}
__finally // 已经开始执行但未完成的带 finally 的 try 块
{
MessageBox(NULL, "This is finally", 0, NULL);
}
g_dwProtectData = 5 / 2; // 调试,不会被执行
}
__finally // 已经开始执行但未完成的带 finally 的 try 块
{
MessageBox(NULL, "This is finally 2", 0, NULL);
}
__try
{
g_dwProtectData = 5 / 3; // 除0,产生异常
}
__finally // 未开始执行但未完成的带 finally 的 try 块
{
MessageBox(NULL, "This is finally 3", 0, NULL); // 不会执行
}
}
int main()
{
__try
{
FuncRen1();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
MessageBox(NULL, "This is except", 0, NULL);
}
}
过程:
注:这里向上是指先被执行的代码。
FuncRen1 中0作为除数的运算产生一个异常,系统夺回控制权,搜索与 try 匹配的 except 块。因为在 FuncRen1 中与 try 匹配的是 finally,于是系统在调用栈中向上搜索之前的 try 块,它会看到函数 main 中 try 与 except 匹配。
于是系统执行与 main 中 except 相关的异常过虑程序,得到 EXCEPTION_EXECUTE_HANDLER 结果,马上在 FuncRen1 的 finally 块中全局展开,此时 main 中 except 块还未执行。在进行全局展开的时候,系统会从调用栈的底部向上搜索所有已经开始执行但尚为完成的 try 块,寻找其中带有 finally 的 try 块,然后会执行这些 finally 块中内容。
最后 finally 中代码执行完之后,系统继续在调用栈中向上寻找需要被执行的 finally,如果没有,就执行 except 代码块。
停止全局展开
可以通过将 return 语句置于 finally 块中以阻止系统完成全局展开。这会导致调用 return 的函数结束,控制流回到上一个函数,并让系统正常执行,就好像没有发生过一样,except 代码块也不会执行。
#include<iostream>
#include<Windows.h>
using namespace std;
DWORD g_dwProtectData = 10;
void FuncRen1()
{
DWORD dwTemp = 0;
__try
{
g_dwProtectData = 5 / dwTemp; // 除0,产生异常
}
__finally // 已经开始执行但未完成的带 finally 的 try 块
{
return; // 停止全局展开,函数停止,控制权流回到上一个函数,
// 并让系统正常执行,就好像没有发生过异常一样
}
}
void Func1()
{
FuncRen1();
MessageBox(NULL, "Func1", "Warning", NULL); // 因为停止全局展开,控制权回到这里,会执行
}
int main()
{
__try
{
Func1();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
MessageBox(NULL, "This is except", "Warning", NULL); // 不会执行
// 停止全局展开...让系统正常执行,就好像没有发生过异常一样
}
}
结果:
EXCEPTION_CONTINUE_SEARCH(0):搜索更高层的except进行执行
实例程序:
#include<iostream>
#include<Windows.h>
#include <tchar.h>
using namespace std;
TCHAR g_szBuffer[100];
LONG OliFilter3(TCHAR **ppchBuffer)
{
if (*ppchBuffer == NULL)
{
*ppchBuffer = g_szBuffer; // ④修复之前pchBuffer = NULL的错误
return EXCEPTION_CONTINUE_EXECUTION; // ⑤继续执行异常位置
}
return EXCEPTION_EXECUTE_HANDLER; // ⑦第二次执行这个函数时,pchBuffer != NULL,此处被执行
}
void FuncAtude3(TCHAR *sz)
{
__try {
*sz = TEXT('\0'); // ① 未初始化指针,异常
// ⑥ 局部变量 sz 还是NULL,依然异常,再执行一次①~③
}
__except (EXCEPTION_CONTINUE_SEARCH) { // ②让系统在调用栈中向上找前一个带except的try代码块,并调用其异常过虑程序
// 这里永远不会被执行
}
}
void FunclinRoosevelt3()
{
TCHAR *pchBuffer = NULL;
__try
{
FuncAtude3(pchBuffer);
MessageBox(NULL, "This is FunclinRoosevelt3 before", "Warning", NULL);
}
__except(OliFilter3(&pchBuffer)) // ③由于 EXCEPTION_CONTINUE_SEARCH,这里被调用。
{
MessageBox(NULL, "This is FunclinRoosevelt3", "Warning", NULL); // ⑧ 执行
}
}
int main()
{
FunclinRoosevelt3();
}
结果:
GetExceptionCode
GetExceptionCode 是内在函数,其返回值表明刚发生过的异常的类型。
DWORD GetExceptionCode();
返回值来自于 Platform SDK 的文档,可以在 WinBase.h 中找到它们:
// 内存相关异常
EXCEPTION_ACCESS_VIOLATION 写一个错误的虚拟地址
EXCEPTION_DATATYPE_MISALIGNMENT 线程试图从没有提供自动对齐机制的硬件里读入没有对齐的数据。
EXCEPTION_ARRAY_BOUNDS_EXCEEDED 线程在支持边界检查的硬件上访问越界的数组元素。
EXCEPTION_IN_PAGE_ERROR 文件系统或设备取得读取错误而引起的页错误。
EXCEPTION_GUARD_PAGE 线程试图访问具备PAGE_GUARD属性内存页。
EXCEPTION_STACK_OVERFLOW 线程用光栈空间
EXCECPTION_ILLEGAL_INSTRUCTION 非法指令
EXCEPTION_PRIV_INSTRUCTION 试图执行当前机器模式下不允许的操作
// 与异常本身相关的异常
EXCEPTION_INVALID_DISPOSITION 异常过滤返回3个常量以外值
EXCEPTION_NONCONTINUABLE_EXCEPTION 异常过滤返回继续执行,但实际这类异常不能继续
// 与调试相关异常
EXCEPTION_BREAKPOINT 执行到断点
EXCEPTION_SINGLE_STEP 单步
EXCEPTION_INVALID_HANDLE 传入无效句柄给一函数
// 与整型相关的异常
EXCEPTION_INT_DIVIDE_BY_ZERO 线程试图在整数除法运算中以0做除数
EXCEPTION_INT_OVERFLOW 整型运算结果超出范围
// 与浮点类型相关的异常
EXCEPTION_FLT_DENORMAL_OPERAND 浮点运算中一个运算数是不能作为标准浮点数的小数。
EXCEPTION_FLT_DIVIDE_BY_ZERO 线程试图在浮点除法中以0做除数
EXCEPTION_FLT_INEXACT_RESULT 浮点运算结果不能精确地表示为十进制小数
EXCEPTION_FLT_INVALID_OPERATION 其它浮点数异常
EXCEPTION_FLT_OVERFLOW 浮点运算指数部分超出该类型允许的最大值
EXCEPTION_FLT_STACK_CHECK 浮点运算造成栈上溢出或下溢出
EXCEPTION_FLT_UNDERFLOW 浮点运算指数部分小于该类型允许的最小值
#include<iostream>
#include<Windows.h>
#include <tchar.h>
using namespace std;
void FunclinRoosevelt3()
{
__try {
int y = 0;
int x = 4 / y;
}
__except (
((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ||
(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO)) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
switch (GetExceptionCode())
{
case EXCEPTION_ACCESS_VIOLATION:
MessageBox(NULL, "EXCEPTION_ACCESS_VIOLATION", 0, NULL);
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
MessageBox(NULL, "EXCEPTION_INT_DIVIDE_BY_ZERO", 0, NULL); // 执行
break;
}
}
}
int main()
{
FunclinRoosevelt3();
}
但是我们不能在异常过虑程序中调用 GetExceptionCode ,所以以下代码会编译报错。
但是可以这样写:将 GetExceptionCode 返回值作为参数传入异常过虑程序。
__except (CoffeeFilter(GetExceptionCode()))
GetExceptionInformation
一个异常发生时,系统将向发生异常的线程的栈中压入3个结构,
- EXCEPTION_RECORD// 包含关于抛出异常的信息
- CONTEXT// 与异常和CPU相关的信息
- EXCEPTION_POINTERS// 含两个指针,指向前两个结构
这个函数只能在异常过滤程序中调用,因为上述3个结构只在系统计算异常过滤程序时才有效。
仅仅限与_except后的括号里。
如果要得到这些信息,并在程序中使用,可以调用 GetExceptionInformation 函数
PEXCEPTION_POINTERS GetExceptionInformation();
// 包含关于抛出异常的信息
typedef struct _EXCEPTION_RECORD
{
// 异常码,就是 GetExceptionCode 的返回值
DWORD ExceptionCode;
// 异常标志
DWORD ExceptionFlags;
// 异常链
struct _EXCEPTION_RECORD *ExceptionRecord;
// 导致异常的CPU指令地址
PVOID ExceptionAddress;
// 异常相关参数个数
DWORD NumberParameters;
// 描述异常的附加数组
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
}EXCEPTION_RECORD;
EXCEPTION_RECORD SavedExceptRec; // 包含关于抛出异常的信息
CONTEXT SacedContext; // 与异常和CPU相关的信息
__try {
// ...
}
__except (
SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord,
SacedContext = *(GetExceptionInformation())->ContextRecord,
EXCEPTION_EXECUTE_HANDLER) {
switch (SavedExceptRec.ExceptionCode)
{
// ...
}
}
软件异常
目前为止我们讨论的都是硬件异常。软件异常的捕获方式和硬件异常完全一样。
本部分要讨论的是如何让函数强制抛出软件异常,以作为一种指明错误的方法。需要调用函数 RaiseException。
// 抛出一个软件异常
VOID RaiseException(
// 要抛出异常标识符。
// 如果定义自己的异常标识符,要遵循windows错误代码定义的标准格式。
DWORD dwExceptionCode,
// 标志:
// 0
// EXCEPTION_NONCONTINUABLE,此时异常过滤程序不能返回EXCEPTION_CONTINUE_EXECUTION,一般用来传递不可恢复的错误信息
// 此时,过滤程序若返回EXCEPTION_CONTINUE_EXECUTION,会引发EXCEPTION_NONCONTINUABLE_EXCEPTION
DWORD dwExceptionFlags,
// 异常附件信息,一般不需要,置NULL
DWORD nNumberOfArguments,
// 异常附件信息,一般不需要,置NULL
CONST ULONG_PTR *pArguments);