WIN32汇编语言教程:第14章 异常处理 · 14.3 使用SEH处理异常(2)
现在还剩下一个关键的问题:到哪里找TIB呢?答案是:TIB永远放在fs段选择器指定的数据段的0偏移处,所以,fs:[0]的地方就是TIB结构的ExceptionList字段,这个答案对于Windows 9x系统和Windows NT系统都是有效的。由于一个进程中的不同线程可以有不同的环境,所以,在不同线程中fs段选择器可以使用不同的值,这种特征使每个线程都可以设置不同的回调函数。
也正是因为使用了fs段选择器,所以使SEH变得与硬件平台相关,试想一下,Power PC或者Alpha平台上有fs段选择器吗?
好了,现在回过头来看看用于设置回调函数的3条指令,第一条push offset _Handler指令将回调函数的地址推入堆栈;第二条push fs:[0]指令则将当前使用的EXCEPTION_ REGISTRATION结构地址推入堆栈,现在堆栈指针esp指向的地方刚好是一个新的EXCEPTION_REGISTRATION结构——[esp]等于原结构地址,也就是prev字段,而[esp+4]等于回调函数地址,也就是handler字段;当第三条指令mov fs:[0],esp将esp的值放入fs:[0]后,设置工作就完成了!需要注意的是,原先的结构地址必须被保存到prev字段中。
当不再需要这个回调函数的时候,只要将fs:[0]的值恢复为原来的EXCEPTION_REGISTRATION结构地址就可以了,这个地址已经被保存在prev字段中,例子程序中使用下面的恢复代码:
pop fs:[0]
pop eax
第一条指令从堆栈中的prev字段中弹出原来的fs:[0]值;第二条指令pop eax仅仅是为了让堆栈平衡,弹出到eax中的值没有实际用途。执行了这两条指令后,堆栈中废弃的EXCEPTION_REGISTRATION结构也被释放掉了。
例子程序在堆栈中构造EXCEPTION_REGISTRATION结构,而不是将结构放在全局的数据段中,这种用法使构建异常处理程序使用的数据可以存放在一个子程序的私有空间中,更有利于程序结构的模块化。实际上,所有的高级语言在使用SEH时都将数据结构建立在堆栈中。
由于MASM编译器默认将fs段寄存器定义为error,所以程序在使用fs之前要用assume fs:nothing伪指令来启用fs寄存器,否则编译的时候会产生下面的错误:
error A2108: use of register assumed to ERROR
14.3.2 异常处理回调函数
1. 回调函数的参数
SEH异常处理回调函数的参数定义与筛选器回调函数的参数定义有所不同,其定义如下:
_Handler proc _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatcherContext
在这个回调函数中,前面的3个参数是要用到的。其中的_lpExceptionRecord参数指向一个EXCEPTION_RECORD结构;_lpContext参数指向一个CONTEXT结构,这两个结构提供的数据就相当于14.2.2节中筛选器回调函数从参数中得到的数据,可以用同样的方法来使用它们;_lpSEH参数指向注册回调函数时使用的EXCEPTION_REGISTRATION结构的地址,在例子程序中,它的值就是我们在堆栈中构造的这个结构的地址,这个参数看上去似乎没有什么用处,例子程序中也确实没有用到它,但是如果希望异常处理程序能够被封装在子程序里面的话,这个参数就是不可缺少的,因为使用它可以避免使用全局变量在模块和回调函数之间传递数据。
本章的前两个例子为了简单起见,演示的都是在主程序中执行异常指令的情况,现在来考虑产生异常的指令发生在子程序中的情况,演示的指令序列如下所示:
_Test proc
pushad
xor ebp,ebp
xor eax,eax
mov dword ptr [eax],0 ;异常指令
...
_SafePlace:
popad
ret
_Test endp
这段代码首先将所有的寄存器入栈,然后使用了ebp寄存器和eax寄存器,最后结束的时候使用popad从堆栈中恢复所有的寄存器。如果不产生异常的话,那么指令执行完毕以后,ebp的值是正常的,堆栈也是平衡的;但是如果中间的某条指令产生异常的话,回调函数必须在将程序修正到“安全”位置去执行的同时恢复esp和ebp的值,否则,由于ebp和esp值被破坏,程序的执行还是不正常的。
要将回调函数写得比较“强壮”的话,就必须考虑到这些可能性。最安全的办法就是预先保存一些关键寄存器的值,如果发生异常,回调函数就可以根据保存的数据恢复这些关键寄存器。这些关键寄存器值可以预先保存在全局变量中,但为了程序的模块化,一般推荐使用堆栈来动态传递数据。
如何使用_lpSEH参数实现用堆栈传输数据呢?大家可以注意到,_lpSEH参数指向的数据就是我们自己定义的EXCEPTION_REGISTRATION结构,这个结构存放在由程序自己分配的内存中,所以可以在结构的后面附加一些自定义的数据,这样通过_lpSEH参数就可以在堆栈中寻址到这些数据。下面是根据这个思路修改后的SEH异常处理注册代码:
;********************************************************************
; 将 _Handler 子程序注册为异常处理程序
;********************************************************************
push ebp ;附加数据
push offset _SafePlace ;附加数据
push offset _Handler
push fs:[0]
mov fs:[0],esp
...
...
_SafePlace:
...
;********************************************************************
; 恢复原来的 SEH 链
;********************************************************************
pop fs:[0]
add esp,0ch
在这里,程序在标准的EXCEPTION_REGISTRATION结构后面增加了两个自定义数据,一个是“安全地址”,一个是原先的ebp值,同时,回调函数也进行了相应的修改:
_Handler proc _lpExceptionRecord,_lpSEH,\
_lpContext,_lpDispatcherContext
pushad
mov esi,_lpExceptionRecord
mov edi,_lpContext
assume esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT
;********************************************************************
; 将 EIP 指向安全的位置并恢复堆栈
;********************************************************************
mov eax,_lpSEH
push [eax + 8]
pop [edi].regEip
push [eax + 0ch]
pop [edi].regEbp
push eax
pop [edi].regEsp
assume esi:nothing,edi:nothing
popad
mov eax,ExceptionContinueExecution
ret
_Handler endp
上页:第14章 异常处理 · 14.3 使用SEH处理异常(1) 下页:第14章 异常处理 · 14.3 使用SEH处理异常(3)