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);
posted @ 2022-11-24 20:26  人类观察者  阅读(624)  评论(0编辑  收藏  举报