Windows结构化异常处理浅析

近期一直被一个问题所困扰,就是写出来的程序老是出现无故崩溃,有的地方自己知道可能有问题,但是有的地方又根本没办法知道有什么问题。更苦逼的事情是,我们的程序是需要7x24服务客户,虽然不需要实时精准零差错,但是总不能出现断线丢失数据状态。故刚好通过处理该问题,找到了一些解决方案,怎么捕获访问非法内存地址或者0除以一个数。从而就遇到了这个结构化异常处理,今就简单做个介绍认识下,方便大家遇到相关问题后,首先知道问题原因,再就是如何解决。废话不多说,下面进入正题。

什么是结构化异常处理

结构化异常处理(structured exception handling,下文简称:SEH),是作为一种系统机制引入到操作系统中的,本身与语言无关。在我们自己的程序中使用SEH可以让我们集中精力开发关键功能,而把程序中所可能出现的异常进行统一的处理,使程序显得更加简洁且增加可读性。

使用SHE,并不意味着可以完全忽略代码中可能出现的错误,但是我们可以将软件工作流程和软件异常情况处理进行分开,先集中精力干重要且紧急的活,再来处理这个可能会遇到各种的错误的重要不紧急的问题(不紧急,但绝对重要)

当在程序中使用SEH时,就变成编译器相关的。其所造成的负担主要由编译程序来承担,例如编译程序会产生一些表(table)来支持SEH的数据结构,还会提供回调函数。

注:

不要混淆SHE和C++ 异常处理。C++ 异常处理再形式上表现为使用关键字catchthrow,这个SHE的形式不一样,再windows Visual C++中,是通过编译器和操作系统的SHE进行实现的。

在所有 Win32 操作系统提供的机制中,使用最广泛的未公开的机制恐怕就要数SHE了。一提到SHE,可能就会令人想起 __try__finally__except 之类的词儿。SHE实际上包含两方面的功能:终止处理(termination handing)异常处理(exception handing)

终止处理

终止处理程序确保不管一个代码块(被保护代码)是如何退出的,另外一个代码块(终止处理程序)总是能被调用和执行,其语法如下:

__try
{
//Guarded body
//...
}
__finally
{
//Terimnation handler
//...
}

__try__finally 关键字标记了终止处理程序的两个部分。操作系统和编译器的协同工作保障了不管保护代码部分是如何退出的(无论是正常退出、还是异常退出)终止程序都会被调用,即__finally代码块都能执行。

try块的正常退出与非正常退出

try块可能会因为returngoto,异常等非自然退出,也可能会因为成功执行而自然退出。但不论try块是如何退出的,finally块的内容都会被执行。

int Func1()
{
cout << __FUNCTION__ << endl;
int nTemp = 0;
__try{
//正常执行
nTemp = 22;
cout << "nTemp = " << nTemp << endl;
}
__finally{
//结束处理
cout << "finally nTemp = " << nTemp << endl;
}
return nTemp;
} int Func2()
{
cout << __FUNCTION__ << endl;
int nTemp = 0;
__try{
//非正常执行
return 0;
nTemp = 22;
cout << "nTemp = " << nTemp << endl;
}
__finally{
//结束处理
cout << "finally nTemp = " << nTemp << endl;
}
return nTemp;
}

结果如下:

Func1
nTemp = 22 //正常执行赋值
finally nTemp = 22 //结束处理块执行 Func2
finally nTemp = 0 //结束处理块执行

以上实例可以看出,通过使用终止处理程序可以防止过早执行return语句,当return语句视图退出try块的时候,编译器会让finally代码块再它之前执行。对于在多线程编程中通过信号量访问变量时,出现异常情况,能顺利是否信号量,这样线程就不会一直占用一个信号量。当finally代码块执行完后,函数就返回了。

为了让整个机制运行起来,编译器必须生成一些额外代码,而系统也必须执行一些额外工作,所以应该在写代码的时候避免再try代码块中使用return语句,因为对应用程序性能有影响,对于简单demo问题不大,对于要长时间不间断运行的程序还是悠着点好,下文会提到一个关键字__leave关键字,它可以帮助我们发现有局部展开开销的代码。

一条好的经验法则:不要再终止处理程序中包含让try块提前退出的语句,这意味着从try块和finally块中移除return,continue,break,goto等语句,把这些语句放在终止处理程序以外。这样做的好处就是不用去捕获哪些try块中的提前退出,从而时编译器生成的代码量最小,提高程序的运行效率和代码可读性。

finally块的清理功能及对程序结构的影响


在编码的过程中需要加入需要检测,检测功能是否成功执行,若成功的话执行这个,不成功的话需要作一些额外的清理工作,例如释放内存,关闭句柄等。如果检测不是很多的话,倒没什么影响;但若又许多检测,且软件中的逻辑关系比较复杂时,往往需要化很大精力来实现繁琐的检测判断。结果就会使程序看起来结构比较复杂,大大降低程序的可读性,而且程序的体积也不断增大。

对应这个问题我是深有体会,过去在写通过COM调用WordVBA的时候,需要层层获取对象、判断对象是否获取成功、执行相关操作、再释放对象,一个流程下来,本来一两行的VBA代码,C++ 写出来就要好几十行(这还得看操作的是几个什么对象)。

下面就来一个方法让大家看看,为什么有些人喜欢脚本语言而不喜欢C++的原因吧。

为了更有逻辑,更有层次地操作 OfficeMicrosoft 把应用(Application)按逻辑功能划分为如下的树形结构

Application(WORD 为例,只列出一部分)
  Documents(所有的文档)
Document(一个文档)
......
  Templates(所有模板)
Template(一个模板)
......
  Windows(所有窗口)
Window
Selection
View
.....
  Selection(编辑对象)
Font
Style
Range
......
  ......

只有了解了逻辑层次,我们才能正确的操纵 Office。举例来讲,如果给出一个VBA语句是:

Application.ActiveDocument.SaveAs "c:\abc.doc"

那么,我们就知道了,这个操作的过程是:

  1. 第一步,取得Application
  2. 第二步,从Application中取得ActiveDocument
  3. 第三步,调用 Document 的函数 SaveAs,参数是一个字符串型的文件名。

这只是一个最简单的的VBA代码了。来个稍微复杂点的如下,在选中处,插入一个书签:

 ActiveDocument.Bookmarks.Add Range:=Selection.Range, Name:="iceman"

此处流程如下:

  1. 获取Application
  2. 获取ActiveDocument
  3. 获取Selection
  4. 获取Range
  5. 获取Bookmarks
  6. 调用方法Add

获取每个对象的时候都需要判断,还需要给出错误处理,对象释放等。在此就给出伪码吧,全写出来篇幅有点长

#define RELEASE_OBJ(obj) if(obj != NULL) \
obj->Realse(); BOOL InsertBookmarInWord(const string& bookname)
{
BOOL ret = FALSE;
IDispatch* pDispApplication = NULL;
IDispatch* pDispDocument = NULL;
IDispatch* pDispSelection = NULL;
IDispatch* pDispRange = NULL;
IDispatch* pDispBookmarks = NULL;
HRESULT hr = S_FALSE; hr = GetApplcaiton(..., &pDispApplication);
if (!(SUCCEEDED(hr) || pDispApplication == NULL))
return FALSE; hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
RELEASE_OBJ(pDispApplication);
return FALSE;
} hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
RELEASE_OBJ(pDispApplication);
return FALSE;
} hr = GetSelection(..., &pDispSelection);
if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
return FALSE;
} hr = GetRange(..., &pDispRange);
if (!(SUCCEEDED(hr) || pDispRange == NULL)){
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
return FALSE;
} hr = GetBookmarks(..., &pDispBookmarks);
if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
RELEASE_OBJ(pDispRange);
return FALSE;
} hr = AddBookmark(...., bookname);
if (!SUCCEEDED(hr)){
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
RELEASE_OBJ(pDispRange);
RELEASE_OBJ(pDispBookmarks);
return FALSE;
}
ret = TRUE;
return ret;

这只是伪码,虽然也可以通过goto减少代码行,但是goto用得不好就出错了,下面程序中稍不留神就goto到不该取得地方了。

BOOL InsertBookmarInWord2(const string& bookname)
{
BOOL ret = FALSE;
IDispatch* pDispApplication = NULL;
IDispatch* pDispDocument = NULL;
IDispatch* pDispSelection = NULL;
IDispatch* pDispRange = NULL;
IDispatch* pDispBookmarks = NULL;
HRESULT hr = S_FALSE; hr = GetApplcaiton(..., &pDispApplication);
if (!(SUCCEEDED(hr) || pDispApplication == NULL))
goto exit6; hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
goto exit5;
} hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
goto exit4;
} hr = GetSelection(..., &pDispSelection);
if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
goto exit4;
} hr = GetRange(..., &pDispRange);
if (!(SUCCEEDED(hr) || pDispRange == NULL)){
goto exit3;
} hr = GetBookmarks(..., &pDispBookmarks);
if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
got exit2;
} hr = AddBookmark(...., bookname);
if (!SUCCEEDED(hr)){
goto exit1;
} ret = TRUE;
exit1:
RELEASE_OBJ(pDispApplication);
exit2:
RELEASE_OBJ(pDispDocument);
exit3:
RELEASE_OBJ(pDispSelection);
exit4:
RELEASE_OBJ(pDispRange);
exit5:
RELEASE_OBJ(pDispBookmarks);
exit6:
return ret;

此处还是通过SEH的终止处理程序来重新该方法,这样是不是更清晰明了。

BOOL InsertBookmarInWord3(const string& bookname)
{
BOOL ret = FALSE;
IDispatch* pDispApplication = NULL;
IDispatch* pDispDocument = NULL;
IDispatch* pDispSelection = NULL;
IDispatch* pDispRange = NULL;
IDispatch* pDispBookmarks = NULL;
HRESULT hr = S_FALSE; __try{
hr = GetApplcaiton(..., &pDispApplication);
if (!(SUCCEEDED(hr) || pDispApplication == NULL))
return FALSE; hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
return FALSE;
} hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
return FALSE;
} hr = GetSelection(..., &pDispSelection);
if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
return FALSE;
} hr = GetRange(..., &pDispRange);
if (!(SUCCEEDED(hr) || pDispRange == NULL)){
return FALSE;
} hr = GetBookmarks(..., &pDispBookmarks);
if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
return FALSE;
} hr = AddBookmark(...., bookname);
if (!SUCCEEDED(hr)){
return FALSE;
} ret = TRUE;
}
__finally{
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
RELEASE_OBJ(pDispRange);
RELEASE_OBJ(pDispBookmarks);
}
return ret;

这几个函数的功能是一样的。可以看到在InsertBookmarInWord中的清理函数(RELEASE_OBJ)到处都是,而InsertBookmarInWord3中的清理函数则全部集中在finally块,如果在阅读代码时只需看try块的内容即可了解程序流程。这两个函数本身都很小,可以细细体会下这两个函数的区别。

关键字 __leave

try块中使用__leave关键字会使程序跳转到try块的结尾,从而自然的进入finally块。

对于上例中的InsertBookmarInWord3try块中的return完全可以用__leave 来替换。两者的区别是用return会引起try过早退出系统会进行局部展开而增加系统开销,若使用__leave就会自然退出try块,开销就小的多。

BOOL InsertBookmarInWord4(const string& bookname)
{
BOOL ret = FALSE;
IDispatch* pDispApplication = NULL;
IDispatch* pDispDocument = NULL;
IDispatch* pDispSelection = NULL;
IDispatch* pDispRange = NULL;
IDispatch* pDispBookmarks = NULL;
HRESULT hr = S_FALSE; __try{
hr = GetApplcaiton(..., &pDispApplication);
if (!(SUCCEEDED(hr) || pDispApplication == NULL))
__leave; hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL))
__leave; hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL))
__leave; hr = GetSelection(..., &pDispSelection);
if (!(SUCCEEDED(hr) || pDispSelection == NULL))
__leave; hr = GetRange(..., &pDispRange);
if (!(SUCCEEDED(hr) || pDispRange == NULL))
__leave; hr = GetBookmarks(..., &pDispBookmarks);
if (!(SUCCEEDED(hr) || pDispBookmarks == NULL))
__leave; hr = AddBookmark(...., bookname);
if (!SUCCEEDED(hr))
__leave; ret = TRUE;
}
__finally{
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
RELEASE_OBJ(pDispRange);
RELEASE_OBJ(pDispBookmarks);
}
return ret;
}

##异常处理程序

软件异常是我们都不愿意看到的,但是错误还是时常有,比如CPU捕获类似非法内存访问和除0这样的问题,一旦侦查到这种错误,就抛出相关异常,操作系统会给我们应用程序一个查看异常类型的机会,并且运行程序自己处理这个异常。异常处理程序结构代码如下

  __try {
// Guarded body
}
__except ( exception filter ) {
// exception handler
}

注意关键字__except,任何try块,后面必须更一个finally代码块或者except代码块,但是try后又不能同时有finallyexcept块,也不能同时有多个finnalyexcept块,但是可以相互嵌套使用

异常处理基本流程

int Func3()
{
cout << __FUNCTION__ << endl;
int nTemp = 0;
__try{
nTemp = 22;
cout << "nTemp = " << nTemp << endl;
}
__except (EXCEPTION_EXECUTE_HANDLER){
cout << "except nTemp = " << nTemp << endl;
}
return nTemp;
} int Func4()
{
cout << __FUNCTION__ << endl;
int nTemp = 0;
__try{
nTemp = 22/nTemp;
cout << "nTemp = " << nTemp << endl;
}
__except (EXCEPTION_EXECUTE_HANDLER){
cout << "except nTemp = " << nTemp << endl;
}
return nTemp;
}

结果如下:

Func3
nTemp = 22 //正常执行 Func4
except nTemp = 0 //捕获异常,

Func3try块只是一个简单操作,故不会导致异常,所以except块中代码不会被执行,Func4try块视图用22除0,导致CPU捕获这个事件,并抛出,系统定位到except块,对该异常进行处理,该处有个异常过滤表达式,系统中有三该定义(定义在Windows的Excpt.h中):

1. EXCEPTION_EXECUTE_HANDLER:
我知道这个异常了,我已经写了代码来处理它,让这些代码执行吧,程序跳转到except块中执行并退出
2. EXCEPTION_CONTINUE_SERCH
继续上层搜索处理except代码块,并调用对应的异常过滤程序
3. EXCEPTION_CONTINUE_EXECUTION
返回到出现异常的地方重新执行那条CPU指令本身

面是两种基本的使用方法:

  • 方式一:直接使用过滤器的三个返回值之一
__try {
……
}
__except ( EXCEPTION_EXECUTE_HANDLER ) {
……
}
  • 方式二:自定义过滤器
__try {
……
}
__except ( MyFilter( GetExceptionCode() ) )
{
……
} LONG MyFilter ( DWORD dwExceptionCode )
{
if ( dwExceptionCode == EXCEPTION_ACCESS_VIOLATION )
return EXCEPTION_EXECUTE_HANDLER ;
else
return EXCEPTION_CONTINUE_SEARCH ;
}

.NET4.0中捕获SEH异常

在.NET 4.0之后,CLR将会区别出一些异常(都是SEH异常),将这些异常标识为破坏性异常(Corrupted State Exception)。针对这些异常,CLR的catch块不会捕捉这些异常,一下代码也没有办法捕捉到这些异常。

try{
//....
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}

因为并不是所有人都需要捕获这个异常,如果你的程序是在4.0下面编译并运行,而你又想在.NET程序里捕捉到SEH异常的话,有两个方案可以尝试:

  • 在托管程序的.config文件里,启用legacyCorruptedStateExceptionsPolicy这个属性,即简化的.config文件类似下面的文件:
App.Config

<?xml version="1.0"?>
<configuration>
 <startup>
 <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
 </startup>
<runtime>
<legacyCorruptedStateExceptionsPolicy enabled="true" />
</runtime>
</configuration>

这个设置告诉CLR 4.0,整个.NET程序都要使用老的异常捕捉机制。

  • 在需要捕捉破坏性异常的函数外面加一个HandleProcessCorruptedStateExceptions属性,这个属性只控制一个函数,对托管程序的其他函数没有影响,例如:
[HandleProcessCorruptedStateExceptions]
try{
//....
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
上一篇:Jvm(jdk8)源码分析1-java命令启动流程详解


下一篇:深入研究 Win32 结构化异常处理(作者博客有许多SEH的研究文章)