我知道作出这个决定后肯定要经历一个痛苦的过程,但我仍然义无反顾地开了 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 对齐什么的……在这里出现也并不稀奇。我也没有对这个函数做深入研究,只是跟进去看了一眼,但是却有了意外的发现。