⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 014.txt

📁 会变语言实现的一些程序
💻 TXT
📖 第 1 页 / 共 4 页
字号:

    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.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.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的正式文档中查到,它们来自于各种零星的资料(包括笔者对一些例子的分析以及编程测试的结果),所以可能与其他资料有所出入。如果读者发现存在错误或者有什么疑问,请告知笔者。

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -