NT 中的异常帧结构和异常嵌套

冲动是魔鬼

本文是《对于结构化异常处理(SEH)的进一步探索》的姊妹篇。所以,如果不先看看那篇文章的话,本文八成会给人不知所云的感觉。但是,那篇文章也是基于 Matt Pietrek 的 《深入研究 Win32 结构化异常处理》的,所以在那篇文章的开头也会强烈建议先看看 Matt Pietrek 的文章——那么我就在这里先打个招呼,免的首先翻到这篇文章的同志有可能会觉得我不厚道 :-p 我本想写完那篇《对于结构化异常处理(SEH)的进一步探索》后就结束对 SEH 的“理论学习”,开始试着写一些异常处理和崩溃点信息收集的代码。但老天爷偏偏又让我注意到了 Matt Pietrek 给出的伪码中的另一个小细节。那就是,在 RtlDispatchException 的伪码中,有这么一段:

if ( 0 == pRegistrationFrame )
{
    pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL;   // Turn off flag
}

这段代码要做的就是当条件满足的时候把 EH_NESTED_CALL 标志位从异常标志位中去掉,也就是在注释中写的“Turn off flag”。从伪码中看,这个条件就是“0 == pRegistrationFrame”——很奇怪的条件不是么?说它奇怪,是因为:如果异常链表的结尾是用空指针值表示的,那么这样判断似乎还有几分道理,而实际情况却是,异常链表的结尾是用 -1 表示的。换句话说,pRegistrationFrame “从头到尾”就没有等于 0 的时候。我意识到大概 Matt Pietrek 又犯错误了,而且这个地方似乎涉及到系统在异常嵌套时的行为。于是我头脑一热,决定再次跟踪到 RtlDispatchException 的机器码中去一探究竟。

RtlDispatchException 完全攻略

关于我是如何看到 RtlDispatchException 的反汇编的,我已经在《对于结构化异常处理(SEH)的进一步探索》一文中详细描述过了。进入 RtlDispatchException 的方法与进入 _except_handler3 的方法无异,故这里不再赘述,直接切入正题。RtlDispatchException 的代码并不比 _except_handler3 复杂,所以不费什么劲我就在反汇编指令中插入了伪码:

001: _RtlDispatchException@8:
002: 77FACBB2  push        ebp
003: 77FACBB3  mov         ebp,esp
004:     ; // DWORD yetAnotherValue;     [ebp-4]
005:     ; // DWORD stackUserBase;       [ebp-8]
006:     ; // DWORD stackUserTop;        [ebp-0Ch]
007:     ; // DWORD dispatcherContext;   [ebp-10h]
008:     ; // EXCEPTION_RECORD localRec; [ebp-60h]
009: 77FACBB5  sub         esp,60h
010: 77FACBB8  push        ebx
011: 77FACBB9  push        esi
012:     ; // RtlpGetStackLimits(&stackUserBase, &stackUserTop);
013: 77FACBBA  lea         eax,[ebp-0Ch]
014: 77FACBBD  push        edi
015: 77FACBBE  push        eax
016: 77FACBBF  lea         eax,[ebp-8]
017: 77FACBC2  push        eax
018: 77FACBC3  call        _RtlpGetStackLimits@8 (77FBB3A0h)
019:     ; // pRegistrationFrame = RtlpGetRegistrationHead();
020: 77FACBC8  call        _RtlpGetRegistrationHead@0 (77FBB3BCh)
021:     ; // yetAnotherValue = 0;
022: 77FACBCD  and         dword ptr [ebp-4],0
023:     ; // while (pRegistrationFrame != -1) {
024: 77FACBD1  mov         ebx,eax
025: 77FACBD3  cmp         ebx,0FFFFFFFFh
026: 77FACBD6  je          _RtlDispatchException@8+0F7h (77FACCA9h)
027:     ; //     pExceptRec => esi
028: 77FACBDC  mov         esi,dword ptr [ebp+8]
029:     ; //     if (pRegistrationFrame < stackUserBase)
030:     ; //         goto _lh_bad_stack;
031: 77FACBDF  cmp         ebx,dword ptr [ebp-8]
032: 77FACBE2  lea         eax,[ebx+8]
033: 77FACBE5  jb          _RtlDispatchException@8+0F3h (77FACCA5h)
034:     ; //     if (pRegistrationFrame > stackUserTop)
035:     ; //         goto _lh_bad_stack;
036: 77FACBEB  cmp         eax,dword ptr [ebp-0Ch]
037: 77FACBEE  ja          _RtlDispatchException@8+0F3h (77FACCA5h)
038:     ; //     if (pRegistrationFrame & 3)
039:     ; //         goto _lh_bad_stack;
040: 77FACBF4  test        bl,3
041: 77FACBF7  jne         _RtlDispatchException@8+0F3h (77FACCA5h)
042:     ; //     // Doesn't seem to do a whole heck of a lot.
043:     ; //     if (someProcessFlag)
044: 77FACBFD  test        byte ptr [_NtGlobalFlag+2 (77FCFC32h)],80h
045: 77FACC04  je          _RtlDispatchException@8+65h (77FACC17h)
046:     ; //         pExceptRec = RtlpLogExceptionHandler(pExcptRec, pContext, 0,
047:     ; //                                              pRegistrationFrame, 0x10);
048: 77FACC06  push        10h
049: 77FACC08  push        ebx
050: 77FACC09  push        0
051: 77FACC0B  push        dword ptr [ebp+0Ch]
052: 77FACC0E  push        esi
053: 77FACC0F  call        _RtlpLogExceptionHandler@20 (77FB4A40h)
054: 77FACC14  mov         dword ptr [ebp+8],eax
055:     ; //     retValue = RtlpExecuteHandlerForException(pExceptRec,
056:     ; //         pRegistrationFrame, pContext, &dispatcherContext,
057:     ; //         pRegistrationFrame->handler);
058: 77FACC17  push        dword ptr [ebx+4]
059: 77FACC1A  lea         eax,[ebp-10h]
060: 77FACC1D  push        eax
061: 77FACC1E  push        dword ptr [ebp+0Ch]
062: 77FACC21  push        ebx
063: 77FACC22  push        esi
064: 77FACC23  call        _RtlpExecuteHandlerForException@20 (77FBB23Ch)
065:     ; //     // Doesn't seem to do a whole heck of a lot.
066:     ; //     if (someProcessFlag)
067: 77FACC28  test        byte ptr [_NtGlobalFlag+2 (77FCFC32h)],80h
068: 77FACC2F  mov         edi,eax
069: 77FACC31  je          _RtlDispatchException@8+8Ah (77FACC3Ch)
070:     ; //         RtlpLogLastExceptionDisposition(pExceptRec, retValue);
071: 77FACC33  push        edi
072: 77FACC34  push        dword ptr [ebp+8]
073: 77FACC37  call        _RtlpLogLastExceptionDisposition@8 (77FB4A46h)
074:     ; //     if (yetAnotherValue == pRegistrationFrame) {
075: 77FACC3C  cmp         dword ptr [ebp-4],ebx
076: 77FACC3F  jne         _RtlDispatchException@8+97h (77FACC49h)
077:     ; //         pExceptRec->ExceptionFlags &= ~EH_NESTED_CALL;
078: 77FACC41  and         dword ptr [esi+4],0FFFFFFEFh
079:     ; //         yetAnotherValue = 0;
080: 77FACC45  and         dword ptr [ebp-4],0
081:     ; //     }
082:     ; //     if (retValue != DISPOSITION_DISMISS) {
083: 77FACC49  mov         eax,edi
084: 77FACC4B  xor         ecx,ecx
085: 77FACC4D  sub         eax,ecx
086: 77FACC4F  je          _RtlDispatchException@8+0BFh (77FACC71h)
087:     ; //         if (retValue != DISPOSITION_CONTINUE_SEARCH) {
088: 77FACC51  dec         eax
089: 77FACC52  je          _RtlDispatchException@8+0E2h (77FACC94h)
090:     ; //             if (retValue != DISPOSITION_NESTED_EXCEPTION) {
091: 77FACC54  dec         eax
092: 77FACC55  je          _RtlDispatchException@8+0AEh (77FACC60h)
093:     ; //                 localRec.ExceptionCode = STATUS_INVALID_DISPOSITION;
094: 77FACC57  mov         dword ptr [ebp-60h],0C0000026h
095:     ; //                 goto _lh_raise_exception;
096: 77FACC5E  jmp         _RtlDispatchException@8+0CCh (77FACC7Eh)
097:     ; //             } else { // retValue == DISPOSITION_NESTED_EXCEPTION
098:     ; //                 dispatcherContext => eax
099: 77FACC60  mov         eax,dword ptr [ebp-10h]
100:     ; //                 pExceptRec->ExceptionFlags |= EH_NESTED_CALL;
101: 77FACC63  or          dword ptr [esi+4],10h
102:     ; //                 if (dispatcherContext > yetAnotherValue)
103: 77FACC67  cmp         eax,dword ptr [ebp-4]
104: 77FACC6A  jbe         _RtlDispatchException@8+0E2h (77FACC94h)
105:     ; //                     yetAnotherValue = dispatcherContext;
106: 77FACC6C  mov         dword ptr [ebp-4],eax
107:     ; //             }
108:     ; //         }
109: 77FACC6F  jmp         _RtlDispatchException@8+0E2h (77FACC94h)
110:     ; //     } else { // retValue == DISPOSITION_DISMISS
111:     ; //         if (!(pExceptRec->ExceptionFlags & EH_NONCONTINUABLE))
112:     ; //             goto _lh_continue_execution;
113: 77FACC71  test        byte ptr [esi+4],1
114: 77FACC75  je          _RtlDispatchException@8+0EFh (77FACCA1h)
115:     ; //         localRec.ExceptionCode = STATUS_NONCONTINUABLE_EXCEPTION;
116: 77FACC77  mov         dword ptr [ebp-60h],0C0000025h
117: 77FACC7E  lea         eax,[ebp-60h]
118:     ; // _lh_raise_exception:
119:     ; //         localRec.ExceptionFlags = EH_NONCONTINUABLE;
120: 77FACC81  mov         dword ptr [ebp-5Ch],1
121: 77FACC88  push        eax
122:     ; //         localRec.ExceptionRecord = pExceptRec;
123: 77FACC89  mov         dword ptr [ebp-58h],esi
124:     ; //         localRec.NumberParameters = 0;
125: 77FACC8C  mov         dword ptr [ebp-50h],ecx
126:     ; //         RtlRaiseException(&localRec);
127: 77FACC8F  call        _RtlRaiseException@4 (77FAC4A0h)
128:     ; //     }
129:     ; //     pRegistrationFrame = pRegistration->prev;
130: 77FACC94  mov         ebx,dword ptr [ebx]
131:     ; // }
132: 77FACC96  cmp         ebx,0FFFFFFFFh
133: 77FACC99  jne         _RtlDispatchException@8+2Dh (77FACBDFh)
134:     ; // return 0;
135: 77FACC9F  jmp         _RtlDispatchException@8+0F7h (77FACCA9h)
136:     ; // _lh_continue_execution:
137:     ; // return 1;
138: 77FACCA1  mov         al,1
139: 77FACCA3  jmp         _RtlDispatchException@8+0F9h (77FACCABh)
140:     ; // _lh_bad_stack:
141:     ; // pExceptRec->ExceptionFlags |= EH_STACK_INVALID;
142:     ; // return 0;
143: 77FACCA5  or          dword ptr [esi+4],8
144: 77FACCA9  xor         al,al
145: 77FACCAB  pop         edi
146: 77FACCAC  pop         esi
147: 77FACCAD  pop         ebx
148: 77FACCAE  leave
149: 77FACCAF  ret         8

为了与 Matt Pietrek 的伪码对比,我尽量去保留他伪码中的变量名和结构。但是为了不使伪码与反汇编指令偏离得太远,我改变了中间判断 RtlpExecuteHandlerForException 返回值的逻辑结构,改变后的伪码与 Matt Pietrek 给出的伪码在效果上是一样的。虽然看上去可能怪怪的,但是我的伪码更贴近机器指令的执行逻辑,基本上每一行伪码对应的反汇编指令都在伪码下面的几行了,很容易对比。可以发现,在我曾经提出过疑问的地方(74 与 81 行之间),Matt Pietrek 的伪码确实有些不对头。这个地方真正的伪码应该是:

if (yetAnotherValue == pRegistrationFrame)
{
    pExceptRec->ExceptionFlags &= ~EH_NESTED_CALL;
    yetAnotherValue = 0;
}

另外,在伪码的第 100 行,也就是反汇编指令第 101 行,我看到的是 or 上了一个 EH_NESTED_CALL(10h),而不是 Matt Pietrek 的伪码中所写的 EH_EXIT_UNWIND(4)。这里说明一下:EXSUP.INC 中对异常标志位的定义只到了 EXCEPTION_STACK_INVALID(8),也就是伪码中的 EH_STACK_INVALID,我是怎么知道 EH_NESTED_CALL 是 10h 呢?说实话,我也不知道,但我在 Matt Pietrek 的 MYSEH2.cpp 中看到了他识别异常标志位的过程,其中 0x10 对应的标志位名称就是“EH_NESTED_CALL”,况且根据这里的上下文判断,这个标志位 0x10 就应该是 EH_NESTED_CALL。

看来一定有些事情 Matt Pietrek 没有在他的文章中提到(是的,这就是异常嵌套的情况)。Matt Pietrek 只在他文章的最后提了一下在异常回调函数中又发生异常的情况是有可能的,说 ExecuteHandler 在调用任何回调函数之前又在它自己的栈上建立了一个异常帧,因为“如果异常回调引起了另一个异常,操作系统需要知道此事件”。那么,如果这种情况出现了,操作系统是如何处理的呢?异常线程最后的归宿在哪里呢?所有这些问题的答案可以说都在 ExecuteHandler 所建立的那个异常帧中,那个异常帧的结构确实像 Matt Pietrek 所说的“和我在 MYSEH 和 MYSEH2 程序里使用的差不多”,但恰恰就是相差的那一点点才是 NT 异常帧结构中的关键。Matt Pietrek 在他给出的 ExecuteHandler 的伪码中把这一点再一次“省略掉”了。但现在,我要把它揪出来。

NT 中的异常帧结构

NT 中的异常帧是在 NTDLL.DLL 中的 ExecuteHandler 函数中建立的。所以,如果我们想要了解这个异常帧的结构,就需要跟踪到这个函数中去。Matt Pietrek 已经说过了,ExecuteHandler 有两个不同的前端(front end),在异常发生和 Unwind 阶段从不同的前端进入 ExecuteHandler。异常发生时使用的前端是 RtlpExecuteHandlerForException。在上面我们已经找到了 RtlDispatchException,正是它调用了 RtlpExecuteHandlerForException,所以不费什么劲就可以看到 ExecuteHandler 了:

01: _RtlpExecuteHandlerForException@20:
02: 77FBB23C  mov         edx,offset ExecuteHandler@20+3Ah (77FBB286h)
03: 77FBB241  jmp         ExecuteHandler@20 (77FBB24Ch)
04: 77FBB243  nop
05: _RtlpExecuteHandlerForUnwind@20:
06: 77FBB244  mov         edx,offset ExecuteHandler@20+61h (77FBB2ADh)
07: 77FBB249  lea         ecx,[ecx]
08: ExecuteHandler@20:
09: 77FBB24C  push        ebp
10: 77FBB24D  mov         ebp,esp
11: 77FBB24F  push        dword ptr [ebp+0Ch]    ; push [pExcptReg]
12: 77FBB252  push        edx                    ; push [nthandler]
13: 77FBB253  push        dword ptr fs:[0]       ; push [prev]
14: 77FBB25A  mov         dword ptr fs:[0],esp
15: 77FBB261  push        dword ptr [ebp+14h]
16: 77FBB264  push        dword ptr [ebp+10h]
17: 77FBB267  push        dword ptr [ebp+0Ch]
18: 77FBB26A  push        dword ptr [ebp+8]
19: 77FBB26D  mov         ecx,dword ptr [ebp+18h]
20: 77FBB270  call        ecx
21: 77FBB272  mov         esp,dword ptr fs:[0]
22: 77FBB279  pop         dword ptr fs:[0]
23: 77FBB280  mov         esp,ebp
24: 77FBB282  pop         ebp
25: 77FBB283  ret         14h
26:     ; _nthandler_excpt@10
27:     ; // EXCEPTION_DISPOSITION __stdcall nthandler_excpt(
28:     ; //     PEXCEPTION_RECORD pExcptRec,
29:     ; //     NT_EXCEPTION_REGISTRATION *pExcptReg,
30:     ; //     PCONTEXT pContext,
31:     ; //     PDWORD pDispatcherContext)
32:     ; // {
33:     ; //     if (pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT)
34: 77FBB286  mov         ecx,dword ptr [esp+4]
35: 77FBB28A  test        dword ptr [ecx+4],6
36:     ; //         return ExceptionContinueSearch;
37: 77FBB291  mov         eax,1
38: 77FBB296  jne         ExecuteHandler@20+5Eh (77FBB2AAh)
39:     ; //     *pDispatcherContext = pExcptReg->_context;
40: 77FBB298  mov         ecx,dword ptr [esp+8]
41: 77FBB29C  mov         edx,dword ptr [esp+10h]
42: 77FBB2A0  mov         eax,dword ptr [ecx+8]
43: 77FBB2A3  mov         dword ptr [edx],eax
44:     ; //     return ExceptionNestedException;
45: 77FBB2A5  mov         eax,2
46:     ; // }
47: 77FBB2AA  ret         10h
48:     ; _nthandler_unwind@10
49: 77FBB2AD  mov         ecx,dword ptr [esp+4]
50: 77FBB2B1  test        dword ptr [ecx+4],6
51: 77FBB2B8  mov         eax,1
52: 77FBB2BD  je          ExecuteHandler@20+85h (77FBB2D1h)
53: 77FBB2BF  mov         ecx,dword ptr [esp+8]
54: 77FBB2C3  mov         edx,dword ptr [esp+10h]
55: 77FBB2C7  mov         eax,dword ptr [ecx+8]
56: 77FBB2CA  mov         dword ptr [edx],eax
57: 77FBB2CC  mov         eax,3
58: 77FBB2D1  ret         10h

这段代码其实包含了 5 个函数,其中三个已经有了 Symbol 了,可以看到它们的名字:_RtlpExecuteHandlerForException@20、_RtlpExecuteHandlerForUnwind@20 和 ExecuteHandler@20。另外两个就是 ExecuteHandler 在调用异常回调函数之前在栈上建立的 NT 异常帧中的内部 handler,一个负责异常回调阶段的异常处理,我把它叫做 nthandler_excpt;另一个负责 Unwind 阶段的异常处理,我把它叫做 nthandler_unwind。虽然没有 Symbol 信息,但是它们的起始地址已经可以看到了。根据研究的重点,我仅写出了 nthandler_excpt 的伪码(这段伪码已经不怎么“伪”了,直接称其为代码也不过分)。但这几个函数都非常简单,相信如果需要的话,其它函数的伪码也可以信手拈来。

先看看 ExecuteHandler 中安装异常帧的那几条指令,在构造“标准”异常帧结构的两条 push 指令之前还有一个 push!是的,NT 的异常帧在继 prev、handler 域之后,还带有一个 DWORD!我把这个 DWORD 命名为 _context(Matt Pietrek 在他的伪码中把这个成员也叫做 scopetable。但我认为非常不合适:scopetable 是 VC 的东西,不是 NT 的,NT 可没有什么 TryLevel 之类的概念),那么,NT 的异常帧结构体就可以描述为:

typedef struct _NT_EXCEPTION_REGISTRATION
{
    struct _NT_EXCEPTION_REGISTRATION *prev;
    FARPROC handler;
    DWORD _context;
} NT_EXCEPTION_REGISTRATION, *PNT_EXCEPTION_REGISTRATION;

NT 要那个 _context 在它的异常帧中做什么呢?结合 nthandler_excpt 和 RtlDispatchException 的伪码,我的结论是:为了正确地控制 EH_NESTED_CALL 标志位。

异常嵌套

什么是异常嵌套?简单的说,就是在处理一个异常的时候引发了另一个异常。为了更直观地重现一个异常嵌套的过程,我写了一个演示程序:

01: int Filter(PEXCEPTION_POINTERS pExcptPtrs, int nLevel)
02: {
03:     printf("In Filter %d...", nLevel);
04:     if (pExcptPtrs->ExceptionRecord->ExceptionFlags & 0x10)
05:         printf("somebody messed up!!!");
06:     else {
07:         printf("seems to be OK.");
08:         if (nLevel == 3 || nLevel == 6) {
09:             printf(" Now doing something bad.../n");
10:             int *p = 0;
11:             *p = 0;
12:         }
13:     }
14:     printf("/n");
15:     return EXCEPTION_CONTINUE_SEARCH;
16: }
17: 
18: void Foo(int nLevel)
19: {
20:     __try {
21:         if (nLevel > 1)
22:             Foo(nLevel - 1);
23:         int *p = 0;
24:         *p = 0;
25:     } __except(Filter(GetExceptionInformation(), nLevel)) {
26:     }
27: }
28: 
29: int _tmain(int argc, _TCHAR* argv[])
30: {
31:     __try {
32:         Foo(9);
33:     } __except(EXCEPTION_EXECUTE_HANDLER) {
34:     }
35:     return 0;
36: }

为了使代码看上去不那么冗长,我用了递归,另外在 Filter 上也加了一个参数以便 Filter 的行为可以有所不同。你可以把这段程序想像着展开:这个程序中包含了九个 FooN() 函数,Foo9 调用 Foo8、Foo8 调用 Foo7……Foo2 调用 Foo1,在 Foo1 中导致一个 AV 异常;每个 FooN 中都安装了一个 FilterN,Foo9 安装了 Filter9、Foo8 安装了 Filter8……Foo1 安装了 Filter1;其中,Filter3 和 Filter6 是两个捣乱的家伙,它们会导致另外一个 AV 异常。这个程序的输出是这样的:

In Filter 1...seems to be OK.
In Filter 2...seems to be OK.
In Filter 3...seems to be OK. Now doing something bad...
In Filter 1...somebody messed up!!!
In Filter 2...somebody messed up!!!
In Filter 3...somebody messed up!!!
In Filter 4...seems to be OK.
In Filter 5...seems to be OK.
In Filter 6...seems to be OK. Now doing something bad...
In Filter 1...somebody messed up!!!
In Filter 2...somebody messed up!!!
In Filter 3...somebody messed up!!!
In Filter 4...somebody messed up!!!
In Filter 5...somebody messed up!!!
In Filter 6...somebody messed up!!!
In Filter 7...seems to be OK.
In Filter 8...seems to be OK.
In Filter 9...seems to be OK.

为什么会产生这样的输出呢?来分析一下程序的执行流程:当异常在 Foo1(也就是 Foo(1) 的那次调用)中发生后,Filter1 首先被执行,这是个没有问题的 filter,它返回 EXCEPTION_CONTINUE_SEARCH 表示不予处理,Filter2 也是这样的过程。当流程进入 Filter3 这个捣蛋鬼时,它引发了另一个 AV 异常——嵌套发生了!于是系统又从当前异常回调链表的表头开始一个个地回调 handler,由于 EH_NESTED_CALL(0x10) 标志位的指示,Filter 们不再进行任何操作就直接返回 EXCEPTION_CONTINUE_SEARCH——包括 Filter3 这时候也老实下来了。当三个 Filter 调用过去以后,由于我们已经跳出了 Filter3 所引起的嵌套块、EH_NESTED_CALL 标志位被清除了,所以 Filter4、Filter5、Filter6 的报告又是正常的了。因为 Filter6 是另一位不良公民,所以同样的异常嵌套情况在 Filter6 中又发生了一遍,但是一旦第二次出了 Filter6 之后,Filter7、Filter8、Filter9 的报告就又是正常的了。

好险啊!EH_NESTED_CALL 标志位真是一根救命稻草!多亏这个标志位及时地为 Filter 指出了当前的嵌套情况,否则 Filter3、Filter6 不知道要错到什么时候(至于嵌套到最后是什么结果,我后面会提到,在这里先打个招呼——进程会死得相当惨)。我们看到的事实是:EH_NESTED_CALL 标志位的逻辑控制得相当完美:该置的时候一定会置,不该置的时候也绝不会误报。对于写 filter 的人来说,这个标志位是系统控制的,只要坐享其成就可以了。但本文的研究内容就是异常嵌套,所以现在还不是休息的时候。正相反,我们刚刚走上正轨。

NT 异常帧中的伎俩

文章写到这里,我想已经有人能猜到我下面要说什么了。是的,我在上一节中制造了一个异常嵌套并提出了 EH_NESTED_CALL 的重要性,在上一节的上一节中找到了 NT 异常帧结构中的 _context 成员,并且曾经埋下过一个伏笔:这个 _context 成员就是为了正确地控制 EH_NESTED_CALL 标志位。那么它是如何控制 EH_NESTED_CALL 标志位的呢?其实问题的答案已经全部包含在上面给出的伪码中了。

再次注意到 ExecuteHandler 的反汇编中那三条构造异常帧的 push 指令,第一条 push 指令压入的就是 NT_EXCEPTION_REGISTRATION 结构的最后一个成员,也就是 _context 成员的值。那么这条语句压入了一个什么值呢?是 [ebp+0Ch],也就是第 2 个参数的值。在 Matt Pietrek 的伪码中可以看到:ExecuteHandler 的第 2 个参数是异常帧指针,这也就是说:在执行任何 handler 之前,NT 都会悄悄地构造一个 NT_EXCEPTION_REGISTRATION 结构所描述的异常帧并插入 TIB 中的异常回调链(甚至在发生异常嵌套后、即将调用的是 nthandler_excpt 时也不例外——挺有趣的,但符合逻辑),这个 NT_EXCEPTION_REGISTRATION 结构的 _context 成员的值就是将要回调的 handler 所在的异常帧结构的地址。

如果 ExecuteHandler 所调用的异常回调 handler 在执行的过程中又发生了异常(也就是发生了异常嵌套的话),系统并不会做什么特殊处理,它将仍然走 [Kernel] => KiUserExceptionDispatcher => RtlDispatchException => RtlpExecuteHandlerForException => ExecuteHandler 的调用路径。只不过,这次将要被执行的异常回调将是 nthandler_excpt(还记得吗?在上一次进入 ExecuteHandler 的时候安装的这个 NT 异常帧)。那么,如果不幸进入了 nthandler_excpt,它会做些什么呢? 伪码中的行为是:将 NT 异常帧中 _context 的值填入 ExecuteHandler 的第 4 个参数 pDispatcherContext 所指向的 DWORD 中,然后返回 2,也就是 ExceptionNestedException。返回到 ExecuteHandler 以后,它也不做什么别的事情,只是将刚刚安装的 NT 异常帧卸载,然后携带 eax 中的值继续返回到 RtlDispatchException 中。与伪码中出现的情况一样,对 EH_NESTED_CALL 标志位的控制也都在这个函数中完成。

ExecuteHandler 返回到 RtlDispatchException 之后(也就是 RtlpExecuteHandlerForException 返回,伪码中第 55 行的调用),RtlDispatchException 开始检查返回值。我们暂且先跳过 74 行和 81 行之间清空 EH_NESTED_CALL 标志位的伪码,一会儿再看,现在指令流来到了第 99 行。这段代码首先给当前异常加上 EH_NESTED_CALL 标志位,然后进行了一个比较判定 if (dispatcherContext > yetAnotherValue),若判定成功,则将本地变量 yetAnotherValue 的值置为 dispatcherContext 的值。我们刚刚分析过:在异常嵌套发生后,nthandler_excpt 会被调用,它会在传入 ExecuteHandler 的第 4 个参数 pDispatcherContext 所指向的 DWORD(也就是 RtlDispatchException 中的局部变量 dispatcherContext)中填入 NT 异常帧结构中 _context 结构的值。那么 _context 的值是多少呢?最绕人的地方就在这里了。但也并不难想清楚:刚才之所以能够进入 nthandler_excpt,是因为向 RtlpExecuteHandlerForException 传入了一个 NT 异常帧,这个异常帧的 handler 成员指向 nthandler_excpt 的地址。但别忘了:这个 NT 异常帧却是在上一次发生异常之前注册的,因为本次注册的 NT 异常帧已经在 ExecuteHandler 返回之前卸载了——本次的异常回调函数是 nthandler_excpt,它没有产生异常,ExecuteHandler 是正常结束的。那么,上一次发生异常时在什么时候?别忘了我们现在的讨论所基于的“上下文”是异常嵌套,也就是说,上一次异常是在某个外部的异常回调函数(对于 VC 编译出来的程序来说,就是 _except_handler3)中发生的异常。那么,根据 ExecuteHandler 构造异常帧的方法——在构造每一个 NT 异常帧时都在 _context 域填上即将调用的 handler 所对应的异常帧的地址,这个 _context 中的内容就是上一次调用 handler 前的异常帧地址(同样以 VC 编译出来的程序为例,就是一个 _EH3_EXCEPTION_REGISTRATION 结构的地址)!

知道 _context 的含义以后,我们就离真相大白不远了。现在想想这个问题,都会觉得已经有些眉目了:在每一次调用异常回调函数之前记住它们所处的异常帧的地址,这样,如果有哪个回调函数不老实、引发了异常嵌套,我们就知道是谁捣的鬼了。在发现了异常嵌套之后,仍然会从 TIB 中的异常回调链表表头开始一个一个地调用 handler,但我们这次就可以知道了:在这个 _context 所指向的异常帧之前(当然也要包括该帧,因为就是该帧在上一次回调过程中引发了异常嵌套)的帧都属于“嵌套调用”,需要有 EH_NESTED_CALL 标志位指示它们的 handler——“情况特殊,请酌情处理”;如果上一次引发异常嵌套的帧(也就是 _context 所指向的帧)在本次没有再引起嵌套并且返回了,那就说明嵌套调用过程结束了,从下一个 handler 开始就是正常的回调了,此时应该清除 EH_NESTED_CALL 标志位了——此时再回头看看我在上一段暂时跳过的伪码 74 至 81 行间的内容,它做的不正是这件事么?

至此,所有的问题似乎都有了令人满意的答案。但其实还有一个细节值得关注一下,而且这个细节虽小,却可以说是生死攸关,因为它直接影响到 EH_NESTED_CALL 在逻辑上的正确性。在刚才的讨论中,我们已经提到过了,在发现 RtlpExecuteHandlerForException 返回 ExceptionNestedException 之后,RtlDispatchException 并没有简单地令 yetAnotherValue 等于 dispatcherContext,而是先进行了一个判定:只有当 dispatcherContext 大于 yetAnotherValue 时才会进行这个赋值操作。这又是为什么呢?

要得到这个问题的答案,我们首先要考虑一下:当异常嵌套发生后,TIB 中的异常回调链表是什么样子的?仍然以我在上面提出来的演示程序为例,当 Filter3 制造了一个 AV 异常之后,异常回调链表的表头将会是 nthandler_excpt,而非 _except_handler3,这种状况将一直持续到发生 Unwind 或者 NtContinue 被调用之前(我就不说为什么了,再说就絮叨了)。但令人失望的是:直到运行到 Filter6,仍然没有一个 handler 处理上一次由 _except_handler3 导致的异常,正相反,Filter6 又引发了一个 AV 异常,于是我们有理由相信,回调链表现在成了这个样子:[NT_EXCEPTION_REGISTRATION] => [NT_EXCEPTION_REGISTRATION] => [_EH3_EXCEPTION_REGISTRATION] => [_EH3_EXCEPTION_REGISTRATION] => ...其中,表头的 NT_EXCEPTION_REGISTRATION 结构的 _context 域指向的是 Filter6 所对应的异常帧的地址——因为它是在调用 Filter6 之前刚刚装上去的;同理,接下来第 2 个 NT_EXCEPTION_REGISTRATION 结构的 _context 域指向的是 Filter3 所对应的异常帧的地址。

很明显,在处理 Filter6 的 AV 异常时,系统将首先遍历到打头阵的那两个 NT_EXCEPTION_REGISTRATION 结构,这将导致 nthandler_excpt 分别被调用两次,也就要返回两次 ExceptionNestedException。现在我们先假设那句“if (dispatcherContext > yetAnotherValue)”不存在,那会发生些什么呢?显然,每一次当 handler 返回 ExceptionNestedException 后,RtlDispatchException 都会“傻呵呵地”用 handler 填写的值无条件地覆盖 yetAnotherValue 的值。在我们这个例子中,回调链表中的第 2 项是最后一个 NT 异常帧,也就是说 yetAnotherValue 的值最终将等于第 2 个 NT 异常帧中 _context 域的值,也就是 Filter3 异常帧的地址。这就意味着:在接下来对各个 Filter 的顺序回调中,EH_NESTED_CALL 标志位将在对 Filter3 的回调返回后被清除——可是这是我们想要的吗?显然不是,因为后面的 Filter4、Filter5 和 Filter6 都不知道当前已经发生了嵌套,最严重的是:Filter6 没有发现 EH_NESTED_CALL 标志位,它会认为目前 everything OK,于是重蹈覆辙,再次引发异常……

经过上面的分析,判定“if (dispatcherContext > yetAnotherValue)”的含义我想也就明朗起来了:只有当 handler 返回的异常帧地址高于当前记录的发生嵌套的最高的异常帧地址时,才会更新当前的嵌套异常帧地址——这是表达式字面上的意思。根据异常帧全部构造在堆栈上、高地址靠近栈底的特点,我们也不难说出它的逻辑意义:无论怎样嵌套,一定要保证在发生嵌套的最早注册的那一帧之后注册的所有 handler 在再次回调时都要带有 EH_NESTED_CALL 标志位(什么?有点像绕口令?上学的时候有没有学过划分句子成分?在这里复习一下吧……哈哈)。

总结和思考

从以上的讨论中,我们大体可以认识到:异常嵌套是一种较严重的“异常”情况,所有注册的异常回调 handler 都应该对 EH_NESTED_CALL 标志位引起足够的重视。另外,由于 pDispatcherContext 实际上作为参数传递给了每一个被调用的 handler,所以对于检测到 EH_NESTED_CALL 标志位的 handler,完全可以读取 *pDispatcherContext 的值(我们的分析可以肯定一点:这个变量建立在父函数 RtlDispatchException 的堆栈上,不用担心“该内存不能为 Read”哦……),然后与自己的异常帧地址(同样已经通过参数传给 handler 了)进行比较,从而判断出自己究竟是本次异常嵌套的“罪魁祸首”还是“受害者”。但遗憾的是,我们没有在 VC 的内置异常回调函数 _except_handler3 中看到相关的代码,而且 _except_handler3 在调用我们的 filter 时也没有把 pDispatcherContext 传给我们,我们能做的仅剩下检测 EH_NESTED_CALL 标志位了——好在大多数情况下有这样的检测就足够了。

好了,作为一个生存在用户模式下的程序员来说,我认为我分析到这里已经很对得起我自己了。在分析异常嵌套的过程中,我曾经发现一个现象:如果在一个 handler 中不停地引发异常而不去检查 EH_NESTED_CALL 标志位、然后悬崖勒马的话,最终的结果就是导致堆栈空间耗尽,在一个 EXCEPTION_STACK_OVERFLOW 之后进程被终止——这也就是我前面说到的“死得很惨”。之所以说它惨,是因为进程在被这样终止的时候不会有任何提示信息:不会有崩溃对话框、不会有系统日志或者应用程序日志、不会有提示音……什么都没有,你的进程会突然蒸发掉。我想这大体上是因为在堆栈溢出后没有用 _resetstkoflw 重建堆栈的 guard page 所导致的现象。事实上,即使调用了 _resetstkoflw 也没有用,大概是因为这个时候 esp 还没有被重置,guard page 根本就没法建立——当然这只是我的猜测,我没有继续跟踪 _resetstkoflw 的代码(虽然 CRT 源码中有这个函数完整的 C 语言实现),我怕我的好奇心和发散性思维会再把我带入关于保护模式的无尽之海中……事实上我对 SEH 的研究就是因为看到了 BitComet 软件使用一个叫做 XCrashReport 程序进行崩溃信息的收集工作而开始的,当初也没有想到最后会走这么远……

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值