对于结构化异常处理(SEH)的进一步探索

来源:互联网 发布:汛情数据统计 编辑:程序博客网 时间:2024/06/07 07:08
对于结构化异常处理(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 filterFuncRet008:     LONG trylevel009:     EXCEPTION_POINTERS exceptPtrs010:     PSCOPETABLE pScopeTable011: 012:     CLD     // Clear the direction flag (make no assumptions!)013: 014:     // if neither the EXCEPTION_UNWINDING nor EXCEPTION_EXIT_UNWIND bit015:     // is set...  This is true the first time through the handler (the016:     // non-unwinding case)017: 018:     if ( ! (pExceptionRecord->ExceptionFlags019:             & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )020:     {021:         // Build the EXCEPTION_POINTERS structure on the stack022:         exceptPtrs.ExceptionRecord = pExceptionRecord;023:         exceptPtrs.ContextRecord = pContextRecord;024: 025:         // Put the pointer to the EXCEPTION_POINTERS 4 bytes below the026:         // establisher frame.  See ASM code for GetExceptionInformation027:         *(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;028: 029:         // Get initial "trylevel" value030:         trylevel = pRegistrationFrame->trylevel 031: 032:         // Get a pointer to the scopetable array033:         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 EBP042: 043:                 // !!!Very Important!!!  Switch to original EBP.  This is044:                 // what allows all locals in the frame to have the same045:                 // value as before the exception occurred.046:                 EBP = &pRegistrationFrame->_ebp 047: 048:                 // Call the filter function049:                 filterFuncRet = scopetable[trylevel].lpfnFilter();050: 051:                 POP EBP                         // Restore handler frame EBP052: 053:                 if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )054:                 {055:                     if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION056:                         return ExceptionContinueExecution;057: 058:                     // If we get here, EXCEPTION_EXECUTE_HANDLER was specified059:                     scopetable == pRegistrationFrame->scopetable060: 061:                     // Does the actual OS cleanup of registration frames062:                     // Causes this function to recurse063:                     __global_unwind2( pRegistrationFrame );064: 065:                     // Once we get here, everything is all cleaned up, except066:                     // for the last frame, where we'll continue execution067:                     EBP = &pRegistrationFrame->_ebp068:                     069:                     __local_unwind2( pRegistrationFrame, trylevel );070: 071:                     // NLG == "non-local-goto" (setjmp/longjmp stuff)072:                     __NLG_Notify( 1 );  // EAX == scopetable->lpfnHandler073: 074:                     // Set the current trylevel to whatever SCOPETABLE entry075:                     // was being used when a handler was found076:                     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->previousTryLevel085: 086:             goto search_for_handler;087:         }088:         else    // trylevel == TRYLEVEL_NONE089:         {090:             retvalue == DISPOSITION_CONTINUE_SEARCH;091:         }092:     }093:     else    // EXCEPTION_UNWINDING or EXCEPTION_EXIT_UNWIND flags are set094:     {095:         PUSH EBP    // Save EBP096:         EBP = pRegistrationFrame->_ebp  // Set EBP for __local_unwind2097: 098:         __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )099: 100:         POP EBP     // Restore EBP101: 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_DISPOSITION2: __cdecl _except_handler(3:     struct _EXCEPTION_RECORD *ExceptionRecord,4:     void * EstablisherFrame,5:     struct _CONTEXT *ContextRecord,6:     void * DispatcherContext7: );

虽然这个定义中的函数名是 _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        ebp003: 004141A1  mov         ebp,esp004:     ; // EXCEPTION_POINTERS exceptPtrs;005: 004141A3  sub         esp,8006: 004141A6  push        ebx007: 004141A7  push        esi008: 004141A8  push        edi009: 004141A9  push        ebp010: 004141AA  cld011:     ; // EstablisherFrame => ebx012: 004141AB  mov         ebx,dword ptr [ebp+0Ch]013:     ; // ExceptionRecord => eax014: 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],6018: 004141B8  jne         _lh_unwinding (414269h)019:     ; // exceptPtrs.ExceptionRecord = ExceptionRecord020: 004141BE  mov         dword ptr [ebp-8],eax021:     ; // exceptPtrs.ContextRecord = ContextRecord;022: 004141C1  mov         eax,dword ptr [ebp+10h]023: 004141C4  mov         dword ptr [ebp-4],eax024:     ; // *(PDWORD)((PBYTE)EstablisherFrame - 4) = &exceptPtrs025: 004141C7  lea         eax,[ebp-8]026: 004141CA  mov         dword ptr [ebx-4],eax027:     ; // EstablisherFrame->trylevel => esi028: 004141CD  mov         esi,dword ptr [ebx+0Ch]029:     ; // EstablisherFrame->scopetable => edi030: 004141D0  mov         edi,dword ptr [ebx+8]031:     ; // if (_ValidateEH3RN(EstablisherFrame) == 0)032:     ; //     goto _lh_abort;033: 004141D3  push        ebx034: 004141D4  call        @ILT+775(__ValidateEH3RN) (41130Ch)035: 004141D9  add         esp,4036: 004141DC  or          eax,eax037: 004141DE  je          _lh_abort (41425Bh)038: _lh_top:039:     ; // if (trylevel == TRYLEVEL_NONE)040:     ; //     goto _lh_bagit;041: 004141E0  cmp         esi,0FFFFFFFFh042: 004141E3  je          _lh_bagit (414262h)043:     ; // EstablisherFrame->scopetable[trylevel].lpfnFilter => eax044: 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,eax049: 004141EE  je          _lh_continue (414249h)050:     ; // PUSH EBP051: 004141F0  push        esi052: 004141F1  push        ebp053:     ; // EBP = &EstablisherFrame->_ebp054: 004141F2  lea         ebp,[ebx+10h]055:     ; // ret = EstablisherFrame->scopetable[trylevel].lpfnFilter();056: 004141F5  xor         ebx,ebx057: 004141F7  xor         ecx,ecx058: 004141F9  xor         edx,edx059: 004141FB  xor         esi,esi060: 004141FD  xor         edi,edi061: 004141FF  call        eax062:     ; // POP EBP063: 00414201  pop         ebp064: 00414202  pop         esi065:     ; // EstablisherFrame => ebx066: 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,eax072: 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        ebx077: 00414210  call        @ILT+700(__global_unwind2) (4112C1h)078: 00414215  add         esp,4079:     ; // EBP = &EstablisherFrame->_ebp080: 00414218  lea         ebp,[ebx+10h]081:     ; // __local_unwind2(EstablisherFrame, trylevel);082: 0041421B  push        esi083: 0041421C  push        ebx084: 0041421D  call        @ILT+385(__local_unwind2) (411186h)085: 00414222  add         esp,8086:     ; // __NLG_Notify(1);087: 00414225  lea         ecx,[esi+esi*2]088: 00414228  push        1089: 0041422A  mov         eax,dword ptr [edi+ecx*4+8]090: 0041422E  call        @ILT+1045(__NLG_Notify) (41141Ah)091:     ; // EstablisherFrame->trylevel =092:     ; //     EstablisherFrame->scopetable[trylevel].previousTryLevel093: 00414233  mov         eax,dword ptr [edi+ecx*4]094: 00414236  mov         dword ptr [ebx+0Ch],eax095:     ; // EstablisherFrame->scopetable[trylevel].lpfnHandler();096: 00414239  mov         eax,dword ptr [edi+ecx*4+8]097: 0041423D  xor         ebx,ebx098: 0041423F  xor         ecx,ecx099: 00414241  xor         edx,edx100: 00414243  xor         esi,esi101: 00414245  xor         edi,edi102: 00414247  call        eax103: _lh_continue:104:     ; // EstablisherFrame->scopetable[trylevel].previousTryLevel => esi105: 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,0112: 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],8117: _lh_bagit:118:     ; // return ExceptionContinueSearch;119: 00414262  mov         eax,1120: 00414267  jmp         _lh_return (41427Eh)121: _lh_unwinding:122:     ; // PUSH EBP123: 00414269  push        ebp124:     ; // EBP = &EstablisherFrame->_ebp125: 0041426A  lea         ebp,[ebx+10h]126:     ; // __local_unwind2(EstablisherFrame, TRYLEVEL_NONE);127: 0041426D  push        0FFFFFFFFh128: 0041426F  push        ebx129: 00414270  call        @ILT+385(__local_unwind2) (411186h)130: 00414275  add         esp,8131:     ; // POP EBP132: 00414278  pop         ebp133:     ; // return ExceptionContinueSearch;134: 00414279  mov         eax,1135: _lh_return:136: 0041427E  pop         ebp137: 0041427F  pop         edi138: 00414280  pop         esi139: 00414281  pop         ebx140: 00414282  mov         esp,ebp141: 00414284  pop         ebp142: 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        ebp02: 00411A11  mov         ebp,esp03: 00411A13  push        0FFFFFFFFh04: 00411A15  push        424020h05: 00411A1A  push        offset @ILT+365(__except_handler3) (411172h)06: 00411A1F  mov         eax,dword ptr fs:[00000000h]07: 00411A25  push        eax08: 00411A26  mov         dword ptr fs:[0],esp09: 00411A2D  add         esp,0FFFFFF2Ch10: 00411A33  push        ebx11: 00411A34  push        esi12: 00411A35  push        edi13: 00411A36  lea         edi,[ebp-0E4h]14: 00411A3C  mov         ecx,33h15: 00411A41  mov         eax,0CCCCCCCCh16: 00411A46  rep stos    dword ptr [edi]17: 00411A48  mov         dword ptr [ebp-18h],esp

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

00000000 →低地址,栈顶……esp →edi12: push edi esi11: push esi ebx10: push ebxebp-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 424020h04: push 424020hebp-4 →0FFFFFFFFh03: push 0FFFFFFFFhebp →调用者的基址 ebp01: 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 的伪码中与事实不符的地方——看上去这个地方涉及到嵌套异常处理甚至堆栈耗尽的问题……所以我打算单独写一篇文章好好分析一下这一部分。那么现在,我应该做的就是去洗个澡,然后舒舒服服地睡上一觉了。

相关文章
  • 深入研究 Win32 结构化异常处理
  • NT 中的异常帧结构和异常嵌套
0 0
原创粉丝点击