环境:VC++6.0, Windows XP SP3
上一篇中,我们看到了如何实现一个终止处理程序,和系统级的异常处理程序,在这一篇中,我们要着重看一下VC++6.0的异常处理程序是如何实现的。
这里要用的结构体有:
typedef struct _SCOPETABLE
{
DWORD previousTryLevel;
DWORD lpfnFilter
DWORD lpfnHandler
} SCOPETABLE, *PSCOPETABLE;
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;
};
如果你不知道这两个结构体是干嘛的,建议你先看一下前一篇文章,再来看这里。
我们知道在系统级的异常处理程序中,那个SEH链上串的EXCEPTION_REGISTRATION结构体只有两项,就是上面的结构体的前两项,而且系 统级的异常处理中没有我们现在所用的过滤函数和处理函数之分,只有一个handler,而现在我们在用C语言或都C++写程序时,用到的SEH时,都有过 滤函数(就是except后面的小括号里的内容)和处理函数(就是except后面的大括号里的内容),其实系统SEH并没有给我们提供这样的服务,我们 之所以可以这样用,是因为编译器的原因。至于系统是如何做SEH的,在上一篇文章我已经说了。现在我们来看一下,VC++6.0是怎样做的。
在VC++6.0中,基本上说只有一个异常处理函数(为什么说是基本上,后面我会说的)——__except_handler3,这个函数的指针会在产生 一个EXCEPTION_REGISTRATION结构时,赋给其成员handler,以后如果产生异常,系统就会调用这个函数,再由这个函数调用用户写 的异常处理函数,也就是说,用户写的异常处理函数(就是你自己写的except后面大括号里的内容)是没有注册到 EXCEPTION_REGISTRATION中的。那么,用户自己的异常处理函数在哪里呢?其实,编译器把用户的异常处理函数放到了一个结构体数组中, 这个结构体类型就是SCOPETABLE,每个try块会对应这么一个结构体,结构体中保存了它到底是哪一个try块(previousTryLevel 表示它的上一层try块的编号,最外层的try块的trylevel为0,所以,可以想像最外层的try块的previousTryLevel为-1), 它的过滤函数以及异常处理函数。在一个函数中出现的try块,编译器会把这些try块对应的SCOPETABLE结构体放到一个数组里,并把这个数组的首 地址给这个函数的EXCEPTION_REGISTRATION结构的成员scopetable。这样,我想大家应该就可以基本上理解系统产生一个异常 时,编译器是如何处理的了。
在上面一话段中,你可能会发现一个函数拥有一个EXCEPTION_REGISTRATION结构,通过这个结构中可以访问这个函数中所在的try块,其 实就是这样的,这就是VC++6.0对SEH所做的一些修改,如果你有多个函数中使用了SEH,那么你会发现fs:[0]上有多个 EXCEPTION_REGISTRATION结构。虽然这和我们看到的系统级SEH不太一样,但这里的SEH与系统的SEH是兼容的,你仔细看这里的 EXCEPTION_REGISTRATION结构,你会发现前两项与系统的一样,不一样的内容只是在后面,所以系统看到编译器产生的SEH结构以后,也 是可以认得的。
加一句,后面我会为了方便把EXCEPTION_REGISTRATION结构称为frame,声明一下。
到这里,我相信大家可以大致想像一下VC++6.0处理后的SEH是怎么运行的了吧。下面,我会仔细说一下这个过程的。
我们先看一下结构体EXCEPTION_REGISTRATION,这个结构体和系统的不一样,但是,为什么要这样设计呢?前四项我就不说了,大家都能看懂,现在就剩下两个了,_ebp,xpointers。
先看_ebp,这个成员保存的值是产生这个frame的函数的栈帧指针,为的是得到这个frame后,可以轻松访问其栈帧,还有一个作用就是在当前函数 里,你可以通过对ebp的负偏移来访问这个frame中的成员,比如trylevel是[ebp - 04h],scopetable是[ebp - 08h]等等。
再来看看xpointers。我们知道,在异常处理时,有一个API叫LPEXCEPTION_POINTERS GetExceptionInformation(void),这个函数的做用是返回异常信息,下面是EXCEPTION_POINTERS的定义,看一 下就行了,目前对我们没什么用:
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS;
MSDN中对这个函数有一句话: The filter expression can invoke a filter function. The filter function cannot call GetExceptionInformation . However, the return value ofGetExceptionInformation can be passed as a parameter to a filter function. 是什么意思呢,就是说GetExceptionInformation这个函数只能在except后面的那个小括号里被调用,为什么有这么奇怪的一条规定呢,我们来看一下这个函数的实现,就一句:
mov eax, dword ptr [ebp - 14h]
这是什么意 思呢,我们再看看EXCEPTION_REGISTRATION的定义,会发现ebp - 14h就是成员xpointers,这时你可能会想,只要在产生EXCEPTION_REGISTRATION的函数里调用 GetExceptionInformation应该也可以啊,为什么只能在except后面的小括号里调用啊?你可以把一个用SEH的程序用 VC++6.0反汇编后看看,你会发现,在程序开始设置frame的值的时候,并没有设置xpointers的值,而这个值是在 __except_handler3里被设定的,所以 如果你在除了except后面的小括号里调用GetExceptionInformation的时候得到的值是无效的!!
看到这儿,过程已经慢慢清楚了,但是还不是很清楚,因为整个过程的实现是通过函数__except_handler3来做的,下面让我们看看这个函数是什 么内容,因为找到一组伪代码,所以用伪代码来分析(有点儿长,大家忍着点儿,不过看汇编的更长,呵呵):
int __except_handler3( //从参数来看,就是我们上一篇文章中提到的那个回调函数,这儿不多说了
struct _EXCEPTION_RECORD * pExceptionRecord,
struct EXCEPTION_REGISTRATION * pRegistrationFrame,
struct _CONTEXT *pContextRecord,
void * pDispatcherContext )
{
LONG filterFuncRet //没什么意思,往后看就知道这个变量是干什么的了
LONG trylevel //搞一个局部变量来存pRegistrationFrame->trylevel
EXCEPTION_POINTERS exceptPtrs //这个就是GetExceptionInformation的返回值
PSCOPETABLE pScopeTable //搞一个局部变量来存pRegistrationFrame->scopetable
CLD // Clear the direction flag (make no assumptions!)
// if neither the EXCEPTION_UNWINDING nor EXCEPTION_EXIT_UNWIND bit
// is set... This is true the first time through the handler (the
// non-unwinding case)
if ( ! (pExceptionRecord->ExceptionFlags
& (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )
{
// Build the EXCEPTION_POINTERS structure on the stack
exceptPtrs.ExceptionRecord = pExceptionRecord;
exceptPtrs.ContextRecord = pContextRecord;
// Put the pointer to the EXCEPTION_POINTERS 4 bytes below the
// establisher frame. See ASM code for GetExceptionInformation
*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs; //看到了吧,这三句就是设定pRegistration->xpointers,现在明白为什么只能在except的小括号里调用GetExceptionInformation了吧
// Get initial "trylevel" value
trylevel = pRegistrationFrame->trylevel
// Get a pointer to the scopetable array
scopeTable = pRegistrationFrame->scopetable;
search_for_handler:
if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE )
{
if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter )
{
PUSH EBP // Save this frame EBP
// !!!Very Important!!! Switch to original EBP. This is
// what allows all locals in the frame to have the same
// value as before the exception occurred.
EBP = &pRegistrationFrame->_ebp
// Call the filter function
filterFuncRet = scopetable[trylevel].lpfnFilter(); //原来我们自己写的过滤函数是在这儿被调用的啊
POP EBP // Restore handler frame EBP
if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )
{
if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION
return ExceptionContinueExecution;
// If we get here, EXCEPTION_EXECUTE_HANDLER was specified
scopetable == pRegistrationFrame->scopetable
// Does the actual OS cleanup of registration frames
// Causes this function to recurse
__global_unwind2( pRegistrationFrame );
// Once we get here, everything is all cleaned up, except
// for the last frame, where we'll continue execution
EBP = &pRegistrationFrame->_ebp
__local_unwind2( pRegistrationFrame, trylevel );
// NLG == "non-local-goto" (setjmp/longjmp stuff)
__NLG_Notify( 1 ); // EAX == scopetable->lpfnHandler
// Set the current trylevel to whatever SCOPETABLE entry
// was being used when a handler was found
pRegistrationFrame->trylevel = scopetable->previousTryLevel;
// Call the _except {} block. Never returns.
pRegistrationFrame->scopetable[trylevel].lpfnHandler();
}
}
scopeTable = pRegistrationFrame->scopetable;
trylevel = scopeTable->previousTryLevel
goto search_for_handler;
}
else // trylevel == TRYLEVEL_NONE
{
retvalue == DISPOSITION_CONTINUE_SEARCH;
}
}
else // EXCEPTION_UNWINDING or EXCEPTION_EXIT_UNWIND flags are set
{
PUSH EBP // Save EBP
EBP = pRegistrationFrame->_ebp // Set EBP for __local_unwind2
__local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )
POP EBP // Restore EBP
retvalue == DISPOSITION_CONTINUE_SEARCH;
}
}
注: 这个函数的伪代码来自于Matt Pietrek 1997年的论文"A Crash Course on the Depths of Win32 Structured Exception Handling",英语的注释就是作者自己写的,如果大家觉得我说得不清楚的话,建议看下这篇文章。
看到了吧, 如果我们的过滤函数返回的是EXCEPTION_CONTINUE_SEARCH,__except_handler3会看这个frame中的下一个过滤 函数,依次类推。还有一点要注意的是,如果我们的过滤函数返回值为EXCEPTION_EXECUTE_HANDLER的话,我们的异常处理会被 __except_handler3调用,但是, __except_handler3把控制权交给我们的程序后,我们的程序不会再返回到__except_handler3,而是顺序执行下去 ,这时也许你会想,就这样执行下去,栈的结构不是被破坏了吗,其实是破坏了,但是,破坏了我们还可以修复嘛,我们来看一段代码:
int main()
{
int a = 0;
__try
{
a = 1;
}
__except(filter(GetExceptionInformation())) //看到了吧,GetExceptionInformation只能在这儿调用
{
a = 2;
}
a = 3;
return 0;
}
变成汇编我们再看看:
12: int main()
13: {
0040D860 push ebp
0040D861 mov ebp,esp
0040D863 push 0FFh
0040D865 push offset string "stream != NULL"+14h (00422f80)
0040D86A push offset __except_handler3 (004011f0)
0040D86F mov eax,fs:[00000000]
0040D875 push eax
0040D876 mov dword ptr fs:[0],esp
0040D87D add esp,0B4h
0040D880 push ebx
0040D881 push esi
0040D882 push edi
0040D883 mov dword ptr [ebp-18h],esp ;保存当前的ESP,待会儿用于恢复栈
0040D886 lea edi,[ebp-5Ch]
0040D889 mov ecx,11h
0040D88E mov eax,0CCCCCCCCh
0040D893 rep stos dword ptr [edi]
14: int a = 0;
0040D895 mov dword ptr [ebp-1Ch],0
15: __try
0040D89C mov dword ptr [ebp-4],0
16: {
17: a = 1;
0040D8A3 mov dword ptr [ebp-1Ch],1
18: }
0040D8AA mov dword ptr [ebp-4],0FFFFFFFFh
0040D8B1 jmp $L564+11h (0040d8d1)
19: __except(filter(GetExceptionInformation()))
0040D8B3 mov eax,dword ptr [ebp-14h] ;看到了吧,GetExceptionInformation就这么一句
0040D8B6 push eax
0040D8B7 call @ILT+10(filter) (0040100f)
0040D8BC add esp,4
$L565:
0040D8BF ret
$L564:
0040D8C0 mov esp,dword ptr [ebp-18h] ;当我们的filter返回EXCEPTION_EXECUTE_HANDLER时,__except_handler3会把控制权交到这一句上,这句就是恢复栈的,所以,即使不用回到__except_handler3,程序也可以正常运行
20: {
21: a = 2;
0040D8C3 mov dword ptr [ebp-1Ch],2
22: }
0040D8CA mov dword ptr [ebp-4],0FFFFFFFFh
23: a = 3;
0040D8D1 mov dword ptr [ebp-1Ch],3
24: return 0;
0040D8D8 xor eax,eax
25: }
0040D8DA mov ecx,dword ptr [ebp-10h]
0040D8DD mov dword ptr fs:[0],ecx
0040D8E4 pop edi
0040D8E5 pop esi
0040D8E6 pop ebx
0040D8E7 add esp,5Ch
0040D8EA cmp ebp,esp
0040D8EC call __chkesp (004010c0)
0040D8F1 mov esp,ebp
0040D8F3 pop ebp
0040D8F4 ret
现在,我们来总结一下,在VC++6.0中,如果我们在一个函数中使用了SEH,编译器会为我们产生一个EXCEPTION_REGISTRATION结 构,设置相应的值,并把这个结构串在fs:[0]指向的SEH链上,如果这个函数中有多个try块,不管是嵌套的还是平行的,编译器都会分别把这些块的信 息放在一个叫SCOPETABLE的结构中,并形成一个数组,并把这个数组的首地址赋给EXCEPTION_REGISTRATION结构的成员 scopetable,如果有多个函数,那么编译器会为我们产生多个EXCEPTION_REGISTRATION结构,并且都串在SEH链上。当一个用 到SEH链的函数产生异常时,系统会调用编译器设定在EXCEPTION_REGISTRATION里的异常处理函数 __except_handler3,再由这个函数在产生异常的那个函数对应的EXCEPTION_REGISTRATION结构中搜索,看哪个 except块可以解决这个异常(如果过滤函数返回不是EXCEPTION_CONTINUE_SEARCH,就表示可以解决),如果没有可以解决的,则 会转到一个叫UnhandledExceptionFilter函数那里,在这里,一般会结束掉产生异常的线程或进程,如果可以解决的话,而且过滤函数的 返回值又是EXCEPTION_EXECUTE_HANDLERR的话,__except_handler3会把控制权交给我们的except块的代码并 恢复栈指针ESP,程序继续向下执行。