Windows 结构化异常与 C / C++异常的一些事

一直以来对“异常”以及相关的知识很模糊,这次做个整理,回答几个基本问题以帮助对“异常”的理解:

  • C语言的异常
  • C语言版的try/catch: setjmp, longjmp
  • C++的异常(try/catch)
  • Windows 结构化异常,以及与C++标准异常的联系(__try/__except与try/catch)
  • 在Windows API 与标准C++代码混用的项目中,如何较好的捕捉所有异常?

C语言的异常

C语言本身对异常处理比较少,当异常发生时,往往有以下方式:

1. exit()与abort()。

使用C库函数exit()和abort() 强行终止程序运行。

exit() - 通常表示程序正常退出。exit()退出之前会做一些清理工作,比如销毁static变量,刷一下缓冲区(不知道是否指文件系统缓冲区?),关闭IO通道,然后结束程序。用户还可通过at_exixt(your_exit_fun)设置自己的退出函数,程序exit()时会调用,通过your_exit_fun()做自定义的清理工作,比如把文件写回磁盘。

abort() - 该函数被调用通常表示出现了无法处理的异常,直接终止程序。它不像exit()那样做清理工作。


2.使用assert(断言)宏调用,位于头文件中,当程序出错时,就会引发一个abort()。


3.使用errno全局变量,由C运行时库函数提供,位于头文件中。


4.使用goto语句,当出错时跳转。


5.使用setjmp, longjmp进行异常处理。

 

C语言版的try/catch: setjmp, longjmp

不像goto只能实现函数内跳转,setjmp / longjmp一起用可以跨函数跳转。

setjump, longjmp的原理是先通过setjump设置一个跳转点,其实就记录下跳转点的栈信息,然后通过longjmp 来恢复该栈点信息,从而跳转到setjump事先定义的跳转点上执行。

函数定义:

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmp() 接受一个jmp_buf类型参数,当显式调用它时,当前栈信息会被记录入该jmp_buf;而longjmp被调用时,并指定同一个jmp_buf作为参数,就会跳转到该栈点上执行,而第二参数val会变成setjmp第二返回时的返回值!这可能是搞晕人的地方:


为什么setjmp会返回两次?


看它的使用,或者就理解了:

#include 
#include 
#include 

jmp_buf jmpbuffer;

void raise_exception(void)
{
	printf("raise_exception()\n");
	longjmp(jmpbuffer,1);
}

int main(int argc, char *argv[])
{
	if( setjmp(jmpbuffer) ==0 ) //跳转点,等一会跳回这里
	{
		raise_exception();
	}
	else //return from calling longjmp, it returns 1
	{
		printf("catch exception here.\n");
	}

	return 0;
}

输出:

raise_exception()
catch exception here.

上述函数中,在main()里调用setjmp()记录的是调用setjmp()那一刻的信息,因为是主动调用,因此setjmp()返回0;于是继续调用raise_exception() ,假设发生了异常,我们通过longjmp()来跳转到事先设置的“跳转点”,这时,栈信息从jmpbuffer中恢复,发现恢复后其实还是main()的setjmp()这一行代码,但这次返回值是longjmp的第二参数,1.

这类似于LINUX的fork()函数,返回两次,但父子进程返回值不同(难道fork()就是这么实现的?)。

通过setjmp / longjmp甚至可用来实现try / catch这样的异常处理。文献<< Exceptions in C withLongjmp and Setjmp >> 里就展示了这种做法。

 

C++的异常(try/catch)


1. terminate()

上述C语言异常中提到abort(),exit(),C++中也有类似的函数终止程序,即terminate(),它最终调用abort()。同样,它不负责做清理工作。

在C++中,清理工作显的更为普遍:C++引入了类,往往希望类对象析构时能做些事情。而无论是C版的abort(),exit() ,还是C++版的terminate()都无法做到这点。


2. 异常处理机制try/catch

C++引入的异常处理机制使得异常发生时,函数内本地对象可以被析构。其实现机制称为“栈回退”(stackunwind)。当一个函数抛出异常时,在栈中先做个标记,等到该异常被捕捉并处理时,就能顺滕摸瓜找到异常抛出处,并析构异常抛出之前的的对象。对栈回退原理感兴趣的读者可以参阅《C++栈回退》。

以下是一个简单的例子:

//省略了必要的头文件

class A
{
public:
	~A(){printf("~A()\n");}
};

void fun()
{
	A a1;
	throw "fun() exception";
	A a2;
}

int main(int argc, char *argv[])
{
	try
	{
		fun();
		printf("would not come here.\n");
	}
	catch (...)
	{
		printf("catch exception here.\n");
	}
	
	return 0;
}

输出:
~A()
catch exception here.

可看到当异常被处理前,A()中a1被析构,而a2这句没有执行到,所以也不存在析构被调用。

需要注意的是,只有异常被处理了,对应的异常抛出函数才能析构其本地对象。如果一个异常没有被处理,最终系统将调用terminate()终止程序,就像上面说的,这时本地变量将没有机会析构。

 

Windows 结构化异常,以及与C++标准异常的联系(__try/__except与try/catch)

我们在编写软件时会发生异常,比如参数不合法,逻辑错误,程序员都可以自定义异常类型;而硬件其实也会发生异常并由CPU抛出,比如除0操作,内存非法访问等。而程序员只关心软件,能不能在软件中统一的捕获所有软件与硬件异常呢?

答案是肯定的。根据MSDN的解释,Windows结构化异常机制就是用来统一软、硬件异常处理机制的,同时还统一了内核与用户态异常的处理机制。简单理解就是对WINDOWS系统上所有异常来个大一统。

那这和C++标准异常又有什么区别与联系?

我们知道C++中定义了关键字try/catch来处理C++标准异常,而C++标准相对于操作来说,是上层的环境,就像CRT与OS的关系一样,CRT的实现依赖于更底层的OSAPI。所以C++标准异常是基于WINDOWS结构化异常上实现的。从Windows结构化异常这层来看,每种异常都有一个代码号。通过C++throw抛出的标准异常代码号总是0xe06d7363。

WINDOWS结构化异常的处理使用 __try/__except(微软自定义的关键字会加两个下划线做前缀)。

注意:__try{}只保护当前线程,如果在__try{}中又创建了新线程,则新的线程不受保护。

 

在WindowsAPI 与标准C++代码混用的项目中,如何较好的捕捉所有异常?

在一些项目中,经常出现C/C++代码混用的情况。C代码调用了WINDOWS API,C++又实现与项目相关的逻辑。这样的环境中,WINDOWS结构化异常和C++标准异常都会出现,一个正确的异常捕捉方案应该能做到:

1. 能捕获C++异常,并且异常处理时,C++本地对象能够被析构。

2. 能捕获其它WINDOWS结构化异常(即其它没有被条件一捕获的异常)。

 

让我们来看看几种方案:


方案一:总是使用__try / __except

即然说到在WINDOWS平台上,__try/ __except是更底层的,它能同时捕获结构化异常和C++标准异常。只可惜,__try/ __except的栈回退无法正确析构C++对象,因此,用它来代替try/catch处理C++异常不是好方案。

事实上,在VS2010中,如果在__try/ __except块中定义了C++对象,会收到编译错误:

error C2712: Cannot use __try in functions that requireobject unwinding


结论:方案一不能满足条件一。


方案二:使用try / catch 与SetUnhandledException(myFilter)

本方案使用try/catch来捕捉C++异常,利用try/catch处理异常时的栈回退机制,C++对象得以析构,从而满足条件一;

WINDOWS结构化异常因为不会被try/catch捕获,会变成“未处理”异常,利用API SetUnhandledExceptionFilter(myFilter) 捕获其它未处理异常,并在myFilter中处理。

该方案能同时满足上述两个条件,算较好解决问题了。


仍然有问题?

可是,在实际应用,仍然发现有些异常,既没有被try/catch捕获,也没有进入myFilter被处理。一个常见的例子就是CRT非法参数异常。

本来,CRT非法参数异常是能被 SetUnhandledExceptionFilter(myFilter) 捕获的,但自从VS2005开始,这一行为发生了改变:http://blog.kalmbachnet.de/?postid=75

导致很多情况下我们的SetUnhandledExceptionFilter()不起作用。

在VS2010上,我们观察一下CRTinvalid parameter异常没被捕获的情况:

//省略了必要头文件

void raise_invalid_parameter_exception()
{
	char *p=0;
	printf(p);
}

LONG WINAPI myFilter(struct _EXCEPTION_POINTERS *pExp)  
{  
	printf("myFilter\n");  
	return EXCEPTION_EXECUTE_HANDLER;  
}  

int main(int argc, char *argv[])
{
	::SetUnhandledExceptionFilter(myFilter);

	try
	{
		raise_invalid_parameter_exception();
	}
	catch (...)
	{
		printf("catch you");
	}

	printf("finished.\n");
	return 0;
}

上述代码中,我们先设置myFilter作为未捕获异常处理器,看看它能否捕获CRT invalid parameter异常。编译成Release版本(注意DEBUG版的异常处理机制与RELEASE不同,本实验用RELEASE)。

运行,程序弹出0xc0000417错误,该错误代码定义在ntstatus.h中,即“CRT invalid parameter”。

此次运行, catch{}块也没有进入,可见try/catch没能捕捉该异常,而myFilter()也没有进入,main()函数的"finished"也没打印了。

究其原因,是因为CRT也调用了SetUnhandledExceptionFilter(NULL)来处理一些未被处理的CRT异常,而CRT的NULL filter覆盖了用户自定义的filter。


方案二的修正:追加CRTInvalid Parameter异常捕获器

对于CRT Invalid Parameter异常,用户需要单独调用_set_invalid_parameter_handler (yourHandler)来专门捕获该异常,若用户没有该设置,则系统默认调用SetUnhandledExceptionFilter(NULL) 来处理,最终调用abort()结束程序。

此外,还有其它类似的异常需要单独捕获,比如:

                - 纯虚函数调用异常。

                - vector访问越界。不过在VS2010上,这一异常可以被SetUnhandledExceptionFilter捕获。

 

方案三:让try/catch捕获WINDOWS结构化异常

如果不想用SetUnhandledExceptionFilter(),可以打开编译选项,让try/catch支持捕获Windows结构化异常:

VS2010中, Project -> [Project Name] Properties...-> Configuration Properties -> C/C++ -> Code Generation -> Enable C++ Exceptions, 选择Yes with SEH Exception (/EHa)。

这里几个选项很令人费解,这里就不展开了。

只是,实验表明,CRT Invalid Parameter异常仍然无法被try/catch捕获。

 

总结各方案

方案二与方案三作用类似,都可以捕获C++异常与Windows结构化异常,只是CRT的一些异常诸如非法参数(Invalid Parameter),纯虚函数调用,vector访问越界(VS2010上没问题)等,仍然无法被try/catch或是SetUnhandledExceptionFilter()捕获。还需要针对某些特殊异常追加专门处理器,最主要的就是CRT Invalid Parameter 异常,需要_set_invalid_parameter_handler (yourHandler) 特别捕获。

 

 

参考文献

C语言异常处理:  http://www.cnblogs.com/vimsk/archive/2010/12/11/1901698.html


<>  作者:Nicolai M. Josuttis


Exceptions in C with Longjmp and Setjmp:  http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html


C++栈回退:http://baiy.cn/doc/cpp/inside_exception.htm#%E6%A0%88%E5%9B%9E%E9%80%80%EF%BC%88Stack_Unwind%EF%BC%89%E6%9C%BA%E5%88%B6


Structured Exception Handling: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680657%28v=vs.85%29.aspx


"SetUnhandledExceptionFilter" and VC8: http://blog.kalmbachnet.de/?postid=75


 

 

你可能感兴趣的:(Windows 结构化异常与 C / C++异常的一些事)