windows异常处理程序与软件异常

以下内容引述《windows核心编程》

异常

异常分为硬件异常和软件异常

硬件异常

CPU负责捕获类似非法内存访问和以0作为除数这样的问题,一旦侦测到这些错误行为,它会抛出相应的异常。
由CPU抛出的异常都是硬件异常

软件异常

操作系统和应用程序抛出的异常,是软件异常

__try{
// Guard body
...
}
__except{
// Exception handler
...
}

异常过滤函数与异常处理程序

当异常被抛出时,系统定位到except块的开始处,并对异常过滤程序的表达式求值,这个表达式的值必定为一下三个标识符之一,这些标识符在Microsoft Windows的Excpt.h文件中定义

标志
EXCEPTION_EXECUTE_HANDLER1
EXCEPTION_CONTINUE_SEARCH0
EXCEPTION_CONTINUE_EXECUTION-1

系统的流程图如下图所示
系统处理异常的过程

EXCEPTION_EXECUTE_HANDLER

当异常过滤程序的值为EXCEPTION_EXECUTE_HANDLER,系统会认为异常已经得到了处理,于是允许应用程序继续执行。
大部分情况下,CPU指令只有在它们之前那条导致异常的指令返回一个合法值才能成功运行。

全局展开

当系统异常过滤程序的计算结果为EXCEPTION_EXECUTE_HANDLER时,系统必须执行全局展开。全局展开导致所有已经开始执行但尚未完成的try-finally块得以继续执行,在调用栈中,这些try-finally块位于对异常进行了处理的try-except块的下方。
系统执行全局展开
当异常过滤程序返回EXCEPTION_EXECUTE_HANDLER时,等于在告诉操作系统,当前线程指令指针应该指向except块中的指令。然而,实际上当前指令指针指向的是try块中的指令。我们知道当一个线程离开try-finally结构的try块部分,系统需要确保finally中的代码得到执行。当异常发生时,系统用来确保这条规则成立的机制就是全局展开。

从windows vista开始,如果异常发生在try/finally块中,并在其上层又没有try/except块(同时过滤函数返回EXCEPTION_EXCUTE_HANDLER),进程就会立刻终止。即全局展开并不会发生,finally块也不会执行。但是在早期版本的Windows中,全局展开会在进程终止前发生,从而finally块也能得到执行。

停止全局展开

可以通过将return语句置于finally块中以阻止系统完成全局展开。但是需要尽量避免这种情况。

void FuncMonkey(){
	__try{
		FuncFish();
	}
	__except(EXCEPTION_EXECUTE_HANDLER) {
		MessageBeep(0);
	}
	MessageBox(...);
}
void FuncFish() {
	FuncPheasant();
	MessageBox();
}
void FuncPheasant() {
	__try {
		strcpy(NULL, NULL);
	}
	__finally {
		return;
	}
}

代码执行过程中,在执行到strycpy时发生内存访问违规,系统开始检查是否可以处理该异常,发现FuncMonkey中的异常过滤程序可以处理它,于是系统初始化一个全局展开。
全局展开从执行FuncPheasant中的finally代码块开始,然而这个代码块中包含一个return语句,导致系统停止展开。实际上FuncPheasant也将停止执行,控制流返回到FunFish函数,后者继续执行并在屏幕上显示一个消息框,然后返回到FuncMonkey。
注意FuncMonkey的exception代码块没有机会执行对MessageBeep的调用,FuncPheasant中finally代码块的return语句导致系统停止之后所有全局展开,并让系统继续正常执行。

EXCEPTION_CONTINUE_EXECUTION

系统在看到过滤程序返回值为EXCEPTION_CONTINUE_EXECUTION后,将程序控制流跳转到导致异常的那条指令,并尝试重新执行这条指令。

谨慎使用EXCEPTION_CONTINUE_EXECUTION,尝试纠正导致异常发生的错误有时候能够成功,但并不是每次都那么幸运。

EXCEPTION_CONTINUE_SEARCH

这个标识符很简单,就是让系统在调用栈向上查找前一个带except的try代码块,并调用try块对应的异常过滤程序。

TCHAR g_szBuffer[100];
void FunclinRoosevelt3() {
	TCHAR* pchBuffer = NULL;
	__try {
		FuncAtude3(pchBuffer);
	}
	__except(OilFilter3(&pchBuffer)) {
		MessageBox(...);
	}
}
void FuncAtude3(TCHAR *sz) {
	__try {
		*sz = TEXT('\0');
	}
	__except (EXCEPTION_CONTINUE_SEARCH) {
		// this never executes.
	}
}
LONG OilFilter3(TCHAR **ppchBuffer) {
	if(*ppchBuffer == NULL) {
		*ppchBuffer = g_szBuffer;
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	return EXCEPTION_EXECUTE_HANDLER;
}

在FuncAtude3试图将’\0’写入地址NULL时,CPU异常,抛出EXCEPTION_CONTINUE_SEARCH,这个标志符让系统在调用栈中向上查找前一个带except块的try代码块,并调用这个try块对应的异常过滤程序。

因为FuncAtude3的异常过滤程序返回EXCEPTIOIN_CONTINUE_SEARCH,系统寻找到在它上层的一个try块并调用对应异常过滤程序中的函数OilFilter3,这会让FuncAtude3中的try块继续执行,但是sz变量并没发生变化,再次触发异常。但是OilFilter3看到pchBuffer不再是NULL,于是返回EXCEPTION_EXCUTE_HANDLER,让系统执行从except代码块开始,即执行FunclinRoosevelt3中的except块中的代码。

如果上述代码中FuncAtude3包含的是一个finally块而不是except块,系统首先会计算FunclinRoosevel3中的异常过滤程序,调用函数OilFilter3。

GetExceptionInfomation

当一个异常发生时,操作系统将向发生异常的线程的栈中压入三个结构,EXCEPTION_RECORD,CONTEXT,EXCEPTION_POINTERS
其中EXCEPTION_RECORD结构包含关于抛出异常的信息,这些信息的内容与具体的CPU没有关系。
CONTEXT结构则包含关于异常但与CPU也有关的信息。
EXCEPTION_POINTERS结构仅包含两个数据成员,他们分别为指向被压入栈中的EXCEPTION_RECORD结构的指针和指向CONTEXT结构的指针。

typedef struct _EXCEPTION_POINTERS{
	PEXCEPTOIN_RECORD ExceptionRecord;
	PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

想要得到这些信息,并在应用程序中使用他们,可以如下调用函数GetExceptionInformation

PEXCEPTION_POINTERS GetExceptionInformation();

但是这个函数只能在异常过滤程序中调用,因为CONTEXT,EXCEPTION_RECORD以及EXCEPTION_POINTERS数据结构只有在计算异常过滤程序时才是有效的。一旦程序控制流被转移到异常处理程序或者别的地方,这些栈上的数据结构就被销毁了。

如果需要在异常处理程序里访问异常信息,必须将EXCEPTION_POINTERS的成员指向的EXCEPTION_RECORD结构或者CONTEXT结构保存到我们建立的一个或多个变量。

void FuncSkunk() {
	EXCEPTION_RECORD SavedExceptRec;
	CONTEXT SavedContex;
	...
	__try{
		...
	}
	__except(
		SavedExcetRec = *(GetExceptionInformation())->ExceptionRecord,
		SavedContext = *(GetExceptionInformation())->ContextRecord,
		EXCEPTION_EXCUTE_HANDLER) {
		switch(SavedExceptRec.ExceptionCode){
			...
		}
	}
}

在__except()中使用的,符号,它告诉编译器从左到右执行逗号所分割的表达式。当所有表达式都求值完毕时,返回最后一个表达式。

说明:
在FuncSkunk中,最左边的表示是将线程栈上的EXCEPTION_RECORD结构到局部变量SavedExceptRec。这个表达式的计算结果就是变量SavedExceptRec的值,然而,这个值会被丢弃,然后其右边的表达式开始执行。
它将栈上的CONTEXT结构存储在局部变量SavedContext中,表达式结果即为变量SavedContext的值,同样,第二个表达式结果也会被丢弃。最后一个表达式很简单,即为常量EXCEPTION_EXECUTE_HANDLER,这个常量就是最后一个表达式也是整个逗号所分隔的表示式组的计算结果。
因为异常过滤程序返回EXCEPTION_EXECUTE_HANDLER,所以系统将会执行except代码块,此时因为SavedExceptRec和SavedContext变量此时已被初始化,所以我们可以在except块中使用它们。但是,请记住在try块以外声明SavedExceptRec和SavedContext变量。

typedef struct _EXCEPTION_RECORD {
	DWORD ExceptionCode;	// 异常代码, 即GetExceptionCode返回代码
	DWORD ExceptionFlags;	// 异常相关的标志,目前只有两个值,分别是0和EXCEPTION_NONCONTINUEABLE(程序试图在一个不能继续的异常发生之后继续执行)
	struct _EXCEPTION_RECORD *ExpcetionRecord; // 执行另一个未处理异常的EXCEPTION_RECORD结构,处理异常时,可能会发生另外的异常。嵌套异常进程会被终止。
	PVOID ExceptionAddress; // 表明导致异常的CPU指令的地址
	DWORD NumberParameters;	// 异常相关的参数的个数,对绝大部分的异常来说,这个值是0
	ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 表示用来进一步描述异常的附加参数数组
} EXCEPTION_RECORD;

EXCEPTION_POINTERS结构中的ContextRecord成员指向一个CONTEXT结构,这个机构是与平台相关的——即它在不同的CPU平台上有着不同的内容。

软件异常

上述为硬件异常,我们也可以通过软件异常在应用代码中强制抛出一个异常。这是函数将运行失败通知其调用者的另一种方式。或者我们可以让函数在失败时抛出异常,而不是返回错误码。
怎么通过异常机制来返回函数错误?
举个例子,首先看下windows的堆操控函数,比如HeapCreate和HeapAlloc等。这些函数给开发者提供了一种选择,通常情况下,当堆操控函数失败时,会返回NULL表示失败。然而也可以传入HEAP_GENERATE_EXCEPTIOIN标记,要求他们在失败时,不要返回NULL而是抛出一个STATUS_NO_MEMORY的软件异常。

抛出异常

VOID RaiseException(
	DWORD dwExceptionCode,	// 异常标识符
	DWORD dwExceptionFlags,	// 必须设置成 0 或者 EXCEPTIOIN_NONCONTINUEABLE(程序不可继续执行)
	DOWRD nNumberOfArguments, // 附加信息
	CONST ULONG_PTR *pArguments);  // 附加信息

其中参数dwExceptionFlags,如果不设置EXCEPTION_NONCONTINUABLE标记,过滤程序就可以返回EXCEPTION_CONTINUE_EXECUTIOIN,正常情况下会导致线程重新执行产生软件异常的那条CPU指令。
如果设置了EXCEPTION_NONCONTINUABLE,等于告诉系统一旦发生这种类型的异常,程序变不能继续,这个标志在操作系统内部被用来传递即为严重的错误信息。比如,当HeapAlloc抛出STATUS_NO_MEMORY软件异常时,使用EXCEPTION_NONCONTINUABLE标志来告诉系统,发生这个异常后,程序就不能再继续执行下去。
如果过滤程序忽略EXCEPTION_NONCONTINUABLE并返回EXCEPTION_CONTINUE_EXECUTION,系统会抛出一个新的异常: EXCEPTIOIN_NONCONTINUABLE_EXCEPTIOIN。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值