博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
对于结构化异常处理(SEH)的进一步探索
阅读量:2400 次
发布时间:2019-05-10

本文共 21068 字,大约阅读时间需要 70 分钟。

导读:
对于结构化异常处理(SEH)的进一步探索
本文关键字: SEH,
_except_handler3, Windows, VisualC
写本文的起因

Matt Pietrek 关于结构化异常处理的文章 A Crash Course on the Depths of Win32

Structured Exception Handling 是一篇很棒的文章(在本文末的“相关文章”中有我整理过的中文版文章链接),对于我了解
Win32 下的这种异常处理机制帮助很大。在仔细读完整篇文章、跟踪过相关代码的执行流程后,仍有意犹未尽的感觉。本文就是在读过这篇文章之后写的,具有一定“实验记录”的性质。所以强烈建议在阅读本文之前看一下
Matt Pietrek 的文章。

在所有被编译器封装的异常处理行为中,最重要的恐怕就要数 _except_handler3 和 __local_unwind2

函数了。在这两者之间,又数 _except_handler3 尤为重要。因为这个函数是操作系统与编译器之间的接口,操作系统只能按照栈帧结构一个一个地调用
handler,而真正的 filter 调用,以及同一个函数内的嵌套 __try 的处理都要依赖 _except_handler3
来完成。因此,认真研究一下这个函数的实现,对于完全理解 VC 下的 SEH 机制就显得十分必要。

_except_handler3 伪码的勘误

Matt Pietrek 在他的文章中已经给出了 _except_handler3() 的伪代码,但不知细心的人是否发现了伪码中的错误,当然,我并不是在计较那两个把“=”写成“==”的无聊笔误,而是另外一个小逻辑错误。让我们再仔细看一下文中给出的伪码:

001: int __except_handler3( 002:     struct _EXCEPTION_RECORD * pExceptionRecord, 003:     struct EXCEPTION_REGISTRATION * pRegistrationFrame, 004:     struct _CONTEXT *pContextRecord, 005:     void * pDispatcherContext ) 006: {
007: LONG filterFuncRet 008: LONG trylevel 009: EXCEPTION_POINTERS exceptPtrs 010: PSCOPETABLE pScopeTable 011: 012: CLD // Clear the direction flag (make no assumptions!) 013: 014: // if neither the EXCEPTION_UNWINDING nor EXCEPTION_EXIT_UNWIND bit 015: // is set... This is true the first time through the handler (the 016: // non-unwinding case) 017: 018: if ( ! (pExceptionRecord->ExceptionFlags 019: & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) ) 020: {
021: // Build the EXCEPTION_POINTERS structure on the stack 022: exceptPtrs.ExceptionRecord = pExceptionRecord; 023: exceptPtrs.ContextRecord = pContextRecord; 024: 025: // Put the pointer to the EXCEPTION_POINTERS 4 bytes below the 026: // establisher frame. See ASM code for GetExceptionInformation 027: *(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs; 028: 029: // Get initial "trylevel" value 030: trylevel = pRegistrationFrame->trylevel 031: 032: // Get a pointer to the scopetable array 033: scopeTable = pRegistrationFrame->scopetable; 034: 035: search_for_handler: 036: 037: if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE ) 038: {
039: if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter ) 040: {
041: PUSH EBP // Save this frame EBP 042: 043: // !!!Very Important!!! Switch to original EBP. This is 044: // what allows all locals in the frame to have the same 045: // value as before the exception occurred. 046: EBP = &pRegistrationFrame->_ebp 047: 048: // Call the filter function 049: filterFuncRet = scopetable[trylevel].lpfnFilter(); 050: 051: POP EBP // Restore handler frame EBP 052: 053: if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH ) 054: {
055: if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION 056: return ExceptionContinueExecution; 057: 058: // If we get here, EXCEPTION_EXECUTE_HANDLER was specified 059: scopetable == pRegistrationFrame->scopetable 060: 061: // Does the actual OS cleanup of registration frames 062: // Causes this function to recurse 063: __global_unwind2( pRegistrationFrame ); 064: 065: // Once we get here, everything is all cleaned up, except 066: // for the last frame, where we'll continue execution 067: EBP = &pRegistrationFrame->_ebp 068: 069: __local_unwind2( pRegistrationFrame, trylevel ); 070: 071: // NLG == "non-local-goto" (setjmp/longjmp stuff) 072: __NLG_Notify( 1 ); // EAX == scopetable->lpfnHandler 073: 074: // Set the current trylevel to whatever SCOPETABLE entry 075: // was being used when a handler was found 076: pRegistrationFrame->trylevel = scopetable->previousTryLevel; 077: 078: // Call the _except {} block. Never returns. 079: pRegistrationFrame->scopetable[trylevel].lpfnHandler(); 080: } 081: } 082: 083: scopeTable = pRegistrationFrame->scopetable; 084: trylevel = scopeTable->previousTryLevel 085: 086: goto search_for_handler; 087: } 088: else // trylevel == TRYLEVEL_NONE 089: {
090: retvalue == DISPOSITION_CONTINUE_SEARCH; 091: } 092: } 093: else // EXCEPTION_UNWINDING or EXCEPTION_EXIT_UNWIND flags are set 094: {
095: PUSH EBP // Save EBP 096: EBP = pRegistrationFrame->_ebp // Set EBP for __local_unwind2 097: 098: __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE ) 099: 100: POP EBP // Restore EBP 101: 102: retvalue == DISPOSITION_CONTINUE_SEARCH; 103: } 104: }

注意第 37 行的 if 语句,对当前 pRegistrationFrame 中的 trylevel 进行了判定:

if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE )

也就是说,如果已经没有 try 块了,就直接返回 DISPOSITION_CONTINUE_SEARCH,然后操作系统会调用下一个栈帧的

handler。但这里的判定写错了,显然应该是:

if ( trylevel != TRYLEVEL_NONE )

因为在没有 filter 或者 filter 不处理异常的情况下,第 84 行的赋值将会回溯到外层 __try 块的

scopetable_entry 结构中:

trylevel = scopeTable->previousTryLevel

如果不进行这样的修改,就不难想象这个 handler 会导致怎样的后果:异常发生后,pRegistrationFrame->trylevel

指示了异常发生的地方所处的 __try 块在 scopetable 中的索引。如果这一层的 filter 没有处理这个异常,那么在这个
handler 中将会执行到第 86 行,也就是说准备开始执行外层 __try 块的 filter。此时 pRegistrationFrame->trylevel
的值并没有任何改变,也就是说,无论外层是否有 __try 块,第 37 行的判定一定可以再次通过,但由于 84 行的赋值操作已经在上一个循环中更新了
trylevel 变量的值,trylevel 的值就有可能是 -1 了(也就是说该帧内没有人处理这个异常),在这种情况下,第
39 行的判定一定会引发一个 Access Violation 异常,因为 trylevel 作为数组下标是一个非法值。而且不难预见的是:除非该帧内没有一个
__try,或者某个 filter 处理了异常,否则这个 handler 肯定是次次要在这里摔跟头的。并且,这个跟头摔的不算轻:这属于在一个异常
handler 中引发了另一个异常(传说中的 double fault?),这个异常会被系统函数 RtlpExecuteHandlerForException
安装的简易 handler 处理(Matt Pietrek 在他的文章中提到过这一点,参看“Into the Inferno”一节),处理结果就是返回
DISPOSITION_NESTED_EXCEPTION,然后给这个异常打上一个“异常嵌套了!!!”的标志(Matt Pietrek
提供的伪码中写的却是 EH_EXIT_UNWIND,虽然乍看上去他的伪码似乎更合理一些,但是却与实际情况不符,我将在后面提到这一点)。

外,伪码中第 96 行似乎少了一个非常关键的取地址符“&”,但我相信这是另一个笔误罢了,因为前面第 46
行的赋值表达式是正确的。但是,我刚刚看到这里的时候却没有现在这么清楚,我曾经为判断这两种写法哪个是正确的、哪个是错误的而迷惑了一段时间。之后,在
跟踪了 VC 构造异常帧的代码后我终于意识到:46 行的那一句才是正确的。再后来,当我在跟踪 _except_handler3
的代码时,无意间发现了 VC 内部真正的 _EH3_EXCEPTION_REGISTRATION 结构的定义才知道:CRT 源码文件
EXSUP.INC 中的那个 _EXCEPTION_REGISTRATION 结构的定义实在是太迷惑人了,尤其是那个“_ebp”成员;而另一个
EXCEPT.INC 文件中用汇编语言给出的 __EXCEPTIONREGISTRATIONRECORD
定义更是胡扯。当然,这些问题一会儿再说,现在先回到 _except_handler3。

我是个好奇心很强的人,发现了伪码中的错误以后不禁觉得有些兴奋(众人语:这什么人嘛!),并且想到了另一个问题:Matt

Pietrek 是怎么写出这些伪代码的?如果说这个错误是他不小心犯的,那么他有没有犯别的错误——他犯错误我管不着,但是如果我跟着学坏了,那岂不是很冤……所以,我决定自己去看看
_except_handler3 的代码究竟是什么样子的。

激动人心的旅程

我知道作出这个决定后肯定要经历一个痛苦的过程,但我仍然义无反顾地开了 VC,然后一头扎进机器语言的茫茫大雾中……

想看到 _except_handler3,就要先抓住它;想抓住它,就要先引发一个异常。这个好办,几行程序就可以把它引出来:

1: __try {
2: int *p = 0; 3: *p = 0; 4: } __except(EXCEPTION_EXECUTE_HANDLER) {
5: }

在第 3 行设断点,然后切换到反汇编,就看到了这样的景象:

01: _try {
02: 00411A4B mov dword ptr [ebp-4],0 03: int *p = 0; 04: 00411A52 mov dword ptr [p],0 05: *p = 0; 06: 00411A59 mov eax,dword ptr [p] 07: 00411A5C mov dword ptr [eax],0 08: 00411A62 mov dword ptr [ebp-4],0FFFFFFFFh 09: 00411A69 jmp $L28580+0Ah (411A7Bh) 10: } _except(EXCEPTION_EXECUTE_HANDLER) {
11: 00411A6B mov eax,1 12: $L28581: 13: 00411A70 ret 14: $L28580: 15: 00411A71 mov esp,dword ptr [ebp-18h] 16: 00411A74 mov dword ptr [ebp-4],0FFFFFFFFh 17: }

啊,在明白了大部分事情之后,一切显得都是那么的自然:第 2 行的指令不就是在设置那个“传说中的”trylevel 么?呵呵,基址后的第一个

DWORD 就是,果然不错。AV 异常显然应该在第 7 行发生,step into 那一行,却发现:VC 在输出窗口中显示有异常发生,然后直接停在了
15 行,也就是 handler 代码开始的地方。这不是我想要的结果,因为据我所知,异常发生后,会产生一大堆系统调用,最后由
_except_handler3 把控制权交回我写的 handler。换句话说,当进入我的 handler 代码时,这一切都已经结束了……

既然 VC 不愿意让我这么容易地看到 _except_handler3 的代码,那么我也就不得不耍点手段了,于是我盯上了

11、13 行的 filter 指令。是的,这应该就是 filter 的代码,如果有人 CALL 到 11 行,那么这行指令会将
eax 置为 1,然后在第 13 行返回,也就是返回 1,根据 EXCPT.H 中的宏定义,1 就是 EXCEPTION_EXECUTE_HANDLER
的值,所以这正是我的 filter-expression 的行为,这就是我的 filter 代码。那么,如果是 _except_handler3
调用了 filter,那么我在 filter 返回之前中断,是不是就能跟回到我梦寐以求的 _except_handler3
中去了呢?是的,当我在第 13 行设断点、step over 之后,VC 终于老老实实地把我带回了 _except_handler3
家。

好在 _except_handler3 的代码不多,更何况我之前已经看过了伪码,所以想弄懂这些指令在做什么并不是件很难的事。首先我意识到必须先弄到它的定义,否则看那一大堆相对于

ebp 寄存器的偏移肯定不是件多么舒服的事。好在 Matt Pietrek 已经在他的文章中提到了,EXCPT.H 中包含了这个函数定义:

1: EXCEPTION_DISPOSITION 2: __cdecl _except_handler( 3:     struct _EXCEPTION_RECORD *ExceptionRecord, 4:     void * EstablisherFrame, 5:     struct _CONTEXT *ContextRecord, 6:     void * DispatcherContext 7: );

虽然这个定义中的函数名是 _except_handler 而非 _except_handler3,但估计也就是一个 Place

Holder。因为我已经尝试过直接在代码中显示调用这个函数名了,但是 Link 不上,所以名字不一样也无所谓了。根据这个定义,可以得出结论:这是一个
__cdecl 调用约定的函数,4 个参数从右至左入栈,调用者负责清理堆栈。因此:指令中出现的 [ebp+8] 引用的是
ExceptionRecord、[ebp+0Ch] 引用的是 EstablisherFrame、[ebp+10h] 引用的是
ContextRecord、[ebp+14h] 引用的是 DispatcherContext,函数返回使用 ret 而非
__stdcall 的函数常用的“ret N”。好了,有了这些信息,分析起来就容易多了:

001: _except_handler3: 002: 004141A0  push        ebp 003: 004141A1  mov         ebp,esp 004:     ; // EXCEPTION_POINTERS exceptPtrs; 005: 004141A3  sub         esp,8 006: 004141A6  push        ebx 007: 004141A7  push        esi 008: 004141A8  push        edi 009: 004141A9  push        ebp 010: 004141AA  cld 011:     ; // EstablisherFrame => ebx 012: 004141AB  mov         ebx,dword ptr [ebp+0Ch] 013:     ; // ExceptionRecord => eax 014: 004141AE  mov         eax,dword ptr [ebp+8] 015:     ; // if (ExceptionRecord->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT) 016:     ; //     goto _lh_unwinding; 017: 004141B1  test        dword ptr [eax+4],6 018: 004141B8  jne         _lh_unwinding (414269h) 019:     ; // exceptPtrs.ExceptionRecord = ExceptionRecord 020: 004141BE  mov         dword ptr [ebp-8],eax 021:     ; // exceptPtrs.ContextRecord = ContextRecord; 022: 004141C1  mov         eax,dword ptr [ebp+10h] 023: 004141C4  mov         dword ptr [ebp-4],eax 024:     ; // *(PDWORD)((PBYTE)EstablisherFrame - 4) = &exceptPtrs 025: 004141C7  lea         eax,[ebp-8] 026: 004141CA  mov         dword ptr [ebx-4],eax 027:     ; // EstablisherFrame->trylevel => esi 028: 004141CD  mov         esi,dword ptr [ebx+0Ch] 029:     ; // EstablisherFrame->scopetable => edi 030: 004141D0  mov         edi,dword ptr [ebx+8] 031:     ; // if (_ValidateEH3RN(EstablisherFrame) == 0) 032:     ; //     goto _lh_abort; 033: 004141D3  push        ebx 034: 004141D4  call        @ILT+775(__ValidateEH3RN) (41130Ch) 035: 004141D9  add         esp,4 036: 004141DC  or          eax,eax 037: 004141DE  je          _lh_abort (41425Bh) 038: _lh_top: 039:     ; // if (trylevel == TRYLEVEL_NONE) 040:     ; //     goto _lh_bagit; 041: 004141E0  cmp         esi,0FFFFFFFFh 042: 004141E3  je          _lh_bagit (414262h) 043:     ; // EstablisherFrame->scopetable[trylevel].lpfnFilter => eax 044: 004141E5  lea         ecx,[esi+esi*2] 045: 004141E8  mov         eax,dword ptr [edi+ecx*4+4] 046:     ; // if (EstablisherFrame->scopetable[trylevel].lpfnFilter == NULL) 047:     ; //     goto _lh_continue; 048: 004141EC  or          eax,eax 049: 004141EE  je          _lh_continue (414249h) 050:     ; // PUSH EBP 051: 004141F0  push        esi 052: 004141F1  push        ebp 053:     ; // EBP = &EstablisherFrame->_ebp 054: 004141F2  lea         ebp,[ebx+10h] 055:     ; // ret = EstablisherFrame->scopetable[trylevel].lpfnFilter(); 056: 004141F5  xor         ebx,ebx 057: 004141F7  xor         ecx,ecx 058: 004141F9  xor         edx,edx 059: 004141FB  xor         esi,esi 060: 004141FD  xor         edi,edi 061: 004141FF  call        eax 062:     ; // POP EBP 063: 00414201  pop         ebp 064: 00414202  pop         esi 065:     ; // EstablisherFrame => ebx 066: 00414203  mov         ebx,dword ptr [ebp+0Ch] 067:     ; // if (ret == EXCEPTION_CONTINUE_SEARCH) 068:     ; //     goto _lh_continue; 069:     ; // else if (ret < 0) 070:     ; //     goto _lh_dismiss; 071: 00414206  or          eax,eax 072: 00414208  je          _lh_continue (414249h) 073: 0041420A  js          _lh_dismiss (414254h) 074:     ; // __global_unwind2(EstablisherFrame); 075: 0041420C  mov         edi,dword ptr [ebx+8] 076: 0041420F  push        ebx 077: 00414210  call        @ILT+700(__global_unwind2) (4112C1h) 078: 00414215  add         esp,4 079:     ; // EBP = &EstablisherFrame->_ebp 080: 00414218  lea         ebp,[ebx+10h] 081:     ; // __local_unwind2(EstablisherFrame, trylevel); 082: 0041421B  push        esi 083: 0041421C  push        ebx 084: 0041421D  call        @ILT+385(__local_unwind2) (411186h) 085: 00414222  add         esp,8 086:     ; // __NLG_Notify(1); 087: 00414225  lea         ecx,[esi+esi*2] 088: 00414228  push        1 089: 0041422A  mov         eax,dword ptr [edi+ecx*4+8] 090: 0041422E  call        @ILT+1045(__NLG_Notify) (41141Ah) 091:     ; // EstablisherFrame->trylevel = 092:     ; //     EstablisherFrame->scopetable[trylevel].previousTryLevel 093: 00414233  mov         eax,dword ptr [edi+ecx*4] 094: 00414236  mov         dword ptr [ebx+0Ch],eax 095:     ; // EstablisherFrame->scopetable[trylevel].lpfnHandler(); 096: 00414239  mov         eax,dword ptr [edi+ecx*4+8] 097: 0041423D  xor         ebx,ebx 098: 0041423F  xor         ecx,ecx 099: 00414241  xor         edx,edx 100: 00414243  xor         esi,esi 101: 00414245  xor         edi,edi 102: 00414247  call        eax 103: _lh_continue: 104:     ; // EstablisherFrame->scopetable[trylevel].previousTryLevel => esi 105: 00414249  mov         edi,dword ptr [ebx+8] 106: 0041424C  lea         ecx,[esi+esi*2] 107: 0041424F  mov         esi,dword ptr [edi+ecx*4] 108: 00414252  jmp         _lh_top (4141E0h) 109: _lh_dismiss: 110:     ; // return ExceptionContinueExecution; 111: 00414254  mov         eax,0 112: 00414259  jmp         _lh_return (41427Eh) 113: _lh_abort: 114:     ; // ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID; 115: 0041425B  mov         eax,dword ptr [ebp+8] 116: 0041425E  or          dword ptr [eax+4],8 117: _lh_bagit: 118:     ; // return ExceptionContinueSearch; 119: 00414262  mov         eax,1 120: 00414267  jmp         _lh_return (41427Eh) 121: _lh_unwinding: 122:     ; // PUSH EBP 123: 00414269  push        ebp 124:     ; // EBP = &EstablisherFrame->_ebp 125: 0041426A  lea         ebp,[ebx+10h] 126:     ; // __local_unwind2(EstablisherFrame, TRYLEVEL_NONE); 127: 0041426D  push        0FFFFFFFFh 128: 0041426F  push        ebx 129: 00414270  call        @ILT+385(__local_unwind2) (411186h) 130: 00414275  add         esp,8 131:     ; // POP EBP 132: 00414278  pop         ebp 133:     ; // return ExceptionContinueSearch; 134: 00414279  mov         eax,1 135: _lh_return: 136: 0041427E  pop         ebp 137: 0041427F  pop         edi 138: 00414280  pop         esi 139: 00414281  pop         ebx 140: 00414282  mov         esp,ebp 141: 00414284  pop         ebp 142: 00414285  ret

好了,我已经在指令前插入了 C 语句,现在 _except_handler3 对于我来说已经没有任何神秘之处了。说点题外话:我发现如果把这些语句提取出来、组成伪码的话,与

Matt Pietrek 的伪码将会非常的像,如果说代码结构方面有相似性也就罢了——毕竟牛人写出来的东西一般都很靠谱的,但是像变量的赋值顺序、指令流的走向、甚至
CLD 指令这样的小地方都一样。不知道他是不是也是用跟踪反汇编的方法写出的那些伪代码?真想问问他本人……

不难发现,Matt Pietrek 没有在他的文章中提到第 31、32 行的代码(也就是反汇编第 33 至 37 行间的指令),这段代码调用了另一个函数并检查返回值,如果返回

0,handler 的指令流就会跳转到 _lh_abort 处:给异常打上一个“EXCEPTION_STACK_INVALID”的标志位(or
上了一个 8,也就是 EXSUP.INC 中定义的 EXCEPTION_STACK_INVALID 的值)然后立即返回。根据这个函数符号名中“Validate”的含义、以及
_except_handler3 发现其返回 0 后神经质般的举动可以判断——这个函数执行的是对栈帧指针的合法性检查。这种检查可以说在整个异常处理过程中并不鲜见,Rtl
函数里经常进行这样的检查,什么是否上下越界、是否 DWORD 对齐什么的……在这里出现也并不稀奇。我也没有对这个函数做深入研究,只是跟进去看了一眼,但是却有了意外的发现。

_EH3_EXCEPTION_REGISTRATION
结构的本来面目

到目前为止,VC 中的 EXCEPTION_REGISTRATION 出现了两个版本。一个是 EXSUP.INC 中的定义,也就是

Matt Pietrek 使用的那个版本;另一个是我自己找到的 EXCEPT.INC 中的版本,是这样定义的:

__EXCEPTIONREGISTRATIONRECORD struc         prev_structure          dd      ?         ExceptionHandler        dd      ?         ExceptionFilter         dd      ?         FilterFrame             dd      ?         PExceptionInfoPtrs      dd      ? __EXCEPTIONREGISTRATIONRECORD ends

可是我在前面说过,这个定义简直就是胡扯。因为,可以肯定的是:这个结构中的 ExceptionFilter 就是 scopetable

指针,FilterFrame 就是当前的 trylevel。那么 PExceptionInfoPtrs 是什么?从名字上判断,这个就是指向
EXCEPTION_POINTERS 结构的指针。这个指针应该在这个位置出现吗?NO,这明明就是 _ebp 的位置嘛……所以我不知道这是一个在什么地方用到的结构。那么,在
EXSUP.INC 的注释中定义的 _EXCEPTION_REGISTRATION 就没有问题吗?答案仍然是否定的:

; 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; ; };

如果说这个结构中的 _ebp 成员还勉强说得过去的话,那么 xpointers 成员简直就是匪夷所思。因为据我所知,在堆栈中,_ebp

下存放的是 CALL 指令压入的返回地址,而不是什么 PEXCEPTION_POINTERS。一下子怀疑这么多问题,即怀疑
CRT 的汇编定义、又怀疑牛人的教导?是不是有点儿过分了……是的,我也觉得挺过分,但是我仍然坚持我的观点,因为我有事实替我说话。

在前面我提到过,我曾经跟踪了 VC 构造异常帧的代码,也就是在函数起始处由编译器自动生成的准备代码(Matt Pietrek

所说的 prologue code),现在就回过头来仔细看看编译器到底在堆栈上干了些什么:

01: 00411A10  push        ebp 02: 00411A11  mov         ebp,esp 03: 00411A13  push        0FFFFFFFFh 04: 00411A15  push        424020h 05: 00411A1A  push        offset @ILT+365(__except_handler3) (411172h) 06: 00411A1F  mov         eax,dword ptr fs:[00000000h] 07: 00411A25  push        eax 08: 00411A26  mov         dword ptr fs:[0],esp 09: 00411A2D  add         esp,0FFFFFF2Ch 10: 00411A33  push        ebx 11: 00411A34  push        esi 12: 00411A35  push        edi 13: 00411A36  lea         edi,[ebp-0E4h] 14: 00411A3C  mov         ecx,33h 15: 00411A41  mov         eax,0CCCCCCCCh 16: 00411A46  rep stos    dword ptr [edi] 17: 00411A48  mov         dword ptr [ebp-18h],esp

那么,当这段指令执行完毕后,堆栈应该是这个样子的:

00000000 → 低地址,栈顶
……
esp → edi 12: push edi
  esi 11: push esi
  ebx 10: push ebx
ebp-0E4h → ……
204 个字节全部填充为 0CCCCCCCCh
……
09: add esp,0FFFFFF2Ch
 
ebp-01Ch →
ebp-18h → prologue code 执行完成后的
esp
  ?
异常帧 → 之前的异常帧 FS:[0] 07: push fs:[0]
  __except_handler3 的地址 05: push __except_handler3
  424020h 04: push 424020h
ebp-4 → 0FFFFFFFFh 03: push 0FFFFFFFFh
ebp → 调用者的基址 ebp 01: push ebp
  CALL 指令压入的返回地址  
……
FFFFFFFF → 高地址,栈底

表格的第一列是 DWORD 数据单元的地址,第二列是堆栈中的内容,第三列是影响到 esp 的指令。

根据先前的理解,第 7 条指令执行完成后,异常帧结构就已经在堆栈上构造完成了,并且当前的栈顶指针 esp 所指的地址正是这个结构的首址,第

8 条指令就是将这个地址装入 FS:[0],做为新的异常 handler 链表的表头。那么,这个异常帧的结构此时就可以确定下来了。这时候再把上面提出的那两个异常帧结构套上去看看,怎么就觉得都不太对劲呢?第一个结构的
PExceptionInfoPtrs 成员对应到了保存的 ebp 的位置上,而第二个结构的 xpointers 成员所对应的数据就更离谱了——居然是返回地址?!

说一下表格中那个问号:为什么是问号呢?因为那个 DWORD 没有经过初始化。那么,为什么不初始化它呢?因为目前不知道该用什么值初始化它,也没有必要初始化它。是什么东西这么邪呼?其实,这个

DWORD 就是 _except_handler3 中的表达式 ((PBYTE)EstablisherFrame - 4)
引用到的那个 DWORD,也就是 EXCEPTION_POINTERS 结构的地址。回想一下 _except_handler3
的代码就可以意识到:EXCEPTION_POINTERS 结构是建立在 _except_handler3 堆栈上的临时变量,换句话说,这个结构的地址也只有在
_except_handler3 执行期间、也就是说有异常发生的时候才有意义。那么,目前我们显然拿这个 DWORD 没有办法,由它去吧。

此,可以得出结论:PEXCEPTION_POINTERS 存放在异常帧地址前的那个 DWORD 中,如果硬要把它“塞”到结构中,那也要放在
prev 的前面,怎么也不可能到最后去。所以这两个结构定义一个都不对!挺疯狂的结论,不是吗?而且有一个值得注意的现象:Matt Pietrek
在他的讲解中完整地引用了 EXSUP.INC 中的异常帧定义,却在他自己的 ShowSEHFrames 演示程序中也把这个成员从他的
VC_EXCEPTION_REGISTRATION
结构中“省略掉”了……我不是把自己的快乐建立在别人的痛苦之上的那一类人,所以与“找碴儿”相比,弄清问题的实质会带给我更多的快感。那么,VC
内部真正的异常帧究竟是什么样儿的?如果可能的话,我甚至连结构中的变量名都想知道。我很幸运,我最终真的知道了——这就是我在跟踪
_ValidateEH3RN 时的意外收获。

_ValidateEH3RN 在上面研究 _except_handler3 的时候提到过,它是用来对异常帧进行合法性验证的,它需要且仅需要用一个参数调用,就是一个

VC 的异常帧指针。Matt Pietrek 说的没错,CRT 中关于 SEH 的函数没有源代码可供参考。但幸运的是,Symbol
文件中的符号信息很充足,只要从 _except_handler3 函数中 step into 到 _ValidateEH3RN,就可以发现调式环境的“局部变量”窗口有了反应!首先出来的是一个
pRN 变量,有四个成员:

  1. Next: 展开之后发现还是一个 *pRN 结构
  2. ExceptionHandler: 值域中写着“__except_handler3”
  3. ScopeTable: 指向一个结构,展开之后有三个成员
    • EnclosingLevel: 值为 -1
    • FilterFunc: 把值敲入反汇编的“地址”窗口,可以定位到 filter 入口
    • HandlerFunc: 把值敲入反汇编的“地址”窗口,可以定位到 handler 入口
  4. TryLevel: 值为 0

呵呵,没错了,这个就是 VC 内部的异常帧结构了!再看看调用栈窗口,借了 _ValidateEH3RN 的光,连结构名都看到了:_EH3_EXCEPTION_REGISTRATION!而且

ScopeTable 的结构也可以看到了。不难发现这个结构中并没有那个“_ebp”成员:最后一个成员是 TryLevel。再回头看看
_except_handler3 的反汇编,就会发现一个规律:所有对“_ebp”的引用(也就是 [ebx+10h])全部都出现在
lea 指令中,这说明什么呢?这说明,_ebp 成员存在的意义只是为了取它的地址!那么 _ebp 成员的值是什么呢?如果把带有
_ebp 成员的 _EXCEPTION_REGISTRATION 结构套到上面的堆栈结构上就可以看出来:_ebp 成员正好处于“调用者的基址
ebp”那个 DWORD 上。也就是说,_ebp 成员确实是 ebp 寄存器的值,但却是上一个函数的 ebp,不是当前函数的。当前函数的
ebp 应该是这个 DWORD 的地址,而不是它的值!所以我前面说过,这是一个很迷惑人的成员,伪码中的第二个笔误必须改正,否则就会在上层函数的
ebp 上下文中执行当前函数的 Unwind 过程,那将是一个什么结果啊……

所以,目前 VC 中的异常帧结构中没有这个“_ebp”成员——显然没有必要,SEH 中大量的递归调用、“non-local-goto”和堆栈

Unwind、已经够让人头昏脑胀的了,这个成员只能把事情搞得更离谱。想要的 ebp 值紧接着当前 _EH3_EXCEPTION_REGISTRATION
结构的地址,只要 &pRN[1] 就可以取到了,实在没有必要为了取这个地址而强加上一个“_ebp”成员。

暂时告一段落

写到这里,我似乎可以松口气了:Matt Pietrek 的文章已经吃透了,_EH3_EXCEPTION_REGISTRATION

真正的结构也已经大白于天下了,VC 中的 SEH 处理似乎已经没有什么神秘的了,唯一剩下还没有研究过的就是 Unwind
过程。但这个过程完全封装在各个编译器厂商的内部实现中,与系统几乎没有关系,系统只负责发起 Unwind 调用,至于怎么
Unwind,系统也不知道。所以,虽然现在还不了解 Unwind,但它也已经是囊中之物了,只是目前还没有必要关心它。本着“师傅领进门、修行在个人”的精神,我又跟踪到
NTDLL.DLL 中的 Rtl 函数中转了一圈,不仅看到了 NT 中异常帧的具体结构,而且又发现了 Matt Pietrek
的伪码中与事实不符的地方——看上去这个地方涉及到嵌套异常处理甚至堆栈耗尽的问题……所以我打算单独写一篇文章好好分析一下这一部分。那么现在,我应该做的就是去洗个澡,然后舒舒服服地睡上一觉了。

相关文章
本文转自

转载地址:http://qgnob.baihongyu.com/

你可能感兴趣的文章
恢复IpTables的默认设置(Script)(转)
查看>>
用iptales实现包过虑型防火墙(一)(转)
查看>>
用iptables实现NAT(转)
查看>>
MYSQL(解决方法):Client does not support authentication(转)
查看>>
Oracle 游标使用大全(转)
查看>>
天龙八步-》打造debian-desktop-》安装声卡[五](转)
查看>>
在Debian系统中编译Linux内核需要安装的软件包(转)
查看>>
在Debian Woody (Stable) 下安装 2.6 内核(转)
查看>>
winrar功能两例(转)
查看>>
设置Win 98开机密码(转)
查看>>
windows命令集(转)
查看>>
p4调优(转)
查看>>
CSS文本:word-wrap(转)
查看>>
CSS文本:text-align(转)
查看>>
CSS文本:unicode-bidi(转)
查看>>
CSS文本:vertical-align(转)
查看>>
Window安全设置(转)
查看>>
wincvs自动checkin和checkout(转)
查看>>
操作系统比对脚本[inststuff.pl](转)
查看>>
Linux中的C语言妙用(转)
查看>>