对于结构化异常处理(SEH)的进一步探索
来源:互联网 发布:汛情数据统计 编辑:程序博客网 时间:2024/06/07 07:08
对 _except_handler3 伪码的勘误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 机制就显得十分必要。
激动人心的旅程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 的代码究竟是什么样子的。
_EH3_EXCEPTION_REGISTRATION 结构的本来面目我知道作出这个决定后肯定要经历一个痛苦的过程,但我仍然义无反顾地开了 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 对齐什么的……在这里出现也并不稀奇。我也没有对这个函数做深入研究,只是跟进去看了一眼,但是却有了意外的发现。
暂时告一段落到目前为止,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 变量,有四个成员:
- Next: 展开之后发现还是一个 *pRN 结构
- ExceptionHandler: 值域中写着“__except_handler3”
- ScopeTable: 指向一个结构,展开之后有三个成员
- EnclosingLevel: 值为 -1
- FilterFunc: 把值敲入反汇编的“地址”窗口,可以定位到 filter 入口
- HandlerFunc: 把值敲入反汇编的“地址”窗口,可以定位到 handler 入口
- 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 中的异常帧结构和异常嵌套
- 对于结构化异常处理(SEH)的进一步探索
- 对于结构化异常处理(SEH)的进一步探索
- 对于结构化异常处理(SEH)的进一步探索
- 对于结构化异常处理(SEH)的进一步探索
- 对于结构化异常处理(SEH)的进一步探索
- 深入探索Win32结构化异常处理SEH
- 关于SEH(结构化异常处理)的一些知识
- SEH结构化异常处理
- 结构化异常处理SEH
- SEH结构化异常处理
- 结构化异常处理SEH
- SEH:结构化异常处理
- 结构化异常处理(SEH)
- SEH(结构异常处理)
- [读书笔记] 关于Windows的结构化异常处理SEH(一)
- [读书笔记] 关于Windows的结构化异常处理SEH(二)
- Win32 结构化异常处理(SEH)探秘(续)
- Win32 结构化异常处理(SEH)探秘
- AndroidStudio或IDEA中连接github失败:Can't login: Connect to api.github.com:443
- Unity协程(Coroutine)原理深入剖析
- 基于JAVA的黄金数据接口调用代码实例
- 3Sum Closest
- 实现页面登录后仍然跳回当前页面
- 对于结构化异常处理(SEH)的进一步探索
- HTTP响应头信息和请求头信息详解
- 03 Struts2入门案例
- java线程
- Java JSP(一) 基本原理和九个隐含对象
- Java正则解析命令行中大括号中的命令
- [bzoj1609]: [Usaco2008 Feb]Eating Together麻烦的聚餐 递推
- 使用Dagger 2进行依赖注入 - API介绍
- QT用API获得文件的版本信息