1、结构化异常处理SEH,是windows系统提供的异常处理机制。
我们可以在自己的程序中添加SEH机制,这样我们的应用程序可以变得更加健壮。使用SEH,我们在编写代码时可以先集中精力完成软件的正常工作流程。也就是说将软件主要功能编写和软件异常处理这两个任务分离开,最后再去处理软件可能遇到的各种错误情况。
2、为了实现SEH,编译器完成了很多的工作。在进入和离开异常处理代码时编译器会插入一些额外的代码,有时这会导致很大的开销。虽然不同的厂商按照不同的方式来实现SEH,但是大部分的编译厂商都遵循了microsoft的语法规则。因此本文我们将介绍Microsoft visualC++编译器规定的语法。
3、SEH包括两个方面的内容:终止处理和异常处理。
4、终止处理程序确保无论一个代码块是如何退出的,都能保证该终止处理程序能够被执行。其语法如下:
- __try
- {
- //被保护的代码。
- }
- __finally//终止处理。
- {
- //终止处理代码。
- }
5、终止程序也分为两个部分:
__try块为中止处理程序要保护的代码。无论程序以何种方式从该块中退出(如return、goto等语句),__finally块都会被执行。
__finally块为终止处理程序块,该块中的代码会在控制流从__try块退出后、程序结束之前被调用。一般用来执行一些清理操作,如释放资源、释放占有的互斥量等。
6、但是上面所说的”无论何种方式“有些绝对。
当在try块中使用了ExitProcess、ExitThread、TerminateProcess、TerminateThread来终止线程或进程时,finally块就不会被调用。要特别注意这些例外,禁止在try块中使用这些函数。
7、下面通过代码给大家讲解终止处理程序的使用:
- __try
- {
- WaitForSingleObject(hMutex,INFINITE);
- if(x==false)
- return-1;
- }
- __finally
- {
- ReleaseMutex(hMutex);
- return -2;
- }
- return 0;
1)可以看到上面的代码在try中首先等待互斥量内核对象被触发,等待成功后互斥量就属于该线程所有。如果此时执行return-1,则该线程结束,就会导致互斥量对象一直被占有的情况的发生,其他线程会一直处于等待状态。这就是所谓的资源泄漏。
2)有了finally块的保护后,当执行return-1程序试图退出try块时,编译器会让finally块在return之前执行,同时在return(其他语句,如goto等语句也一样)语句之前插入一些代码。return的返回值会被保存在一个临时变量中。
3)但上面的代码程序的返回值是多少呢?是-1,还是-2呢?答案是-2。
当编译器在try块中检测到return语句时,虽然会生成一些代码将返回值保存在一个临时变量中。但是由于finally块代码中也有一个return,finally块中return的值会将原来的返回值覆盖。所以函数最终返回-2。
4)这个过程被称为局部展开。
局部展开会在系统因为try块中的代码提前退出时发生。局部展开会导致非常大的额外开销,因为编译器必须插入代码来保证finally块在程序退出之前执行。最理想的情况就是代码控制流正常的离开try块而进入到finally块中,这时的额外开销最小。在x86体系下离开try块正常进入到finally块只需要执行一条指令。
5)因此为了将性能开销降到最低,我们改进了上面的代码:
- __try
- {
- WaitForSingleObject(hMutex,INFINITE);
- if(x==false)
- gotoEndOfTryBlock;
- //一些代码。
- EndOfTryBlock:
- }
- __finally
- {
- ReleaseMutex(hMutex);
- return -2;
- }
- return 0;
在上面改进后的代码中,当在try块中检查到错误发生时不再直接调用return强制退出。而是执行了一条goto语句到try块的末尾。执行此跳转后,控制流从try块中正常退出,直接进入到finally块中。由于没有发生局部展开,编译器不需要插入额外指令,也就没有导致额外的开销。
1)由于goto语句会破坏程序的执行流程,很多书上都再三强调禁止使用goto。
其实我们也没有必要使用goto,因为microsoft提供给我们一个关键字_leave,也可以执行类似的操作。关键字_leave会导致代码执行控制流跳转到try块的末尾,从而代码将正常的从try块进入到finally块中。
2)下面为使用__leave关键字的改进代码:
- __try
- {
- WaitForSingleObject(hMutex,INFINITE);
- if(x==false)
- __leave
- //一些代码。
- }
- __finally
- {
- ReleaseMutex(hMutex);
- return -2;
- }
- return 0;
8、下面我们分别展示两个例子,一个没有使用SEH,而另一个使用SEH,看下它们到底有何差别,通过比较我们也可以更好的知道SEH是如何完成上述工作的。
1)下面是没有使用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;
- }
- //其他工作.......
- returntrue;
- }
相信我们很多人都写过类似上面的代码。我们可以看到,上面的代码中包含了很多错误检查和资源清理的代码。过多的错误代码检查和资源清理工作会使得代码难以阅读,同时也难以编写、修改和维护。
2)现在让我们来通过使用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);
- }
- returntrue;
- }
从上面的代码我们可以看到代码简洁多了。清理工作放在最后执行,且能够保证能得到执行,代码看起来简洁而有序,提高了可读性也有利于以后的维护。
3)注意事项:前面我们介绍了两种会引起finally块执行的情形:
一:从try块正常退出,进入到finally块。
二:局部展开:从try块中提前退出,将程序控制流强制转到finally块。
除了上面的情况外,还有一种情况:全局展开,也会导致finally块被执行。
9、如果要确定到底正常进入finally还是异常退出try块退出,可以调用AbnormalTermination函数来判断:
- BOOL AbnormalTermination();
注意只能在finally块中调用该函数。
当返回true时,表示控制流从try块中正常进入到finally块中。
返回false时,则表示控制流从try块中异常退出,通常是由于执行goto、break、return或continue语句而导致局部展开,或是try块中的代码抛出异常而引起全局展开所致。
10、我们知道,CPU负责捕获类似非法访问内存和以0作除数这样的问题,一旦侦测到这种行为,它会抛出相应的异常,由CPU抛出的异常都是硬件异常。
11、异常处理的语法结构:
__try {
//Guarded body
....
}
__except (exception filter ) {
//Ecxeption handler
....
}
请注意__except关键字,任何时候创建一个try块,后面必须跟一个finall代码块或except代码块。但是try块后不能同时有finally块和except块,也不能同时有多个finally块或except块。不过,却可以将try-finally块嵌套于try-except块中,反过来也可以。
12、系统处理异常的过程:
执行一条CPU命令,如果有异常抛出,则系统确定最里面一层的try代码块。此时判断try代码块里是否有对应的except代码块,若是没有则去寻找外面一层的try代码块;如果有则对过滤程序求值,值为EXCEPTION_EXECUTE_HANDLER或EXECPTION_EXECUTE_SEARCH或EXCEPTION_CONTINUE_EXECUTION中的一种。值分别为1、0、-1。若求的的值是1,则进行全局展开。之后执行except代码块中的代码,继续执行位于except代码块后面的代码。
13、系统如何执行全局展开:
开始进行全局展开,记住那个对过滤程序求值得到的结果是1的try代码块的位置,系统确定最里面一层的try代码块,此时,我们若是找到了能够对异常进行处理的try代码块,则执行except代码块中对异常进行处理的代码;若是没有找到,则应该看try代码块是否有对应的finally代码块。若是有就执行finally代码块中的代码;若是没有,则去寻找外面一层的try代码块。
我们可以通过将return语句置于finally块中以阻止系统完成全局展开,不过最好避免这样做。
14、GetExceptionCode是内在函数,它的返回值表明刚刚发生的异常的类型:
DWORD GetExceptionCode( );
注意:此函数只能在异常过滤程序里(即__except之后的括号里)或者异常处理程序的代码里调用。
与此对应的有:
PEXCEPTION_POINTERS GetExceptionInformation( );
此内在函数返回一个指向EXCEPTION_POINTERS结构的指针。
15、抛出一个软件异常:
VOID RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
CONST ULONG_PTR *pArguments);
dwExceptionCode是所要抛出异常的标识符。HeapAlloc函数对这个参数的设置为STATUS_NO_MEMORY。