📄 014.txt
字号:
popad
mov eax,ExceptionContinueExecution
ret
_Handler endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
;********************************************************************
; 在堆栈中构造一个 EXCEPTION_REGISTRATION 结构
;********************************************************************
assume fs:nothing
push offset _Handler
push fs:[0]
mov fs:[0],esp
;********************************************************************
; 会引发异常的指令
;********************************************************************
xor eax,eax
mov dword ptr [eax],0 ;产生异常,然后_Handler被调用
; ...
; 如果这中间有指令,这些指令将不会被执行!
; ...
_SafePlace:
invoke MessageBox,NULL,addr szSafe,addr szCaption,MB_OK
;********************************************************************
; 恢复原来的 SEH 链
;********************************************************************
pop fs:[0]
pop eax
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
14.3.1 注册回调函数
在例子程序中,SEH异常处理回调函数的设置由下面3条指令完成:
push offset _Handler
push fs:[0]
mov fs:[0],esp
为什么这3句简单的指令就可以完成设置工作,为什么又要使用fs段选择器呢?这要从线程信息块(Thread Information Block/TIB)说起。
Win32为每个线程定义了一个线程信息块,其中保存了线程的一些属性数据,线程信息块的格式被定义为NT_TIB结构:
NT_TIB STRUCT
ExceptionList dd ? ;SEH链入口
StackBase dd ? ;堆栈基址
StackLimit dd ? ;堆栈大小
SubSystemTib dd ?
FiberData dd ?
ArbitraryUserPointer dd ?
Self dd ? ;本NT_TIB结构自身的线性地址
NT_TIB ENDS
NT_TIB结构的第一个字段ExceptionList指向一个EXCEPTION_REGISTRATION结构,SEH异常处理回调函数的入口地址就是由EXCEPTION_REGISTRATION结构指定的,这个结构的定义如下:
EXCEPTION_REGISTRATION STRUCT
prev dd ? ;前一个EXCEPTION_REGISTRATION结构的地址
handler dd ? ;异常处理回调函数地址
EXCEPTION_REGISTRATION ENDS
当异常发生时,系统从TIB中取出ExceptionList字段,然后从ExceptionList字段指定的EXCEPTION_REGISTRATION结构中取出handler字段,并根据其中的地址去调用回调函数,整个过程如图14.1所示,所以只要构建一个含有回调函数地址的EXCEPTION_REGISTRATION结构,然后修改TIB中的ExceptionList字段,指向这个结构就可以注册一个SEH异常处理回调函数。
图14.1 SEH异常处理程序入口地址的定义
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
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -