WIN32汇编语言教程:第14章 异常处理 · 14.3 使用SEH处理异常(3)

将_lpSEH参数放入eax后,[eax]相当于EXCEPTION_REGISTRATION结构的prev字段,[eax+4]相当于handler字段,从[eax+8]开始就是程序自定义的数据了,按照主程序中的入栈顺序,[eax+8]是“安全地址”,[eax+0ch]是程序保存的ebp值,程序中并没有单独设置一个自定义字段来保存esp,因为在这个例子中,_lpSEH本身就相当于正确的esp。经过这样的处理后,整个异常处理过程将不使用任何全局变量。修改后的完整代码可以在本书所附光盘的Chapter14\SEH02目录中找到。

Windows下的许多高级语言都在EXCEPTION_REGISTRATION结构的后面添加自定义的数据,比如,Microsoft SDK的except.inc中是这样定义的:

__EXCEPTIONREGISTRATIONRECORD struc

       prev_structure          dd     ?

       ExceptionHandler        dd     ?

       ExceptionFilter         dd     ?       ;附加数据

       FilterFrame             dd     ?   ;附加数据

       PExceptionInfoPtrs      dd     ?   ;附加数据

__EXCEPTIONREGISTRATIONRECORD ends

而在VC++中是这样定义的:

struct _EXCEPTION_REGISTRATION{

    struct _EXCEPTION_REGISTRATION *prev;

    void (*handler)(PEXCEPTION_RECORD,

                   PEXCEPTION_REGISTRATION,

                   PCONTEXT,

                   PEXCEPTION_RECORD);

    struct scopetable_entry *scopetable;   //附加数据

    int trylevel;                          //附加数据

    int _ebp;                              //附加数据

    PEXCEPTION_POINTERS xpointers;

};

除了上面两个以不同方式定义的结构,笔者在很多汇编源代码中也见过更多的各不相同的EXCEPTION_REGISTRATION结构定义,正是因为这些结构的定义各不相同,Microsoft又没有提供一份标准的文档,这使很多初次接触SEH的人根本搞不清楚SEH究竟是如何定义的。读者现在应该明白这种现象的由来了,回过头去看一看这些结构,就可以发现它们的前面两个字段就是基本的EXCEPTION_REGISTRATION结构!

2. 回调函数的返回值

SEH异常处理回调函数的返回值定义不同于筛选器异常处理回调函数,它可以使用下面列出的4种取值:

● ExceptionContinueExecution(等于0):回调函数返回后,系统将线程环境设置为_lpContext参数指定的CONTEXT结构并继续执行。

● ExceptionContinueSearch(等于1):回调函数拒绝处理这个异常,系统将通过EXCEPTION_REGISTRATION结构的prev字段得到前一个回调函数的地址并调用它。

● ExceptionNestedException(等于2):回调函数在执行中又发生了新的异常,即发生了嵌套的异常。

● ExceptionCollidedUnwind(等于3):发生了嵌套的展开操作(展开操作的介绍参见14.3.4小节)。

 

14.3.3 SEH链和异常的传递

每次定义了一个新的SEH异常处理回调函数时,EXCEPTION_REGISTRATION结构的prev字段都被要求填写为原来的EXCEPTION_REGISTRATION结构地址,随着应用程序对执行模块的调用一层层深入下去,如果有多个模块设置了回调函数,那么到最后全部的回调函数会形成一个SEH链,如图14.2所示。

当程序中有多个线程在运行的时候,每个线程中都会存在各自的SEH链,这些SEH链中指定了多个回调函数,除它们以外,系统中可能还会存在一个全局性的筛选器异常处理回调函数,再者,如果进程被调试的话,调试器进程也相当于一个异常处理程序存在。既然会同时存在这么多的回调函数,而每个函数都可能对发生的异常提出不同的处理意见,那么当一个异常发生的时候,系统究竟该听谁的意见呢?


图14.2 SEH链

在这种情况下,系统按照一定的步骤选择一个回调函数并执行它,如果这个被执行的回调函数可以处理这个异常,那么程序被修正后继续执行并且其他的回调函数不会再被执行,否则系统继续执行下一个回调函数,查找的步骤如下:

(1)系统查看产生异常的进程是否正在被调试,如果正在被调试的话,那么向调试器发送EXCEPTION_DEBUG_EVENT事件。

(2)如果进程没有被调试或者调试器不去处理这个异常,那么系统检查异常所处的线程,并在这个线程的环境中查看fs:[0]来确定是否安装有SEH异常处理回调函数,如果有的话则调用它。

(3)回调函数尝试处理这个异常,如果可以正确处理的话,则修正错误并将返回值设置为ExceptionContinueExecution,这时系统将结束整个查找过程。

(4)如果回调函数返回ExceptionContinueSearch,告知系统它无法处理这个异常,那么系统将根据SEH链中的prev字段得到上一个回调函数地址并重复步骤(3),直到链中的某个回调函数返回ExceptionContinueExecution为止,查找结束。

(5)如果到了SEH链的尾部却没有一个回调函数愿意处理这个异常,那么系统将再次检测进程是否正在被调试,如果被调试的话,则再一次通知调试器。

(6)如果调试器还是不去处理这个异常或者进程没有被调试,那么系统检查有没有安装筛选器回调函数,如果有,则去调用它,筛选器回调函数返回时,系统默认的异常处理程序根据这个返回值做相应的动作。

(7)如果没有安装筛选器回调函数,系统直接调用默认的异常处理程序终止进程。

这个过程归纳起来就是:系统按照调试器、SEH链上从新到旧的各个回调函数、筛选器回调函数的步骤一个个去调用它们,一直到某个回调函数愿意处理异常为止。如果大家都无法处理异常的话,那么最后由系统默认的异常处理程序来终止发生异常的进程。

Windows拿着一份处理异常的活挨个问每个回调函数,“你干不干?”,“不干”,“你呢?”,“我也不干”...当问到某一个的时候,他说:“那我来干好了!”,那么Windows就不会再问余下的其他人了,于是相安无事。

有时,问完了一圈以后谁都不愿干活,Windows大怒:“谁都不干,看我炒了你们!”,于是就把整个进程终止掉了,所有的回调函数随之完蛋。

14.3.4 展开操作(Unwinding

执行上面演示的SEH例子文件,程序会在显示了如图14.3中A所示的消息框后,再显示一个“转移到安全地址”的消息框后正常退出,这一切都在我们的意料之中。

现在来看看回调函数不处理异常时会怎样,将SEH.asm修改一下,去掉回调函数中修正eip寄存器的指令并将函数的返回值改为ExceptionContinueSearch,编译执行后再执行一下。首先看到的是图14.3 中A所示的消息框,单击“确定”按钮后,程序不会再显示“转移到安全地址”的消息框,而是出现系统的错误报告对话框,到此为止也在我们的意料之中,现在,单击“确定”按钮,奇怪的事情出现了,回调函数再一次被调用并显示了如图14.3中B所示的消息框!

进一步试验可以发现,如果程序在SEH链上挂了多个回调函数,并且每个回调函数都不处理异常的话,在系统默认的显示错误的对话框出现以后,每个回调函数都会被再调用一遍,这时参数中指定的异常代码是EXCEPTION_UNWIND,异常标志的取值是2,也就是EXCEPTION_UNWINDING标志。这种调用并不是要求回调函数去处理什么异常,而是告知回调函数:“你将要被卸掉了,自己处理一些后事吧”,在这时回调函数应该进行一些卸载前的扫尾工作并且返回ExceptionContinueSearch。


图14.3 展开操作时的异常代码和标志

上页:第14章 异常处理 · 14.3 使用SEH处理异常(2) 下页:第14章 异常处理 · 14.3 使用SEH处理异常(4)

第14章 异常处理

版权所有 © 中山市飞娥软件工作室 证书:粤ICP备09170368号