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

📄 785.html

📁 里面收集的是发表在www.xfocus.org上的文章
💻 HTML
📖 第 1 页 / 共 4 页
字号:
Compiler-level SEH <br />
<br />
尽管我偶尔会使用 _try 和 _except,但目前我所讲到的都是由操作系统实现的。然而,看看我那两个使用纯操作系统 SEH 的程序的变态样子,编译器对此的封装实在是必要的。我们来看一下 Visual C++ 是如何在操作系统级的 SEH 支持之上构建其结构化异常处理的。<br />
<br />
在继续进行之前要记住一件重要的事,那就是另一种编译器可能会与纯操作系统级的 SEH 的做法完全不同。没有人说过必须要实现 Win32 SDK 文档所描述的 _try/_except 模型。例如,Visual Basic 5.0 在其运行时代码里使用了结构化异常处理,但其数据结构与算法与我这里所讲的完全不同。若读一下 Win32 SDK 文档关于结构化异常处理的描述,就会找到所谓的“frame-based”的异常处理程序的语义,其形式如下:<br />
<br />
try {<br />
&nbsp;&nbsp;&nbsp;&nbsp; // guarded body of code<br />
}<br />
except (filter-expression) {<br />
&nbsp;&nbsp;&nbsp;&nbsp; // exception-handler block<br />
}<br />
<br />
<br />
简单讲,try 中的所有的代码都被一个构建在函数堆栈帧上的 EXCEPTION_REGISTRATION 保护起来。在函数的入口,新的 EXCEPTION_REGISTRATION 被放入异常处理链表的表头。在 _try 块的结尾处,其 EXCEPTION_REGISTRATION 被从链表头移除。如前所述,异常处理链的表头保存在 FS:[0]。因此,若在调试器中的汇编代码中单步执行,就会看到以下的指令:<br />
<br />
MOV DWORD PTR FS:[00000000],ESP<br />
<br />
或是<br />
<br />
MOV DWORD PTR FS:[00000000],ECX<br />
<br />
可以十分确信代码正在建立或撤除一个 _try/_except 块。现在知道了一个 _try 块对应着堆栈上的一个 EXCEPTION_REGISTRATION 结构体,那 EXCEPTION_ REGISTRATION 里的回调函数呢?使用 Win32 的术语,异常回调函数对应着 filter-expression 代号。filter-expression 就是关键字 _except 后括号中的代码。正是这个 filter-expression 代号决定了是否执行后面 {} 块中的代码。<br />
<br />
因为 filter-expression 是程序员写的,程序员可以决定代码中某处发生的异常是否在该处处理。filter-expression 代码可以简单到只有一个“EXCEPTION_EXECUTE_HANDLER”,也可以调用一个函数把 p 算到两千万再返回一个代号告诉系统下一步做什么,这是程序员的选择。关键一点是:filter-expression 的代号正对应我前面提到的异常回调函数。<br />
<br />
我刚才所讲的都十分简单,但是只是理想中的美好的情形。残酷的现实是事情要复杂的多。对于初学者,filter-expression 并不是由操作系统直接调用的。实际的情形是每个 EXCEPTION_REGISTRATION 的异常处理程序域都指向同一个函数。这个函数在 Visual C++ 的运行时库中,叫做 __except_handler3。是&nbsp;&nbsp;<br />
<br />
另外一点就是并不是每次进入或退出 _try 块都要建立或撤除 EXCEPTION_REGISTRATION。对于使用 SEH 的每个函数,只创建一个 EXCEPTION_REGISTRATION。换句话说,在一个函数里可以使用多个 _try/_except 组合,但只在堆栈上建立一个 EXCEPTION_REGISTRATION。类似地,可以在一个函数的 _try 块中嵌套另一个 _try 块,Visual C++ 仍然只创建一个 EXCEPTION_REGISTRATION。如果对于整个 EXE 或 DLL 来说一个异常处理程序就足够了以及如果用一个 EXCEPTION_REGISTRATION 就可以处理多个 _try 块,那显然还要有比所见到的更多的机制。这是通过一个一般情况下看不到的表中的数据来完成的。然而,既然本文的目的就是要解剖结构化异常处理,我们就来看一下这些数据结构。<br />
<br />
<br />
The Extended Exception Handling Frame <br />
<br />
Visual C++ 的 SEH 实现并没有使用纯粹的 EXCEPTION_REGISTRATION 结构,而是在结构体的末尾加入了额外的数据域。这个额外的数据的关键之处在于它允许一个函数(__except_handler3)来处理所有的异常并将控制流转向相应的 filter-expressions 和代码中的 _except 块。关于这个 Visual C++ 扩展的 EXCEPTION_REGISTRATION 的一点信息可以从 Visual C++ 的运行时库源代码中的 EXSUP.INC 文件里找到。在这个文件里,可以找到一下定义:<br />
<br />
;struct _EXCEPTION_REGISTRATION{<br />
;&nbsp;&nbsp;&nbsp;&nbsp; struct _EXCEPTION_REGISTRATION *prev;<br />
;&nbsp;&nbsp;&nbsp;&nbsp; void (*handler)(PEXCEPTION_RECORD,<br />
;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PEXCEPTION_REGISTRATION,<br />
;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PCONTEXT,<br />
;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PEXCEPTION_RECORD);<br />
;&nbsp;&nbsp;&nbsp;&nbsp; struct scopetable_entry *scopetable;<br />
;&nbsp;&nbsp;&nbsp;&nbsp; int trylevel;<br />
;&nbsp;&nbsp;&nbsp;&nbsp; int _ebp;<br />
;&nbsp;&nbsp;&nbsp;&nbsp; PEXCEPTION_POINTERS xpointers;<br />
;};<br />
<br />
前两个域前面已经见过了,prev 和 handler。他们组成了最基本的 EXCEPTION_REGISTRATION 结构体。新加的是最后的三个域:scopetable、trylevel 和 _ebp。scopetable 域指向一个 scopetable_entries 类型结构体数组,而 trylevel 是这个数组的索引。最后一个域,_ebp,是创建 EXCEPTION_REGISTRATION 之前的堆栈帧指针(EBP)的值。<br />
<br />
_ebp 域成为扩展的 EXCEPTION_REGISTRATION 结构体的一部分不是偶然的。结构体包含它是因为大多数函数都以一个 PUSH EBP 开始。这就使得所有其它的 EXCEPTION_REGISTRATION 域可以通过帧指针的负偏移来访问。例如,trylevel 在 [EBP-04],scopetable 指针在 [EBP-08] 等等。<br />
<br />
在扩展的 EXCEPTION_REGISTRATION 结构体后面,Visual C++ 压入了两个额外的值。第一个 DWORD 为一个指向 EXCEPTION_POINTERS 结构体(一个标准的 Win32 结构体)的指针保留空间。这个指针就是调用 GetExceptionInformation API 返回的指针。尽管 SDK 文档隐含提到 GetExceptionInformation 是一个标准的 Win32 API,但事实上 GetExceptionInformation 是一个编译器相关的函数。当调用此函数时,Visual C++ 生成下面的代码:<br />
<br />
MOV EAX,DWORD PTR [EBP-14]<br />
<br />
与 GetExceptionInformation 相同,GetExceptionCode 也依赖于编译器。GetExceptionCode 返回的值是 GetExceptionInformation 返回的数据结构中一个域的值。Visual C++ 会生成以下的代码,这些代码的作用留给读者作为练习。<br />
<br />
MOV EAX,DWORD PTR [EBP-14]<br />
MOV EAX,DWORD PTR [EAX]<br />
MOV EAX,DWORD PTR [EAX]<br />
<br />
回到扩展的 EXCEPTION_REGISTRATION 结构体,在结构体起始处之前的8个字节处,Visual C++ 保留了一个 DWORD 来保存所有 prologue 代码执行后最终的堆栈指针(ESP)。这个 DWORD 就是函数执行时一个普通的 ESP 寄存器的值(当然参数压栈是为了准备调用另外函数的情况除外)。<br />
<br />
看起来我好像一股脑儿倒出了一大堆东西,确实是。在向下继续之前,我们先暂停一会儿,复习一下 Visual C++ 为用到结构化异常处理的函数生成的标准异常帧:<br />
<br />
EBP-00 _ebp<br />
EBP-04 trylevel<br />
EBP-08 scopetable pointer<br />
EBP-0C handler function address<br />
EBP-10 previous EXCEPTION_REGISTRATION<br />
EBP-14 GetExceptionPointers<br />
EBP-18 Standard ESP in frame<br />
<br />
从操作系统的观点来看,构成纯 EXCEPTION_REGISTRATION 的域仅有两个:[EBP-10] 处的 prev 指针和 [EBP-0Ch] 处的处理函数指针。帧中其它的东西都是依赖于 Visual C++ 实现的。记住这些后,我们来看包含着编译器级 SEH 的 Visual C++ 的运行时库函数,__except_handler3。<br />
<br />
<br />
__except_handler3 and the scopetable <br />
<br />
尽管我非常想将 Visual C++ 的运行时库源代码指点出来并让读者自己去研究 __except_handler3 函数,但是我不能,因为此函数的代码并未提供。这里只好用我仓促拼凑出的 __except_handler3 的伪码来应付一下了(见 Figure 9)。<br />
<br />
尽管 __except_handler3 看上去是成堆的代码,但要记着它只是一个异常回调函数,就像我在文章开头介绍的那样。和 MYSEH.EXE 和 MYSEH2.EXE 中的 homegrown 异常回调函数一样,此函数也需要四个参数。在最高一级上,__except_handler3 被一个 if 语句分为了两部分。这是因为函数可以被调用两次,一次是正常调用,一次是在 unwind 过程中。大部分的代码都用在了 non-unwinding 的回调中。<br />
<br />
这段代码的开头首先在堆栈上创建一个 EXCEPTION_POINTERS 结构体,并用 __except_handler3 的两个参数将其初始化。此结构体的地址,即伪码中的 exceptPtrs,被放在 [EBP-14]。这就初始化了&nbsp;&nbsp;GetExceptionInformation 和 GetExceptionCode 函数用到的指针。接着,__except_handler3 从 EXCEPTION_REGISTRATION 帧([EBP-04])取得当前的 trylevel。trylevel 变量用作 scopetable 数组的索引,使得一个 EXCEPTION_REGISTRATION 可以用于一个函数中的多个 _try 块和嵌套的 _try 块。每一个 scopetable 的成员定义如下:<br />
<br />
typedef struct _SCOPETABLE<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp; DWORD&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; previousTryLevel;<br />
&nbsp;&nbsp;&nbsp;&nbsp; DWORD&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lpfnFilter<br />
&nbsp;&nbsp;&nbsp;&nbsp; DWORD&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lpfnHandler<br />
} SCOPETABLE, *PSCOPETABLE;<br />
<br />
SCOPETABLE 中的第二个和第三个参数都很容易理解。它们是 filter-expression 和相应的 _except 块代码的地址。previousTryLevel 域有点复杂。简言之,它是用于嵌套 try 块的。重要的一点是对函数中每一个 _try 块都有一个 SCOPETABLE 成员。<br />

⌨️ 快捷键说明

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