SEH 结构化异常处理 工程方法

首先需要区分的是SEH与C++异常处理的异同,它们相同的地方在于都是基于编译程序支持的文法特性,编译器在遇到异常处理的文法时会生成所需的数据结构并插入相关代码。它们的区别在于C++异常处理属于语言特性,虽然大多数编译器也支持了,正如SEH也为很多编译器所实现一样,在MSVC编译器中C++异常处理实现上利用了已经引入到编译程序和Windows操作系统的结构化异常处理的功能。

SEH包含两个主要功能:结束处理和异常处理

结束处理

结束处理的主要作用是确保守护代码段A的代码段G,在A无论以何种方式退出之后,G都可以被执行到。

SEH结束处理示例代码

当你满怀欣喜地走上使用SEH的道路上时,你很快就会摔个鼻青脸肿,因为你极有可能会遇上编译器错误 C2712 ,私自理解下: SEH代码另和对象厮混,而且别用/EHsc编译选项(用/EHa或者别用/EH选项,编译选项的问题看这里)

使用结束处理程序,对try块中的return的情况,也同样适用(即会在finally块执行之后再return)

try块中有return的SEH处理

实际的执行结果是 MessageBox是会被执行的,而__finally块之后的代码是永远不会被执行的。

编译程序是如何保证在try块可以退出之前执行finally块的

编译程序检查源代码时看到在try块中有return语句。这样,编译程序就生成代码将返回值保存在一个编译程序建立的临时变量中。编译程序然后再生成代码来执行finally块中包含的指令,这称为局部展开。更特殊的情况是,由于try块中存在过早退出的代码,从而产生局部展开,导致系统执行finally块中的内容。在finally块中的指令执行之后,编译程序临时变量的值被取出并从函数中返回。 由于局部展开会导致编译器插入比执行正常从try块离开要带来额外的一堆指令,所以应当尽量避免直接从try块中return,更多地从代码逻辑上避免这种写法。

含有goto的情况也会发生局部展开:

goto也会导致局部展开

SEH所擅长的


SEH的价值

    想象一下如果Funcinator函数调用引发无效内存访问,如果没有SEH,在这种情况下,将会给用户显示一个很常见的Application Error对话框。当用户忽略这个错误对话框,该进程就结束了。当这个进程结束(由于一个无效内存访问),信标仍将被占用并且永远不会被释放,这时候,任何等待信标的其他进程中的线程将不会被分配CPU时间。但若将对ReleaseSemaphore的调用放在finally块中,就可以保证信标获得释放,即使某些其他函数会引起内存访问错误。

带continue和break的例子

在continue和break之前都会先执行finally块

返回值dwTemp在程序结束之后将会是14

如果__try和__finally块中同时有return怎么办

在try块中缓存了所return的值的地方,会替换成__finally中所产生的返回值,并最终返回此值。

尽管结束处理程序可以捕捉try块过早退出的大多数情况,但当线程或进程被结束时,它不能引起finally块中的代码执行。当调用ExitThread或ExitProcess时,将立即结束线程或进程,而不会执行finally块中的任何代码。另外,如果由于某个程序调用TerminateThread或TerminateProcess,线程或进程将死掉,finally块中的代码也不执行。某些C运行期函数(例如abort)要调用ExitProcess,也使finally块中的代码不能执行。虽然没有办法阻止其他程序结束你的一个线程或进程,但你可以阻止你自己过早调用ExitThread和TerminateProcess。

利用SEH结束处理来处理复杂的编程问题

先看下原代码

待优化代码

可以看出零乱的错误处理代码拿代码可阅读性很低差。重写之后更易于理解

使用SEH改写的代码

使用SEH改写

鉴于try块中使用return语句会增加系统开销,下面是使用了微软支持的leave的版本

在try块中使用__leave关键字会引起跳转到try块的结尾。可以认为是跳转到try块的右大括号。由于控制流自然地从try块中退出并进入finally块,所以不产生系统开销。

关于finally块的说明

我们已经明确区分了强制执行finally块的两种情况:

• 从try块进入finally块的正常控制流。

• 局部展开:从try块的过早退出(goto、longjump、continue、break、return等)强制控制转移到finally块。

第三种情况,全局展开( global unwind),在发生的时候没有明显的标识,比如发生内存访问违规时( memory access violation )一个全局展开会使finally块执行。

在finally块中可以通过BOOL AbnormalTermination(); 接口判断是否非自然地从try块中退出,局部展开和全局展开均会导致此接口返回TRUE,因为可以避免局部展开,所以,怎么用,程序员可以自己决定。

异常处理

CPU引发的异常(硬件异常),操作系统或者应用程序触发的软件异常(无效内存访问,除0等)发生时,OS会向应用程序提供机会考察异常类型,并能够让应用程序自己来处理异常。相应语法为在__try块后添加 __except(exception filter){} 块

理解异常过滤器和异常处理程序

与结束处理程序不同,异常过滤器和异常处理程序是通过 OS直接执行的,编译程序在计算异常过滤器表达式和执行异常处理程序方面不做什么事。

除零触发硬件异常

异常发生后,系统会定位到except块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果值只能是下面三个标识符之一,这些标识符定义在Windows的Excpt.h中,分别是EXCEPTIONEXECUTEHANDLER   1,EXCEPTIONCONTINUESEARCH 0,EXCEPTIONCONTINUEEXECUTION  -1

windows处理异常的机制

EXCEPTION_EXECUTE_HANDLER和全局展开(主要目的其实是执行finally)

此返回值说明异常过滤器认出了此异常,此时系统执行全局展开并完成向except块中代码的跳转。

except块和finally块

外层函数和FuncORen1函数,由于FuncORen1函数中试图除零产生异常,系统因此获得控制,开始搜索一个与except块相配的try块,由此上溯找到外层函数中的try块。于是系统计算与except块相联的异常过滤器的值,并等待返回值。当系统看到返回值为EXCEPTION_EXECUTE_HANDLER时,就在FuncORen1的finally块中开始一个全局展开。注意这个展开是在系统执行外层函数的except块中的代码之前发生的。对于一个全局展开,系统回到所有未完成的try块的结尾,查找与finally块相配的try块。在这里,系统发现的finally块是FuncORen1中所包含的finally块,执行完之后向上回溯寻找其他未完成try块的finally块,直到系统到达处理异常的try-except块就停止上溯。这里全局展开结束,系统可以执行except块中所包含的代码了。

如果某个finally块中包含了return,则在执行到此finally块中的return时,全局展开马上结束,代码继续向前执行,except块中的代码也不会被执行,就像什么也没发生过一样。具体也参考windows核心编程24.2节

EXCEPTION_CONTINUE_EXECUTION

发现此过滤器值时,系统回到产生异常的指令再执行一次。但需要注意的是如果产生异常的代码由多条汇编指令组成,则再执行会从异常的汇编指令开始,此时就算数据已经恢复正常,此汇编指令仍会出错,所以有可能产生死循环,这种情况要小心。

EXCEPTION_CONTINUE_SEARCH

需要注意的地方可以参考windows核心编程24.4节,嵌套的多个try块含有except块时,不同的过滤器返回值组合会产生奇妙的bug。

GetExceptionCode

GetExceptionCode获取异常类型方便异常处理,且只可在过滤器(except之后的括号中)和异常处理except块中调用,但不能在过滤器函数里面调用。

GetExceptionCode示例

GetExceptionInformation

当一个异常发生时,操作系统要向引起异常的线程的栈里压入三个结构,这三个结构是:
EXCEPTIONRECORD结构、CONTEXT结构和EXCEPTIONPOINTERS结构。EXCEPTIONRECORD结构包含有关已发生异常的独立于cpu的信息,CONTEXT结构包含已发生异常的依赖于cpu的信息。EXCEPTIONPOINTERS结构只有两个数据成员,二者都是指针,分别指向被压入栈的EXCEPTIONRECORD和CONTEXT结构。使用GetExceptionInformation可以得到PEXCEPTION_POINTERS 数据。

需要注意的是,它只能在异常过滤器中调用,因为仅仅在处理异常过滤器时,EXCEPTIONRECORD、CONTEXT和EXCEPTIONPOINTERS才是有效的。一旦控制被转移到异常处理程序,栈中的数据就被删除(当然你也可以将其存在变量中供异常处理程序中调用)

exception_record demo

EXCEPTIONPOINTERS结构的ContextRecord成员指向一个CONTEXT结构(与线程调度相关)。这个结构是依赖于平台的,也就是说,对于不同的CPU平台,这个结构的内容也不一样。本质上,对CPU上每一个可用的寄存器,这个结构相应地包含一个成员。当一个异常被引发时,可以通过检验这个结构的成员找到更多的信息。遗憾的是,为了得到这种可能的好处,要求程序员编写依赖于平台的代码,以确认程序所运行的机器,使用适当的CONTEXT结构。最好的办法是在代码中安排一个#ifdefs指令。Windows支持的不同CPU的CONTEXT结构定义在WinNT.h文件中。

软件异常

除了CPU检测到的异常,还可以在代码中强制引发一个异常。作为对将错误以返回值的形式逐层向上传递的替代方案,引发异常的方式更易于编写和维护。

引发一个软件异常很容易,只需要调用RaiseException函数


未处理异常和C++异常


线程的启动函数BaseProcessStart和BaseThreadStart在执行代码外层都有一层try exception块

BaseProcessStart


BaseThreadStart

所以,当线程引发一个异常,所有过滤器都返回EXCEPTION_CONTINUE_SEARCH时,将会自动调用一个由系统提供的特殊过滤器函数: UnhandledExceptionFilter。

需要注意的是,当一个用户层的函数产生异常时,系统会查找是否有内核方式异常过滤器准备处理这个异常,如果未找到,则其是未处理的。如果异常发生在内核模式,则系统会蓝屏。而不再按用户态的unhandledExceptionFilter方式去处理了。

C++异常与SEH之间的区别与联系

SEH是可用于任何编程语言的操作系统设施,而异常处理只能用于编写C++代码。如果你在编写C++程序,你应该使用C + +异常处理而不是结构化异常处理。理由是C++异常处理是语言的一部分,编译器知道C++类对象是什么。也就是说编译器能够自动生成代码来调用C++对象析构函数,保证对象的清除。

但是也应该知道,Microsoft Visual C++编译器已经利用操作系统的结构化异常处理实现了C++异常处理。所以当你建立一个C++ try块时,编译器就生成一个SEH_try块。一个C++catch测试变成一个SEH异常过滤器,并且catch中的代码变成SEH_except块中的代码。实际上,当你写一条C++ throw语句时,编译器就生成一个对Windows的RaiseException函数的调用。用于throw语句的变量传递给RaiseException作为附加的参数

C++异常处理翻译为对应的SEH代码

首先可以发现RaiseException调用了异常代码0xE06D7363,这个是由开发人员选择的C++软件异常的代码。

当引发了C++异常时,总要使用EXCEPTION_NONCONTINUABLE标志。C++异常不能被重新执行。对于诊断C++异常的过滤器,如果返回EXCEPTION_CONTINUE_EXECUTION,那将是个错误。实际上,我们看一看上面程序段右边函数中的_except过滤器,就会发现它只能计算成EXCEPTION_EXECUTE_HANDLER或EXCEPTION_CONTINUE_SEARCH

最后要指出的是_except过滤器。这个过滤器的用途是将throw变量的数据类型同用于C++ catch语句的变量类型相比较。如果数据类型相同,过滤器返回EXCEPTION_EXECUTE_HANDLER,导致catch块( __except块)中的语句执行。如果数据类型不同,过滤器返回EXCEPTION_CONTINUE_SEARCH,导致catch过滤器上溯要计算的调用树。

可以看出C++语言完全不支持这再恢复执行的异常处理方式,所以自由选择吧

windows核心编程 25.6 中介绍了一种可以在except中识别不同异常代码的方法。

推荐阅读更多精彩内容