WIN32汇编语言教程:第14章 异常处理 · 14.3 使用SEH处理异常(4)
对回调函数的这种调用是由展开操作(Unwinding)引起的。当SEH链上的某个回调函数进行展开操作时,它所做的事情是从SEH链上的第一个回调函数开始(也就是fs:[0]指定的回调函数),以EXCEPTION_UNWIND代码和EXCEPTION_UNWINDING标志去调用每个回调函数,一直到调用到自身所处的位置为止,然后将自身之前的所有回调函数卸载,也就是将fs:[0]直接指向描述自身位置的那个EXCEPTION_REGISTRATION结构。当进行展开操作后,发起展开操作的那个回调函数将成为SEH链上的第一个回调函数。
1. 为什么要进行展开操作
展开操作在某些情况下是必要的。原因之一是让被卸载的回调函数有机会进行扫尾操作;原因之二是为了防止某些异常情况的发生,这个原因分析起来要复杂一些。
为了程序的模块化设计,一般在堆栈中构造EXCEPTION_REGISTRATION结构来注册SEH异常处理回调函数,这种方法已经成为各种语言注册SEH异常处理程序的首选,然而,与将结构定义成全局变量相比,这种方法又带来了一个新的问题。
图14.4 堆栈中的SEH链存在情况
现在来看看一个典型的应用,如图14.4所示,假设在主程序中调用_Proc1子程序来实现某种功能,这个子程序将涉及内存操作,所以设置了一个回调函数来处理内存访问异常,在_Proc1中又会调用_Proc2子程序来对所分配的内存中的数据进行一些运算,为了检测计算中的溢出错误,_Proc2设置了一个回调函数来处理溢出或除零异常,对其他的异常将不予处理并让它在SEH链中继续传递。
当程序执行到_Proc2中间的时候,堆栈中的数据如图14.4的右边所示,最下方是_Proc2注册异常处理回调函数使用的EXCEPTION_REGISTRATION结构,现在fs:[0]指向这个结构,上面一点是_Proc2使用的局部变量、返回地址等,再上面就是注册_Proc1中异常处理回调函数使用的EXCEPTION_REGISTRATION结构了。
当_Proc2中发生溢出异常时,_Proc2的回调函数将程序修正到Safe2执行,在这里堆栈被修正到如图14.4中的B所示的位置。到_Proc2返回的时候,回调函数被卸掉,堆栈中的后一个EXCEPTION_REGISTRATION结构被丢弃且fs:[0]被恢复指向_Proc1设置的结构中,一切都很正常。
但是在_Proc2中发生内存存取异常的时候,问题就出现了,这时系统根据fs:[0]的值首先找到并调用_Proc2设置的回调函数,但这个回调函数不处理这种异常,它会要求Windows继续搜索,接下来_Proc1设置的回调函数被调用,在这里堆栈被修正到图14.4的A所示的位置,这是执行到Safe1位置时正确的堆栈位置。
问题就在这里,这时候esp指向A,在A位置以下的堆栈空间都是自由的,包括A和B之间的堆栈空间,如果_Proc1接下来进行了一些入栈出栈的操作,原先由_Proc2设置的EXCEPTION_REGISTRATION结构就会被冲掉,不要忘了这时fs:[0]还指向这个失效的结构,如果这时再发生一个异常的话,Windows就会调用一个无效的回调函数地址。
这就是要进行展开操作的第二个原因,为了防止这个意外,_Proc2的异常处理回调函数在被执行的时候应该将fs:[0]中的值重新置为本身使用的EXCEPTION_REGISTRATION结构的地址,这样即使再次发生异常,也不会有前面这种危险的情况发生,这个操作相当于将后面所设的所有异常处理回调函数都卸掉了。
2. 完整的异常处理回调函数
综上所述,一个完整的异常处理回调函数应该包括异常处理、展开操作和响应展开调用等部分,其结构示意如下:
_Handler proc _lpExceptionRecord,_lpSEH,\
_lpContext,_lpDispatcherContext
.if (异常代码 == 0c0000027h) || \
(异常标志 & EXCEPTION_UNWINDING) || \
(异常标志 & EXCEPTION_UNWINDING_FOR_EXIT)
进行释放资源等扫尾工作 ;(1)
mov eax,ExceptionContinueSearch
.elseif 异常代码 == 可以处理的异常代码
处理异常,对CONTEXT进行修正 ;(2)
进行展开操作 ;(3)
mov eax,ExceptionContinueExecution
.else ;其他无法处理的异常代码
mov eax,ExceptionContinueSearch ;(4)
.endif
ret
_Handler endp
但是在实际的应用中,并不一定要存在上面所示的全部代码,如果某个异常处理回调函数对所有的异常代码都进行处理的话,那么就不会有(4)所示的代码,这样在它以后的回调函数就不可能再被调用,这样一来,这个回调函数也不可能被其他回调函数以展开操作的异常代码调用,结果是(1)所示的代码也就不需要了。
另外,当回调函数能够确定自己是SEH链上的最后一个回调函数的话,由于不存在展开操作的对象,也就不需要(3)所示的代码。
在本章的SEH例子中,程序设置的回调函数既是最后一个异常处理回调函数,又对所有的异常代码进行处理并将程序转移到“安全地址”去执行,所以仅仅需要(2)所示的代码。
3. 如何进行展开操作
自己书写展开操作的代码并不复杂,第一步是在一个循环中以EXCEPTION_UNWINDING标志调用从fs:[0]开始到当前回调函数为止的所有回调函数,第二步是将fs:[0]重新设置一下,指向注册当前回调函数使用的EXCEPTION_REGISTRATION结构就可以了。
但是,更方便的办法是使用Win32中未公开的函数RtlUnwind,这个函数可以完成上述的功能,函数的使用方法如下所示:
invoke RtlUnwind,lpLastStackFrame,lpCodeLabel,lpExceptionRecord,dwRet
使用参数lpLastStackFrame可以有两种方法。
第一,将它指定为当前回调函数使用的EXCEPTION_REGISTRATION结构地址的话,表示对当前回调函数之后的所有其他回调函数进行展开操作,RtlUnwind函数调用每个被展开的回调函数时,异常标志中会含有EXCEPTION_UNWINDING标志位。
第二,如果这个参数指定为NULL的话,表示对SEH链上所有的回调函数进行展开操作,这时所有回调函数参数中的异常标志在带有EXCEPTION_UNWINDING标志位的同时也带有EXCEPTION_UNWINDING_FOR_EXIT标志位,这种方式的展开称为退出展开(Exit Unwind)。
lpCodeLabel指定函数返回的位置。如果这个参数指定为NULL,函数使用正常的返回方式,也就是返回到调用RtlUnwind函数的后面一条指令,否则,函数直接返回到lpCodeLable指定的地址。
lpExceptionRecord指定一个EXCEPTION_RECORD结构。这个结构将在展开操作的时候被传给每一个被调用的回调函数,一般建议使用NULL来让系统自动生成代表展开操作的EXCEPTION_RECORD结构。dwRet参数一般不被使用,可以将它指定为NULL。
本书所附光盘的Chapter14\Unwind目录中包含了一个SEH展开操作的例子,读者可以自行分析一下,由于篇幅所限,在此就不详细列出了。
使用RtlUnwind函数时要注意的是:这个函数并不像其他API函数一样保存esi,edi和ebx寄存器,在函数返回的时候这些寄存器的值可能会被改变,所以,如果程序用到了这些寄存器的话,必须自己去保存和恢复它们。
最后需要说明的是,SEH异常处理属于Win32中未公开的特征,本章中的大部分内容无法从Microsoft的正式文档中查到,它们来自于各种零星的资料(包括笔者对一些例子的分析以及编程测试的结果),所以可能与其他资料有所出入。如果读者发现存在错误或者有什么疑问,请告知笔者。
上页:第14章 异常处理 · 14.3 使用SEH处理异常(3) 下页:第15章 注册表和INI文件 · 15.1 注册表和INI文件简介