走进SEH(Structured Exception Handling)
最近手头上工作比较轻松,于是继续充点电。本系列主要从核心编程里面学来,同时外加网上的搜集整理得来。文章主要以应用为主,所以对已经了解的人可能不会有太大帮助,但学习的总结分享出来总会有其用武之地。如发现内容表述有误,请指正。由于篇幅较长,为便于阅读故斩其首分上中下以述之~
SEH("Structured Exception Handling"),即结构化异常处理,是Windows操作系统提供给程序设计者的强有力的处理程序错误或异常的武器。促使微软将SHE机制引入Windows系统的因素之一是它可以简化操作系统本身的开发工作,操作系统的开发人员使用SHE来让系统更加健壮,而我们可以使用SHE来强化我们的应用程序,其也是微软对C++的扩展。
SEH主要关键词包括:__try、__finnally、__except、__leave。
让编译器支持SHE并不是一件简单的任务,为了让其运作起来,编译器需要额外做一些工作。比如在进入和离开异常处理代码块时,需要实现局部展开、全局展开等工作,不同的编译器也会以不同的方式来实现它,但大部分编译器都遵循了Microsoft建议的语法。本次讨论均是以VC++规定的语法。
SEH和C++标准的异常处理是不同的后面会解释它们之间的关系。除了在语法的表现形式上不同外,值得注意的是在VC++中对C++异常处理支持上其实是利用了编译器和Windows的SEH功能。
SHE包含了两方面的功能:终止处理(termination handling)和异常处理(exception handling)。
下面先感性的认识下它们的语法形式:
//code
}
__finnally{
//终止处理程序
}
__try{
//code
}
__except(//异常过滤程序){
//异常处理程序
}
第一篇 终止处理
先来看看微软是对try-finally怎么解释的:
The try-finally statement is a Microsoft extension to the C and C++ languages that enables 32-bit target applications toguarantee execution of cleanup code when execution of a block of code is interrupted.Cleanup consists of such tasks as deallocating memory, closing files, and releasing file handles. The try-finally statement is especially useful for routines that have several places where a check is made for an error that could cause premature return from the routine.
即终止处理就是保证应用程序在一段被保护的代码发生中断后(无论是异常还是其他)还能够执行清理工作,清理工作包括关闭文件、清理内存等。先来简单的看段代码,了解其特性:
通过结果可以看出就算是return(以及其他goto等),__finally最后也还会执行。这里面会发生局部展开。在try块中,return时会创建一个临时变量用于保存返回结果,然后局部展开进入finally代码块执行,return时会将“2013”写入返回结果的临时变量中,因此最终结果是2013,而不是1.
局部展开
局部展开(local unwind):指某个函数中的__try块中的代码因为执行了return,break,goto,continue等指令使得指令流要提前流出__try块,这时编译器就会生成一些代码以跳转执行__finally块中的代码,以保证__finally语义的正确性,执行完__finally里面的代码后程序指针会回到原来的位置(除非__finally程序块存在return等导致函数提前结束)继续执行,这个过程好像是在__try的return(等)处展开了__finally的代码。
可以看出局部展开会先执行finally中的语句,然后再执行goto跳转的代码段。
注:
“不管在何种情况下,在离开当前的作用域时,finally块区域内的代码都将会被执行到”,这是核心法则。
当然,这也不完全是绝对的法则,假如__try语句或是外部出现了ExitProcess/ExitThread等终止线程或进程时,以及其他的一些异常导致SHE链的中断,终止处理程序也不会得到执行。
注:
在__try/__finally结构中,因为局部展开会产生额外的开销,所以在__try/__finally中应该避免使用return/goto等语句。
当需要提前退出__try块时请使用关键词__leave代替return等,__leave会直接将代码跳转至__try的结尾,这种情况下将不会产生额外的开销。
这就是__leave关键字的作用,也许大家在编程时很少使用它。但是请注意,如果你的程序中,尤其在那些业务特别复杂的函数模块中,既采用了SEH机制来保证程序的可靠性,同时代码中又拥有大量的goto语句和return语句的话,那么你的源代码编译出来的二进制程序将是十分糟糕的,不仅十分庞大,而且效率也受很大影响。此时,建议不妨多用__leave关键字来提高程序的性能。
AbnormalTermination函数
Finally块的执行总是由以下3中状况引起的:
(1) 正常流出__try语句进入__finally;
(2) 被保护代码发生异常中断而导致的全局展开进入__finally;
(3) 由于return、goto等语句导致的局部展开进入__finally;
内在函数AbnormalTermination()函数可以判断是由哪种方式进入__finally块中的。该函数只能在__finally里面调用,当情况(1)的时候返回FALSE,其他两种非正常情况返回TRUE。
内在函数是由编译器所识别并处理的特殊函数,编译器会为这个函数生成内联代码,比如memecpy等。
第二篇 异常处理程序与软件异常
在程序的设计中,难免会碰到访问非法内存地址以及除数为0等异常错误,CPU负责捕获,一旦侦测到这些错误行为,它会抛出相应的异常,由CPU抛出的异常都是硬件异常;同样,操作系统和应用程序也可以抛出异常,这些异常通常被称为软件异常。语法结构为:
异常处理
__try{
//code
}
__except(//异常过滤程序){
//异常处理程序
}
该结构和C++标准里面的try-catch有些类似,但catch 和 except 有一点不同: catch关键字后面往往接受一个函数参数一样,可以是各种类型的异常数据对象;但是__except关键字则不同,它后面跟的却是一个表达式(亦称为异常过滤程序),该段只能为表达式或函数调用,返回值必须是以下3种之一:
#define EXCEPTION_EXECUTE_HANDLER 1 // 执行异常处理程序,触发全局展开
#define EXCEPTION_CONTINUE_SEARCH 0 // 不执行,继续查找外层(上一级)的try块
#define EXCEPTION_CONTINUE_EXECUTION -1 // 不会触发全局展开,继续返回异常处重新执行
注意:一个__try块后面只能跟一个__finally或者__except,不能为多个,而且二者不能同时存在。但是__try/__finally倒是可以喝__try/__except互相嵌套。
先简单看一个例子来了解其用法与特性:
假如没有try-except块,上面的代码由于访问不合法内存会直接导致应用程序的终止,但是SHE能够帮助我们捕获异常并允许我们处理它而不是直接的导致程序的终止。根据上面的结果我们很容易分析该程序的流程。__except只有在发生异常后才会进入判断并执行,否则该代码永远也不会进入,这点跟__finally是完全不同的,__finlly是几乎在所有情况下均会进入执行。
注:在__try/__finally块中我们不建议使用return、goto等,这样会导致触发局部展开程序性能损失或增加代码量;但是在__try/__except中则不会产生局部展开这样的额外开销,(因为局部展开就是为了执行__finally,这个解释还是很给力的吧…),但还是不建议使用return等,因为这会导致代码逻辑变乱。虽然这里不会产生局部展开,但确会发生全局展开。
全局展开当异常过滤程序的计算结果为EXCEPTION_EXECUTE_HANDLER时,系统必须执行全局展开。全局展开导致执行其内部嵌套的尚未完成的__try/__finally块。简单的说就是发生异常后,先找到外层的能够处理该异常的__try/__except块,然后再由内向外执行还未完成的__finally程序块。
说再多都不如看代码更直接,看个更典型的例子:注意代码中标示出的执行流程。这一切顺序说明了发生异常后先执行异常过滤程序找到EXCEPTION_EXECUTE_HANDLER处(异常处理程序入口),然后再全局展开执行尚未执行的__finally程序,最后进入异常处理程序。
如下图为全局展开的流程:
停止全局展开
当全局展开发生后,若某一个__finally块中存在return语句,则会导致后续的全局展开终止,即程序会像什么异常都没发生过一样继续执行。看下面一个例子:结果发现全局展开被中断了,在发生异常后面的语句被执行了,SubTestFunc()函数被当做正常返回而结束。很显然这会带来很严重的后果,因为有可能后面的程序会因为异常而发生未知的错误,程序执行变得无法控制,这也是我们为什么强调不在__finally内部使用return等语句的原因。如果在__try-finally块内使用return等语句,编译器也会产生warning:
warning C4532: 'return' : jump out of __finally block has undefined behavior during termination handling。
EXCEPTION_CONTINUE_EXECUTION其他两个异常过滤返回值很容易理解,上面的例子中也有所涉及,而EXCEPTION_CONTINUE_EXECUTION的用法却是很危险的。举个例子:
这说明程序修复异常后还是会发生异常。我们调试下看看编译器是怎么完成该过程的:
这说明过滤程序确实按照期望完成了p指针的重新分配内存,可为什么还会继续发生异常呢?这就追溯到异常的发生处:
原来编译器现将*p的传到寄存器ecx中(即为null),然后再向该寄存器中代表的地址赋值,因而导致访问非法内存抛出异常。在filter过滤函数返回后仍返回了该语句(而不是重新将*p内容传到ecx中),这样当然还会发生异常,因为改变后的p实际并没有生效。这也就解释了为什么会出现上面的结果。
总之,应该慎用EXCEPTION_CONTINUE_EXECUTION。合理的运用SHE可以写出高效的代码。
GetExceptionCode函数
该函数能够返回所捕获的异常为何种异常,其必须在__except之后的括号内部使用,甚至不能在上面例子中的Filter函数中调用,否则会出现编译错误。具体用法以及返回值请参考msdn。
GetExceptionInformation()略。
软件异常
之前我们讨论的都是硬件异常,有的时候我们希望在应用程序代码里面强制抛出一个自己定义的异常(可能仅仅是逻辑上的不合法),然后运用异常机制来捕获并处理。
比如传统上,我们通过函数的返回值来标示函数的失败,然后不断地将返回值向调用者一层一层往外抛。这种传播导致代码很难写也很难维护。而若是函数在失败时不是返回错误码,而是直接抛出异常,这样就可以利用SHE机制省略了很多的错误检测代码,程序的效率也会更高。
Microsoft为我们提供了一个接口函数用以完成这个功能:RaiseException。以上示例展示了自定义软件异常的用法,相信大家已经基本明白这个流程是怎么实现的了。软件异常的用法有很多,比如我们可能想给系统的时间日志发送通知消息,一旦程序中的某个函数发生了问题,我们便可以抛出软件异常,并在异常处理处写入到事件的日志中。我们还可能需要利用软件异常来传递应用程序的严重错误信息。