深入探索Win32结构化异常处理 - 图文(4)

2019-08-17 13:54

; void (*handler)( PEXCEPTION_RECORD, ; PEXCEPTION_REGISTRATION, ; PCONTEXT,

; PEXCEPTION_RECORD); ; struct scopetable_entry *scopetable; ; int trylevel; ; int _ebp;

; PEXCEPTION_POINTERS xpointers; ;};

在 前面你已经见过前两个域:prev和handler。它们组成了基本的

EXCEPTION_REGISTRATION结构。后面三个 域:scopetable(作用域表)、trylevel和_ebp是新增加的。scopetable域指向一个scopetable_entry结构 数组,而trylevel域实际上是这个数组的索引。最后一个域_ebp,是EXCEPTION_REGISTRATION结构创建之前栈帧指针(EBP)的值。

_ebp 域成为扩展的EXCEPTION_REGISTRATION结构的一部分并非偶然。它是通过PUSH EBP这条指令被包含进这个结构中的,而大多数函数开头都是这条指令( 通常编译器并不为使用FPO优化的函数生成标准的堆栈帧,这样其第一条指令可能不是PUSH EBP。但是如果使用了SEH的话,那么无论你是否使用了FPO优化,编译器一定生成标准的堆栈帧 )。 这条指令可以使EXCEPTION_REGISTRATION结构中所有其它的域都可以用一个相对于栈帧指针(EBP)的负偏移来访问。例如 trylevel域在[EBP-04]处,scopetable指针在[EBP-08]处,等等。(也就是说,这个结构是从[EBP-10H]处开始 的。)

紧跟着扩展的EXCEPTION_REGISTRATION结构下面,Visual C++压入了另外两个值。紧跟着(即[EBP-14H]处)的一个DWORD,是为一个指向EXCEPTION_POINTERS 结构(一个标准的Win32 结构)的指针所保留的空间。这个指针就是你调用GetExceptionInformation这个API时返回的指针。尽管SDK文档暗示GetExceptionInformation 是一个标准的Win32 API,但事实上它是一个编译器内联函数。当你调用这个函数时,Visual C++生成以下代码:

MOV EAX,DWORD PTR [EBP-14]

GetExceptionInformation 是一个编译器内联函数,与它相关的 GetExceptionCode 函 数也是如此。此函数实际上只是返回GetExceptionInformation返回的数据结构

(EXCEPTION_POINTERS)中的一个结构 (EXCEPTION_RECORD)中的一个域(ExceptionCode)的值。当Visual C++为GetExceptionCode函数生成下面的指令时,它到底是想干什么?我把这个问题留给读者。(现在就能理解为什么SDK文档提醒我们要注 意这两个函数的使用范围了。) MOV EAX,DWORD PTR [EBP-14] ; 执行完毕,EAX指向 EXCEPTION_POINTERS 结构 MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX指向EXCEPTION_RECORD结构 MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX中是ExceptionCode的值

现在回到扩展的EXCEPTION_REGISTRATION结构上来。在这个结构开始前的8个字节处(即[EBP-18H]处),Visual C++保留了一个DWORD来保存所有 prolog 代 码执行完毕之后的堆栈指针(ESP)的值(实际生成的指令为MOV DWORD PTR [EBP-18H],ESP)。这个DWORD中保存的值是函数执行时ESP寄存器的正常值(除了在准备调用其它函数时把参数压入堆栈这个过程会改变 ESP寄存器的值并在函数返回时恢复它的值外,函数在执行过程中一般不改变ESP寄存器的值)。

看起来好像我一下子给你灌输了太多的信息,这点我承认。在继续下去之前,让我们先暂停,来回顾一下Visual C++为使用结构化异常处理的函数生成的标准异常堆栈帧,它看起来像下面这个样子: EBP-00 _ebp EBP-04 trylevel

EBP-08 scopetable 数组指针 EBP-0C handler 函数地址

EBP-10指向前一个EXCEPTION_REGISTRATION结构 EBP-14 GetExceptionInformation EBP-18 栈帧中的标准ESP

在操作系统看来,只存在组成原始EXCEPTION_REGISTRATION结构的两个域:

即 [EBP-10h] 处的prev指针和[EBP-0Ch]处的handler函数指针 。栈帧中的其它所有内容是针对于Visual C++的。把这个Visual C++生成的标准异常堆栈帧记到脑子里之后,让我们来看一下真正实现编译器层面SEH的这个Visual C++运行时库例程——__except_handler3。

__except_handler3 和scopetable

我 真的很希望让你看一看Visual C++运行时库源代码,让你自己好好研究一下

__except_handler3函数,但是我办不到。因为Microsoft并没有提供。在这里你就将就 着看一下我为__except_handler3函数写的伪代码吧(如图9所示)。 图9 __except_handler3函数的伪代码

int __except_handler3(

struct _EXCEPTION_RECORD * pExceptionRecord, struct EXCEPTION_REGISTRATION * pRegistrationFrame, struct _CONTEXT *pContextRecord, void * pDispatcherContext )

{

LONG filterFuncRet; LONG trylevel;

EXCEPTION_POINTERS exceptPtrs; PSCOPETABLE pScopeTable;

CLD // 将方向标志复位(不测试任何条件!)

// 如果没有设置EXCEPTION_UNWINDING标志或EXCEPTION_EXIT_UNWIND标志 // 表明这是第一次调用这个处理程序(也就是说,并非处于异常展开阶段) if ( ! (pExceptionRecord->ExceptionFlags

& (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) ) {

// 在堆栈上创建一个EXCEPTION_POINTERS结构 exceptPtrs.ExceptionRecord = pExceptionRecord; exceptPtrs.ContextRecord = pContextRecord; // 把前面定义的EXCEPTION_POINTERS结构的地址放在比 // establisher栈帧低4个字节的位置上。参考前面我讲 // 的编译器为GetExceptionInformation生成的汇编代码*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs; // 获取初始的“trylevel”值

trylevel = pRegistrationFrame->trylevel; // 获取指向scopetable数组的指针

scopeTable = pRegistrationFrame->scopetable; search_for_handler:

if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE ) {

if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter ) {

PUSH EBP // 保存这个栈帧指针

// !!!非常重要!!! 切换回原来的EBP。正是这个操作才使得 // 栈帧上的所有局部变量能够在异常发生后仍然保持它的值不变。 EBP = &pRegistrationFrame->_ebp; // 调用过滤器函数

filterFuncRet = scopetable[trylevel].lpfnFilter();

POP EBP // 恢复异常处理程序的栈帧指针 if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH ) {

if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION return ExceptionContinueExecution ;

// 如果能够执行到这里,说明返回值为EXCEPTION_EXECUTE_HANDLER scopetable = pRegistrationFrame->scopetable;

// 让操作系统清理已经注册的栈帧,这会使本函数被递归调用 __global_unwind2( pRegistrationFrame );

// 一旦执行到这里,除最后一个栈帧外,所有的栈帧已经 // 被清理完毕,流程要从最后一个栈帧继续执行 EBP = &pRegistrationFrame->_ebp;

__local_unwind2( pRegistrationFrame, trylevel ); // NLG = \__NLG_Notify( 1 ); // EAX = scopetable->lpfnHandler // 把当前的trylevel设置成当找到一个异常处理程序时 // SCOPETABLE中当前正在被使用的那一个元素的内容

pRegistrationFrame->trylevel = scopetable->previousTryLevel; // 调用__except {}块,这个调用并不会返回

pRegistrationFrame->scopetable[trylevel].lpfnHandler(); }

}

scopeTable = pRegistrationFrame->scopetable; trylevel = scopeTable->previousTryLevel; goto search_for_handler; }

else // trylevel == TRYLEVEL_NONE {

return ExceptionContinueSearch; } }

else // 设置了EXCEPTION_UNWINDING标志或EXCEPTION_EXIT_UNWIND标志 {

PUSH EBP // 保存EBP

EBP = &pRegistrationFrame->_ebp; // 为调用__local_unwind2设置EBP __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )

POP EBP // 恢复EBP

return ExceptionContinueSearch; } }

虽 然__except_handler3的代码看起来很多,但是记住一点:它只是一个我在文章开头讲过的异常处理回调函数。它同MYSEH.EXE和 MYSEH2.EXE中的异常回调函数都带有同样的四个参数。__except_handler3大体上可以由第一个if语句分为两部分。这是由于这个函 数可以在两种情况下被调用,一次是正常调用,另一次是在展开阶段。其中大部分是在非展开阶段的回调。

__except_handler3 一 开始就在堆栈上创建了一个EXCEPTION_POINTERS结构,并用它的两个参数来对这个结构进行初始化。我在伪代码中把这个结构称为 exceptPrts,它的地址被放在[EBP-14h]处。你回忆一下前面我讲的编译器为GetExceptionInformation和 GetExceptionCode函数生成的汇编代码就会意识到,这实际上初始化了这两个函数使用的指针。

接 着,__except_handler3从EXCEPTION_REGISTRATION帧中获取当前的trylevel(在[EBP-04h]处)。 trylevel变量实际是scopetable数组的索引,而正是这个数组才使得一个函数中的多个__try块和嵌套的__try块能够仅使用一个 EXCEPTION_REGISTRATION结构。每个scopetable元素结构如下: typedef struct _SCOPETABLE {

DWORD previousTryLevel; DWORD lpfnFilter; DWORD lpfnHandler; } SCOPETABLE, *PSCOPETABLE;

SCOPETABLE 结构中的第二个成员和第三个成员比较容易理解。它们分别是过滤器表达式代码的地址和相应的__except块的地址。但是prviousTryLevel成员有点复杂。总之一句话,它用于嵌套的__try块。这里的关键是 函数中的每个__try块都有一个相应的SCOPETABLE结构 。

正 如我前面所说,当前的trylevel指定了要使用的scopetable数组的哪一个元素,最终也就是指定了过滤器表达式和__except块的地址。 现在想像一下两个__try块嵌套的情形。如果内层__try块的过滤器表达式不处理某个异常,那外层__try块的过滤器表达式就必须处理它。那现在要 问,__except_handler3是如何知道SCOPETABLE数组的哪个元素相应于外层的__try块的呢?答案是:外层__try块的索引由 SCOPETABLE结构的previousTryLevel域给出。利用这种机制,你可以嵌套任意层的__try块。previousTryLevel 域就好像是一个函数中所有可能的异常处理程序构成的线性链表中的结点一样。如果trylevel的值为0xFFFFFFFF(实际上就是-1,这个值在 EXSUP.INC中被定义为 TRYLEVEL_NONE ),标志着这个链表结束。

回到__except_handler3的代码中。在获取了当前的trylevel之后,它就调用相应的SCOPETABLE结构中的过滤器表达式代码。如果过滤器表达式返回 EXCEPTION_CONTINUE_SEARCH ,__exception_handler3 移向SCOPETABLE数组中的下一个元素,这个元素的索引由

previousTryLevel域给出。如果遍历完整个线性链表(还记得吗?这个链表是 由于在一个函数内部嵌套使用__try块而形成的)都没有找到处理这个异常的代码,__except_handler3返回 DISPOSITION_CONTINUE_SEARCH (原文如此,但根据_except_handler函数的定义,这个返回值应该为 ExceptionContinueSearch 。实际上这两个常量的值是一样的。我在伪代码中已经将其改正过来了),这导致系统移向下一个EXCEPTION_REGISTRATION帧(这个链表是由于函数嵌套调用而形成的)。

如果过滤器表达式返回 EXCEPTION_EXECUTE_HANDLER , 这意味着异常应该由相应的__except块处理。它同时也意味着所有前面的EXCEPTION_REGISTRATION帧都应该从链表中移除,

并且相 应的__except块都应该被执行。第一个任务通过调用__global_unwind2来完成的,后面我会讲到这个函数。跳过这中间的一些清理代码, 流程离开__except_handler3转向__except块。令人奇怪的是,流程并不从__except块中返回,虽然是 __except_handler3使用CALL指令调用了它。

当 前的trylevel值是如何被设置的呢?它实际上是由编译器隐含处理的。编译器非常机灵地修改这个扩展的EXCEPTION_REGISTRATION 结构中的trylevel域的值(实际上是生成修改这个域的值的代码)。如果你检查编译器为使用SEH的函数生成的汇编代码,就会在不同的地方都看到修改 这个位于[EBP-04h]处的trylevel域的值的代码。

__except_handler3 是 如何做到既通过CALL指令调用__except块而又不让执行流程返回呢?由于CALL指令要向堆栈中压入了一个返回地址,你可以想象这有可能破坏堆 栈。如果你检查一下编译器为__except块生成的代码,你会发现它做的第一件事就是将

EXCEPTION_REGISTRATION结构下面8个字节 处(即[EBP-18H]处)的一个DWORD值加载到ESP寄存器中(实际代码为MOV ESP,DWORD PTR [EBP-18H]),这个值是在函数的prolog代码中被保存在这个位置的(实际代码为MOV DWORD PTR [EBP-18H],ESP)。

ShowSEHFrames 程序

如 果你现在觉得已经被EXCEPTION_REGISTRATION、scopetable、trylevel、过滤器表达式以及展开等等之类的词搞得晕头 转向的话,那和我最初的感觉一样。但是编译器层面的结构化异常处理方面的知识并不适合一点一点的学。除非你从整体上理解它,否则有很多内容单独看并没有什 么意义。当面对大堆的理论时,我最自然的做法就是写一些应用我学到的理论方面的程序。如果它能够按照预料的那样工作,我就知道我的理解(通常)是正确的。

图 10是ShowSEHFrame.EXE的源代码。它使用__try/__except块设置了好几个Visual C++ SEH帧。然后它显示每一个帧以及Visual C++为每个帧创建的scopetable的相关信息。这个程序本身并不生成也不依赖任何异常。相反,我使用了多个__try块以强制Visual C++生成多个EXCEPTION_REGISTRATION帧以及相应的scopetable。 图10 ShowSEHFrames.CPP

//========================================================= // ShowSEHFrames - Matt Pietrek 1997 // Microsoft Systems Journal, February 1997 // FILE: ShowSEHFrames.CPP

// 使用命令行CL ShowSehFrames.CPP进行编译

//========================================================= #define WIN32_LEAN_AND_MEAN #include #include #pragma hdrstop

//------------------------------------------------------------------- // 本程序仅适用于Visual C++,它使用的数据结构是特定于Visual C++的 //------------------------------------------------------------------- #ifndef _MSC_VER

#error Visual C++ Required (Visual C++ specific information is displayed) #endif

//------------------------------------------------------------------- // 结构定义

//-------------------------------------------------------------------


深入探索Win32结构化异常处理 - 图文(4).doc 将本文的Word文档下载到电脑 下载失败或者文档不完整,请联系客服人员解决!

下一篇:七年级数学下册《2.1 两条直线的位置关系(一)》教学设计(新版

相关阅读
本类排行
× 注册会员免费下载(下载后可以自由复制和排版)

马上注册会员

注:下载文档有可能“只有目录或者内容不全”等情况,请下载之前注意辨别,如果您已付费且无法下载或内容有问题,请联系我们协助你处理。
微信: QQ: