结构化异常处理(Structuredexception handling)简称SEH。是windows系统提供的异常处理机制。促使windows将SEH加入到windows系统的一个关键原因就是:它可以简化操作系统本身的开发工作,同时还让系统更加健壮。
我们当然也可以在我们的程序中添加SEH机制,这样我们的应用程序也可以变得更加健壮。使用SEH,我们在编写代码时可以先集中精力完成软件的正常工作流程。也就是说将软件主要功能编写和软件异常处理这两个任务分离开,最后再去处理软件可能遇到的各种错误情况。
为了实现SEH,编译器完成了很多的工作。在进入和离开异常处理代码时编译器会插入一些额外的代码,有时这会导致很大的开销。后面的文章我会介绍编译器都是做了哪些工作。
虽然不同的厂商按照不同的方式来实现SEH,但是大部分的编译厂商都遵循了microsoft的语法规则。因此本文我们将介绍Microsoft visualC++编译器规定的语法。
SEH包括两个方面的内容:终止处理和异常处理。本文我们介绍终止处理,下一篇文章将介绍异常处理。
终止处理
终止处理程序确保无论一个代码块是如何退出的,都能保证该终止处理程序能够被执行。其语法如下:
__try
{
//被保护的代码。
}
__finally//终止处理。
{
//终止处理代码。
}
注意:try和finally前都有两个下划线。
终止程序也分为两个部分:__try块为中止处理程序要保护的代码。无论程序以何种方式从该块中退出(如return、goto等语句),__finally块都会被执行。
__finally块为终止处理程序块,该块中的代码会在控制流从__try块退出后、程序结束之前被调用。一般用来执行一些清理操作,如释放资源、释放占有的互斥量等。
但是上面所说的”无论何种方式“有些绝对。因为这个世界本来就没有绝对的东西。当在try块中使用了ExitProcess、ExitThread、TerminateProcess、TerminateThread来终止线程或进程时,finally块就不会被调用。要特别注意这些例外,禁止在try块中使用这些函数。
下面通过代码给大家讲解终止处理程序的使用:
__try
{
WaitForSingleObject(hMutex,INFINITE);
if(x==false)
return-1;
}
__finally
{
ReleaseMutex(hMutex);
return -2;
}
return 0;
可以看到上面的代码在try中首先等待互斥量内核对象被触发,等待成功后互斥量就属于该线程所有。如果此时执行return-1,则该线程结束,就会导致互斥量对象一直被占有的情况的发生,其他线程会一直处于等待状态。这就是所谓的资源泄漏。有了finally块的保护后,当执行return-1程序试图退出try块时,编译器会让finally块在return之前执行,同时在return(其他语句,如goto等语句也一样)语句之前插入一些代码。return的返回值会被保存在一个临时变量中。
但上面的代码程序的返回值是多少呢?是-1,还是-2呢?答案是-2。当编译器在try块中检测到return语句时,虽然会生成一些代码将返回值保存在一个临时变量中。但是由于finally块代码中也有一个return,finally块中return的值会将原来的返回值覆盖。所以函数最终返回-2。
这个过程被称为局部展开。局部展开会在系统因为try块中的代码提前退出时发生。局部展开会导致非常大的额外开销,因为编译器必须插入代码来保证finally块在程序退出之前执行。最理想的情况就是代码控制流正常的离开try块而进入到finally块中,这时的额外开销最小。在x86体系下离开try块正常进入到finally块只需要执行一条指令。
因此为了将性能开销降到最低,我们改进了上面的代码:
__try
{
WaitForSingleObject(hMutex,INFINITE);
if(x==false)
goto EndOfTryBlock;
//一些代码。
EndOfTryBlock:
}
__finally
{
ReleaseMutex(hMutex);
return -2;
}
return 0;
在上面改进后的代码中,当在try块中检查到错误发生时不再直接调用return强制退出。而是执行了一条goto语句到try块的末尾。执行此跳转后,控制流从try块中正常退出,直接进入到finally块中。由于没有发生局部展开,编译器不需要插入额外指令,也就没有导致额外的开销。
由于goto语句会破坏程序的执行流程,很多书上都再三强调禁止使用goto。其实我们也没有必要使用goto,因为microsoft提供给我们一个关键字_leave,也可以执行类似的操作。关键字_leave会导致代码执行控制流跳转到try块的末尾,从而代码将正常的从try块进入到finally块中。
下面为使用__leave关键字的改进代码:
__try
{
WaitForSingleObject(hMutex,INFINITE);
if(x==false)
__leave
//一些代码。
}
__finally
{
ReleaseMutex(hMutex);
return -2;
}
return 0;
本章开头曾介绍过使用SEH可以让程序员将程序正常执行流程和错误处理分开。下面我们分别展示两个例子,一个没有使用SEH,而另一个使用SEH,看下它们到底有何差别,通过比较我们也可以更好的知道SEH是如何完成上述工作的。
下面是没有使用SEH的程序:
bool fun(char* fileName)
{
HANDLEhFile=CreateFile(....);
if(hFile==INVALID_HANDLE_VALUE)
{
returnfalse;
}
HANDLEhFileMapping=CreateFileMapping(...);
if(hFile==NULL)
{
CloseHandle(hFile);
returnfalse;
}
char*p=MapViewOfFile(..);
if(p==NULL)
{
CloseHandle(hFile);
CloseHandle(hFileMapping);
return false;
}
//其他工作.......
return true;
}
相信我们很多人都写过类似上面的代码。我们可以看到,上面的代码中包含了很多错误检查和资源清理的代码。过多的错误代码检查和资源清理工作会使得代码难以阅读,同时也难以编写、修改和维护。
现在让我们来通过使用SEH机制改进上面的代码:
bool fun(char* fileName)
{
HANDLEhFile=INVALID_HANDLE_VALUE;
HANDLEhFileMapping=NULL;
char*p=NULL;
__try
{
hFile=CreateFile(....);
if(hFile==INVALID_HANDLE_VALUE)
__leave;
hFileMapping=CreateFileMapping(...);
if(hFile==NULL)
__leave;
p=MapViewOfFile(..);
if(p==NULL)
__leave;
//其他代码。
}
__finally
{
if(hFile!=INVALID_HANDLE_VALUE)
CloseHandle(hFile);
if(hFileMapping)
CloseHandle(hFileMapping);
}
return true;
}
从上面的代码我们可以看到代码简洁多了。清理工作放在最后执行,且能够保证能得到执行,代码看起来简洁而有序,提高了可读性也有利于以后的维护。
注意事项:前面我们介绍了两种会引起finally块执行的情形:
一:从try块正常退出,进入到finally块。
二:局部展开:从try块中提前退出,将程序控制流强制转到finally块。
除了上面的情况外,还有一种情况:全局展开,也会导致finally块被执行。关于全局展开由于涉及到异常处理的内容,我们将在下一篇文章中详细介绍!
finally的执行都是由上面三种情况之一造成的。
如果要确定到底正常进入finally还是异常退出try块退出,可以调用AbnormalTermination()函数来判断:
注意只能在finally块中调用该函数。当返回true时,表示控制流从try块中正常进入到finally块中。返回false时,则表示控制流从try块中异常退出,通常是由于执行goto、break、return或continue语句而导致局部展开,或是try块中的代码抛出异常而引起全局展开所致。