《谈谈windows核心编程系列》 结构化异常处理SEH之__finally终止处理

来源:互联网 发布:带着淘宝混异界 编辑:程序博客网 时间:2024/05/16 09:29

转载自:http://blog.csdn.net/ithzhang/article/details/8790567


结构化异常处理SEH:__finally终止处理。

 

  结构化异常处理(Structuredexception handling)简称SEH。是windows系统提供的异常处理机制。促使windows将SEH加入到windows系统的一个关键原因就是:它可以简化操作系统本身的开发工作,同时还让系统更加健壮。

我们当然也可以在我们的程序中添加SEH机制,这样我们的应用程序也可以变得更加健壮。使用SEH,我们在编写代码时可以先集中精力完成软件的正常工作流程。也就是说将软件主要功能编写和软件异常处理这两个任务分离开,最后再去处理软件可能遇到的各种错误情况。

为了实现SEH,编译器完成了很多的工作。在进入和离开异常处理代码时编译器会插入一些额外的代码,有时这会导致很大的开销。后面的文章我会介绍编译器都是做了哪些工作。

虽然不同的厂商按照不同的方式来实现SEH,但是大部分的编译厂商都遵循了microsoft的语法规则。因此本文我们将介绍Microsoft visualC++编译器规定的语法。

SEH包括两个方面的内容:终止处理和异常处理。本文我们介绍终止处理,下一篇文章将介绍异常处理。

 

终止处理

终止处理程序确保无论一个代码块是如何退出的,都能保证该终止处理程序能够被执行。其语法如下:

[cpp] view plaincopy
  1. <span style="font-size:18px;">__try  
  2. {  
  3.    //被保护的代码。  
  4. }  
  5. __finally//终止处理。  
  6. {  
  7.    //终止处理代码。  
  8. }</span>  


注意:try和finally前都有两个下划线。

终止程序也分为两个部分:__try块为中止处理程序要保护的代码。无论程序以何种方式从该块中退出(如return、goto等语句),__finally块都会被执行。

__finally块为终止处理程序块,该块中的代码会在控制流从__try块退出后、程序结束之前被调用。一般用来执行一些清理操作,如释放资源、释放占有的互斥量等。

但是上面所说的”无论何种方式“有些绝对。因为这个世界本来就没有绝对的东西。当在try块中使用了ExitProcess、ExitThread、TerminateProcess、TerminateThread来终止线程或进程时,finally块就不会被调用。要特别注意这些例外,禁止在try块中使用这些函数。

下面通过代码给大家讲解终止处理程序的使用:

[cpp] view plaincopy
  1. <span style="font-size:18px;">__try  
  2. {  
  3.     WaitForSingleObject(hMutex,INFINITE);  
  4.     if(x==false)  
  5.        return-1;  
  6. }  
  7. __finally  
  8. {  
  9.           ReleaseMutex(hMutex);  
  10.       return -2;  
  11. }  
  12. return 0;</span>  


可以看到上面的代码在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块只需要执行一条指令。

因此为了将性能开销降到最低,我们改进了上面的代码:

[cpp] view plaincopy
  1. <span style="font-size:18px;">__try  
  2. {  
  3.       WaitForSingleObject(hMutex,INFINITE);  
  4.     if(x==false)  
  5.      gotoEndOfTryBlock;  
  6.    //一些代码。  
  7.   EndOfTryBlock:  
  8. }  
  9. __finally  
  10. {  
  11.    ReleaseMutex(hMutex);  
  12.     return -2;  
  13. }  
  14. return 0;</span>  


在上面改进后的代码中,当在try块中检查到错误发生时不再直接调用return强制退出。而是执行了一条goto语句到try块的末尾。执行此跳转后,控制流从try块中正常退出,直接进入到finally块中。由于没有发生局部展开,编译器不需要插入额外指令,也就没有导致额外的开销。

 

由于goto语句会破坏程序的执行流程,很多书上都再三强调禁止使用goto。其实我们也没有必要使用goto,因为microsoft提供给我们一个关键字_leave,也可以执行类似的操作。关键字_leave会导致代码执行控制流跳转到try块的末尾,从而代码将正常的从try块进入到finally块中。

下面为使用__leave关键字的改进代码:

[cpp] view plaincopy
  1. <span style="font-size:18px;">__try  
  2. {  
  3.       WaitForSingleObject(hMutex,INFINITE);  
  4.     if(x==false)  
  5.     __leave  
  6.    //一些代码。  
  7. }  
  8. __finally  
  9. {  
  10.    ReleaseMutex(hMutex);  
  11.     return -2;  
  12. }  
  13. return 0;  
  14.  </span>  


本章开头曾介绍过使用SEH可以让程序员将程序正常执行流程和错误处理分开。下面我们分别展示两个例子,一个没有使用SEH,而另一个使用SEH,看下它们到底有何差别,通过比较我们也可以更好的知道SEH是如何完成上述工作的。

下面是没有使用SEH的程序:

[cpp] view plaincopy
  1. <span style="font-size:18px;">bool fun(char* fileName)  
  2. {  
  3.       HANDLEhFile=CreateFile(....);  
  4.       if(hFile==INVALID_HANDLE_VALUE)  
  5.       {  
  6.            returnfalse;  
  7.       }  
  8.       HANDLEhFileMapping=CreateFileMapping(...);  
  9.       if(hFile==NULL)  
  10.       {  
  11.            CloseHandle(hFile);  
  12.            returnfalse;  
  13.    
  14.       }  
  15.       char*p=MapViewOfFile(..);  
  16.       if(p==NULL)  
  17.       {  
  18.            CloseHandle(hFile);  
  19.            CloseHandle(hFileMapping);  
  20.             return false;  
  21.       }  
  22.    
  23.       //其他工作.......  
  24.       returntrue;  
  25. }</span>  


 

相信我们很多人都写过类似上面的代码。我们可以看到,上面的代码中包含了很多错误检查和资源清理的代码。过多的错误代码检查和资源清理工作会使得代码难以阅读,同时也难以编写、修改和维护。

现在让我们来通过使用SEH机制改进上面的代码:

[cpp] view plaincopy
  1. <span style="font-size:18px;">bool fun(char* fileName)  
  2. {  
  3.       HANDLEhFile=INVALID_HANDLE_VALUE;  
  4.       HANDLEhFileMapping=NULL;  
  5.       char*p=NULL;  
  6.       __try  
  7.       {  
  8.            hFile=CreateFile(....);  
  9.            if(hFile==INVALID_HANDLE_VALUE)  
  10.                  __leave;  
  11.             hFileMapping=CreateFileMapping(...);  
  12.            if(hFile==NULL)  
  13.                  __leave;          
  14.            p=MapViewOfFile(..);  
  15.            if(p==NULL)  
  16.                  __leave;  
  17.       //其他代码。  
  18.       }  
  19.       __finally  
  20.       {  
  21.            if(hFile!=INVALID_HANDLE_VALUE)  
  22.                  CloseHandle(hFile);  
  23.            if(hFileMapping)  
  24.                  CloseHandle(hFileMapping);  
  25.    
  26.       }  
  27.       returntrue;  
  28. }</span>  


从上面的代码我们可以看到代码简洁多了。清理工作放在最后执行,且能够保证能得到执行,代码看起来简洁而有序,提高了可读性也有利于以后的维护。

注意事项:前面我们介绍了两种会引起finally块执行的情形:

一:从try块正常退出,进入到finally块。

二:局部展开:从try块中提前退出,将程序控制流强制转到finally块。

除了上面的情况外,还有一种情况:全局展开,也会导致finally块被执行。关于全局展开由于涉及到异常处理的内容,我们将在下一篇文章中详细介绍!

finally的执行都是由上面三种情况之一造成的。

如果要确定到底正常进入finally还是异常退出try块退出,可以调用AbnormalTermination函数来判断:

[html] view plaincopy
  1. <span style="font-size:18px;">BOOL AbnormalTermination();</span>  

注意只能在finally块中调用该函数。当返回true时,表示控制流从try块中正常进入到finally块中。返回false时,则表示控制流从try块中异常退出,通常是由于执行goto、break、return或continue语句而导致局部展开,或是try块中的代码抛出异常而引起全局展开所致。


                                                       如有纰漏,请指正!谢谢!

                                                                    2013、3、11于浙江杭州


原创粉丝点击