深入研究Win32结构化异常处理(SEH总结篇)
本文假设你熟悉WIN32,C/C++。
引言:
本文是在《Win32 结构化异常处理(SEH)探秘》基础上做的更新总结。探索MS C/C++编译器和MS核心DLL在操作系统SEH上的不同扩展,逐个分析其内部采用的数据结构及处理流程。
摘要:
Win32 结构化异常处理其核心是操作系统提供的服务,特定的编译器运行库包装操作系统SEH的实现,这其中就包括MS C/C++编译器定义的__try,__finally,__except关键字,这些关键字包装操作系统SEH的实现,方便我们编写异常处理语句。在本文中,我将一层层对SEH进行解剖,以便展示其最基本的概念。
SEH 浅析
当某个线程出错了,操作系统会给我们一个机会通知这个情况。具体点说,操作系统会调用某个用户定义的回调函数,这个回调函数可以做任何它想做的事情,不管回调函数做什么,其最后总是返回一个值,这个值告诉系统下一步做什么。我们来看看这个异常回调函数的原型(该原型在VC/include/excpt.h中有声明):
- EXCEPTION_DISPOSITION __cdecl _except_handler (
- _In_ struct _EXCEPTION_RECORD *_ExceptionRecord,
- _In_ void * _EstablisherFrame,
- _Inout_ struct _CONTEXT *_ContextRecord,
- _Inout_ void * _DispatcherContext
- );
第一个参数是指向EXCEPTION_RECORD结构指针,该结构在WINNT.H中定义(成员含义查阅MSDN):
- typedef struct _EXCEPTION_RECORD {
- DWORD ExceptionCode;
- DWORD ExceptionFlags;
- struct _EXCEPTION_RECORD *ExceptionRecord;
- PVOID ExceptionAddress;
- DWORD NumberParameters;
- ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
- } EXCEPTION_RECORD;
ExceptionCode是有操作系统提供给异常的一个数,代表异常发生的原因。ExceptionAddress表示异常发生的地址。
第二个参数是指向异常帧结构的指针,是一个很重要的参数。操作系统的异常帧结构类型如下:
- 0:000> dt _EXCEPTION_REGISTRATION_RECORD
- ntdll!_EXCEPTION_REGISTRATION_RECORD
- +0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
- +0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION
第三个参数是CONTEXT结构的指针,CONTEXT结构在WINNT.H中定义,它表示特定线程异常发生时寄存器的值:
- typedef struct _CONTEXT {
- //
- // The flags values within this flag control the contents of
- // a CONTEXT record.
- //
- // If the context record is used as an input parameter, then
- // for each portion of the context record controlled by a flag
- // whose value is set, it is assumed that that portion of the
- // context record contains valid context. If the context record
- // is being used to modify a threads context, then only that
- // portion of the threads context will be modified.
- //
- // If the context record is used as an IN OUT parameter to capture
- // the context of a thread, then only those portions of the thread's
- // context corresponding to set flags will be returned.
- //
- // The context record is never used as an OUT only parameter.
- //
- DWORD ContextFlags;
- //
- // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
- // set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
- // included in CONTEXT_FULL.
- //
- DWORD Dr0;
- DWORD Dr1;
- DWORD Dr2;
- DWORD Dr3;
- DWORD Dr6;
- DWORD Dr7;
- //
- // This section is specified/returned if the
- // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
- //
- FLOATING_SAVE_AREA FloatSave;
- //
- // This section is specified/returned if the
- // ContextFlags word contians the flag CONTEXT_SEGMENTS.
- //
- DWORD SegGs;
- DWORD SegFs;
- DWORD SegEs;
- DWORD SegDs;
- //
- // This section is specified/returned if the
- // ContextFlags word contians the flag CONTEXT_INTEGER.
- //
- DWORD Edi;
- DWORD Esi;
- DWORD Ebx;
- DWORD Edx;
- DWORD Ecx;
- DWORD Eax;
- //
- // This section is specified/returned if the
- // ContextFlags word contians the flag CONTEXT_CONTROL.
- //
- DWORD Ebp;
- DWORD Eip;
- DWORD SegCs; // MUST BE SANITIZED
- DWORD EFlags; // MUST BE SANITIZED
- DWORD Esp;
- DWORD SegSs;
- //
- // This section is specified/returned if the ContextFlags word
- // contains the flag CONTEXT_EXTENDED_REGISTERS.
- // The format and contexts are processor specific
- //
- BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
- } CONTEXT;
- typedef CONTEXT *PCONTEXT;
第四个参数是_DispatcherContext,用于存放下一个异常帧,全局展开在执行当前异常帧局部展开中发生嵌套展开时使用。
为了简化起见,我们可以这样理解:当异常发生时,有一个回调函数被调用,此回调函数带有四个参数。那么,当错误发生时候,操作系统如何知道去哪里调用这个回调函数呢?答案涉及另一个结构_EXCEPTION_REGISTRATION,它在vc/crt/src/exsup.inc中定义:
- _EXCEPTION_REGISTRATION struc
- prev dd ?
- handler dd ?
- _EXCEPTION_REGISTRATION ends
大家会发现这个结构和回调函数第二个参数的类型很相似,是的,它其实就是操作系统的异常帧结构类型!当错误发生时,操作系统将调用这个结构中的hanlder所指向的回调函数。好了,新的问题又来了,OS在哪里查找并发现_EXCEPTION_REGISTRATION结构?
为了回答这个问题,回想一下结构化异常处理是以线程为基础的、并作用在每个线程上,明白这一点是有助于理解的。也就是说,每个线程具备其自己的异常处理回调函数。在Intel Win32 平台上,FS寄存器总是指向当前的_TEB(线程环境块),而_TEB的第一项又指向_NT_TIB,接着_NT_TIB的第一项又指向_EXCEPTION_REGISTRATION_RECORD。因此,我们可以通过FS:[0]获取_EXCEPTION_REGISTRATION_RECORD结构的信息。使用WinDbg我们可以很好地理解以上之间的关系:
- 0:000> dt _TEB
- ntdll!_TEB
- +0x000 NtTib : _NT_TIB
- +0x01c EnvironmentPointer : Ptr32 Void
- +0x020 ClientId : _CLIENT_ID
- +0x028 ActiveRpcHandle : Ptr32 Void
- +0x02c ThreadLocalStoragePointer : Ptr32 Void
- +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
- ... ...
- ... ...
- 0:000> dt _NT_TIB
- ntdll!_NT_TIB
- +0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
- +0x004 StackBase : Ptr32 Void
- +0x008 StackLimit : Ptr32 Void
- +0x00c SubSystemTib : Ptr32 Void
- ... ...
- ... ...
- 0:000> dt _EXCEPTION_REGISTRATION_RECORD
- ntdll!_EXCEPTION_REGISTRATION_RECORD
- +0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
- +0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION
现在我们知道了,当异常发生时,系统检查出错线程的_TEB并获取_EXCEPTION_REGISTRATION_RECORD结构指针。这个结构中就有一个类型为_except_handler的异常回调函数指针Handler。这些信息足以让操作系统知道在哪里以及如何调用_except_handler异常回调函数。
通过前面的描述,下面编写一个小程序来对操作系统层的结构化异常进行示范。代码如下:
- /**
- * 文件:MySeh.cpp
- * 编译:cl.exe myseh.cpp /link -SAFESEH:NO
- *
- **/
- #include <windows.h>
- #include <stdio.h>
- DWORD scratch;
- //自定义异常回调函数
- EXCEPTION_DISPOSITION __cdecl my_except_handler (
- struct _EXCEPTION_RECORD *_ExceptionRecord,
- void * _EstablisherFrame,
- struct _CONTEXT *_ContextRecord,
- void * _DispatcherContext
- )
- {
- printf( "Hello from my exception handler/n" );
- //修复异常,使EAX寄存器指向一个有效的地址
- _ContextRecord->Eax = (DWORD)&scratch;
- //异常已修复,重新执行引发异常的指令
- return ExceptionContinueExecution;
- }
- int main()
- {
- DWORD handler = (DWORD)my_except_handler;
- __asm
- {
- //创建_EXCEPTION_REGISTRATION_RECORD结构
- push handler //Handler成员
- push fs:[0] //Next成员,指向下一个异常帧
- mov fs:[0],esp //安装SEH
- }
- __asm
- {
- xor eax,eax //EAX = 0
- mov dword ptr [eax],1234h //写EAX指向的内存从而故意引发一个异常!
- }
- //异常已修复,此时scratch的值为0x1234
- printf( "After writing! scratch=0x%08x/n",scratch);
- __asm
- {
- //移除_EXCEPTION_REGISTRATION_RECORD结构
- pop dword ptr fs:[0]
- add esp,4
- }
- return 0;
- }
代码只有两个函数,main函数使用了三部分内联汇编块__asm。第一个__asm块通过两个PUSH指令在堆栈上建立一个_EXCEPTION_REGISTRATION_RECORD结构,紧接着的指令(mov fs:[0],esp)将FS:[0]指向这个新的_EXCEPTION_REGISTRATION_RECORD结构。值得注意的是,当我们使用MS C/C++编译器的__try/__except时,MS C/C++编译器也是这么在堆栈上建立起_EXCEPTION_REGISTRATION_RECORD结构的。第二个__asm块先把EAX寄存器清零(mov eax,0),然后把此寄存器的值作为内存地址向其写入数据,从而故意引发一个错误。最后一个__asm块清除我们安装的异常处理例程:首先它恢复以前的FS:[0]内容,然后再恢复栈指针到安装前的值。
现在,我们运行MySeh.exe并观察所发生的事情:当mov dowrd ptr [eax],1234h指令执行时,它导致一个数据访问违例。系统查看FS:[0]并找到线程环境块(_TEB)中的_EXCEPTION_REGISTRATION_RECORD结构指针。此结构中则有一个指向MySeh.cpp中定义的my_except_handler函数指针。系统将四个必要的参数压入堆栈并调用my_except_handler函数。
一旦进入my_except_handler函数,代码首先打印"Hello from my exception handler",接着,通过改变CONTEXT结构中的Eax成员,让EAX寄存器指向某个允许进行写入操作的位置,修复此前导致出错的问题。最后,my_except_handler函数返回ExceptionContinueExecution值(ExceptionContinueExecution在EXCPT.H文件中定义)。
当操作系统看到返回值为ExceptionContinueExecution,就会认为我们已经修复了问题并让引起错误的指令重新执行。因此main中的mov dword ptr [eax],1234h指令被再次执行,函数main一切正常!
进一步深入
有了前面的最简单的例子,让我们再回过头去填补一些空白。前面讲过_EXCEPTION_REGISTRATION_RECORD结构,它的第一个成员Next实际上是一个指向另一个_EXCEPTION_REGISTRATION_RECORD结构的指针,第二个_EXCEPTION_REGISTRATION_RECORD结构的Next又可以指向下一个_EXCEPTION_REGISTRATION_RECORD结构,以此类推(其中每一个_EXCEPTION_REGISTRATION_RECORD结构都可以拥有完全不同的异常处理函数)。而线程环境块(_TEB)的第一个成员(在基于Intel CPU 的机器上是 FS:[0])总是指向这个链表的头部。
操作系统要这个_EXCEPTION_REGISTRATION_RECORD结构链表做什么呢?原来,当异常发生时,系统遍历这个链表以便查找一个其异常处理函数Handler同意处理该异常的_EXCEPTION_REGISTRATION_RECORD结构。当然,异常处理函数也可以拒绝处理这个异常。在这种情况下,系统移向链表的下一个_EXCEPTION_REGISTRATION_RECORD结构并询问它的异常处理函数,看它是否愿意处理这个异常。一旦系统找到一个同意处理该异常的某个异常处理函数,它就停止遍历结构链表。在MySeh.cpp的例子中,异常处理程序my_except_handler通过返回ExceptionContinueExecution表示它同意处理这个异常。
下面的代码MySeh2.cpp就是一个异常处理函数不处理某个异常的例子。为了使代码尽量简单,这里使用了编译器层面的异常处理。main函数只设置了一个__try/__except块。在__try块内部调用了HomeGrownFrame函数。
- /**
- * 文件: MySeh2.cpp
- * 编译: CL MYSEH2.CPP /link -SAFESEH:NO
- **/
- #include <windows.h>
- #include <stdio.h>
- EXCEPTION_DISPOSITION __cdecl _except_handler (
- struct _EXCEPTION_RECORD *ExceptionRecord,
- void * EstablisherFrame,
- struct _CONTEXT *ContextRecord,
- void * DispatcherContext
- )
- {
- printf( "Home Grown handler: Exception Code: %08X Exception Flags %X",
- ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags );
- if ( ExceptionRecord->ExceptionFlags & 1 )
- printf( " EH_NONCONTINUABLE" );
- if ( ExceptionRecord->ExceptionFlags & 2 ) //注意这里的标识
- printf( " EH_UNWINDING" );
- if ( ExceptionRecord->ExceptionFlags & 4 )
- printf( " EH_EXIT_UNWIND" );
- if ( ExceptionRecord->ExceptionFlags & 8 )
- printf( " EH_STACK_INVALID" );
- if ( ExceptionRecord->ExceptionFlags & 0x10 )
- printf( " EH_NESTED_CALL" );
- printf( "/n" );
- // 我们不想处理这个异常,让其它函数处理吧
- return ExceptionContinueSearch;
- }
- void HomeGrownFrame( void )
- {
- DWORD handler = (DWORD)_except_handler;
- __asm
- {
- // 创建EXCEPTION_REGISTRATION结构
- push handler // Handler成员
- push FS:[0] // Next成员,指向下一个异常帧
- mov FS:[0],ESP // 安装新的EXECEPTION_REGISTRATION结构
- }
- // 写入地址0,从而引发一个错误
- *(PDWORD)0 = 0;
- printf( "I should never get here!/n" );
- __asm
- {
- // 移去EXECEPTION_REGISTRATION结构
- pop dword ptr fs:[0]
- add esp, 4
- }
- }
- int main()
- {
- __try
- {
- HomeGrownFrame();
- }
- __except( EXCEPTION_EXECUTE_HANDLER )
- {
- printf( "Caught the exception in main()/n" );
- }
- return 0;
- }
MySeh2.cpp中,函数通过向一个 NULL 指针所指向的内存处写入数据而故意引发一个异常:
- *(PDWORD)0 = 0;
异常处理函数_except_handler捕获该异常,简单的输出异常标识并返回ExceptionContinueSearch来表明自己不适合处理这个异常,让系统继续在_EXCEPTION_REGISTRATION_RECORD结构链表中寻找下一个异常帧结构(_EXCEPTION_REGISTRATION_RECORD)。下一个异常帧结构安装的异常回调函数是针对 main 函数中的__try/__except块的。__except 块简单地打印出“Caught the exception in main()”。
我们运行MySeh2.exe,观察输出结果:
第一行输出结果大家都可以理解,那么第二行的输出是怎么回事呢?异常处理函数_except_handler为什么被调用了两次?通过仔细观察我们很容易看出这两行的区别,第一行的异常码是0xC0000005(EXCEPTION_ACCESS_VIOLATION,WinNT.h中定义),异常标识是0;而第二行的异常码是0xC0000027(STATUS_UNWIND,ntstatus.h中定义),异常标识是2(EXCEPTION_UNWINDING,exsup.inc中定义)。原来,当异常发生时,操作系统遍历EXCEPTION_REGISTRATION 结构链表,直到找到一个同意处理该异常的_EXCEPTION_REGISTRATION_RECORD结构节点。一旦操作系统找到该结节点,它就再次遍历这个结构链表,重新调用之前遍历过的、未处理该异常的_EXCEPTION_REGISTRATION_RECORD结构节点的异常处理函数,并传入异常标识为2(EXCEPTION_UNWINDING)的异常记录作为其第一个参数。
当一个异常处理函数被第二次调用时(带 EXCEPTION_UNWINDING 标志),操作系统给这个函数一个最后清理的机会,它会调用诸如局部对象的析构函数以及__finally块代码对发生异常的代码及其上下文执行最后的清理工作。(若要深入理解这句话,请参考《逆向分析kernel32.dll!_except_handler3函数》、全局展开函数《逆向分析NtDLL.dll!RtlUnwind函数》、局部展开函数《逆向分析kernel32.dll!__local_unwind2函数》)。
在异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程由处理异常的那个回调函数来决定从什么地方开始继续执行。一定要记住,仅仅把指令指针设置到所需的代码处就开始执行是不行的。流程恢复执行处的代码的堆栈指针和栈帧指针(在Intel CPU上是 ESP 和EBP)也必须被恢复成它们在处理这个异常的函数的栈帧上的值。因此,这个处理异常的回调函数必须负责把堆栈指针和栈帧指针恢复成它们在包含处理这个异常的 SEH 代码的函数的堆栈上的值。
通常,展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,就好像我们从来没有调用过这些函数一样。展开的另外一个效果就是_EXCEPTION_REGISTRATION_RECORD结构链表上处理异常的那个结构之前的所有_EXCEPTION_REGISTRATION_RECORD 结构都被移除了。也就是说,FS:[0]被指向这个能够处理当前异常的_EXCEPTION_REGISTRATION_RECORD结构节点。
顶级异常处理函数
为了避免引发的异常影响到操作系统,操作系统在任何用户代码开始之前、线程执行的早期,就暗中为每个线程都提供了一个默认的异常处理程序。这个默认的异常处理程序总是链表的最后一个节点,并且它总是选择处理异常。
下面我们随意编写一个程序,准备找出这个默认的异常处理程序。
1、打开Visual Studio 2005/2008,新建[Visual C++] -> [Win32] -> [Win32 控制台应用程序],在应用程序设置的附加选项中勾上[空项目],点击[完成]按钮。
2、右击解决方案下的[源文件]->[添加]->[新建项],添加一个名为DefaultExHandler.cpp的文件
3、编写一个简单的控制台代码,并设断点,如下图所示:
4、调试运行,观察调用堆栈,如下图:
5、由于调试符号未加载,我们程序的第一个执行函数并没有显示出它的名字来,只是用kernel32.dll!7c816fe7表示。右击调用堆栈中的[kernel32.dll!7c816fe7],选择[符号设置],弹跳出下面的对话框,按图设置:
6、点击[确定]后,我们回到调用堆栈,这时就可以看到我们运行的应用程序其最先调用的函数。
7、右击Kerne32.dll!_BaseProcessStart@4(),选择[转到反汇编],来到这里:
8、通过对7C816FCB call __SEH_prolog (7C8024C6h)处的分析,我们得知_BaseProcessStart的异常处理函数是__except_handler3。也就是说,操作系统会为任何应用程序暗中安装一个名为__except_handler3的顶级异常处理函数,当一个进程引发了一个错误而没有异常处理程序去处理它,这个进程就会被系统终止。
下面是_BaseProcessStart的伪代码,具体分析过程请参考《逆向分析Kernel32.dll!BaseProcessStart函数》。
- #define SET_THREAD_ENTRY_ROUTINE 9
- typedef struct CPPEH_RECORD
- {
- DWORD old_esp; //ESP
- DWORD exc_ptr; //GetExceptionInformation返回值
- DWORD prev_er; //prev _EXCEPTION_REGISTRATION_RECORD
- DWORD handler; //handler
- DWORD msEH_ptr; //scopetable
- DWORD disabled; //trylevel
- }CPPEH_RECORD,*PCPPEH_RECORD;
- __stdcall BaseProcessStart(LPVOID lpfnStartRoutine)
- {
- DWORD retValue = 0;
- __try
- {
- //将主线程的入口函数设置为mainCRTStartup
- NtSetInformationThread(GetCurrentThread(),SET_THREAD_ENTRY_ROUTINE,
- &lpfnStartRoutine,sizeof(lpfnStartRoutine));
- retValue = lpfnStartRoutine();
- }
- __except(retValue=GetExceptionCode(),
- UnhandledExceptionFilter(GetExceptionInformation()))
- {
- if(BaseRunningInServerProcess)
- ExitThread(retValue);
- else
- ExitProcess(retValue);
- }
- }
在这段伪码中,注意对lpfnStartRoutine的调用被封装在一个__try 和 __except 块中。正是此__try 块安装了默认的、异常处理程序链表上的最后一个异常处理程序。所有后来注册的异常处理程序都被安装在此链表中这个结点的前面。如果lpfnStartRoutine函数返回,那么表明线程一直运行到完成并且没有引发异常。这时 BaseProcessStart 调用 ExitThread 使线程退出。
另一方面,如果线程引发了一个异常但是没有异常处理程序来处理它时,该怎么办呢?这时,执行流程转到 __except 关键字后面的括号中。在 BaseProcessStart 中,这段代码调用UnhandledExceptionFilter,显示了一个对话框告诉我们发生了一个错误,要么终止出错进程,要么调试它(详细情况可以反汇编Kernel32.dll!UnhandledExceptionFilter函数进行分析)。
编译器级的SEH
现在让我们来看一下 Visual C++ 是如何在操作系统对SEH 功能实现的基础上来创建它自己的结构化异常处理支持的。在继续往下讨论之前,记住其它编译器可以使用原始的系统 SEH 来做一些完全不同的事情这一点是非常重要的。没有谁规定编译器必须实现 Win32 SDK 文档中描述的__try/__except 模型。例如 Visual Basic 5.0 在它的运行时代码中使用了结构化异常处理,但是那里的数据结构和算法与我这里要讲的完全不同。
如果你把 Win32 SDK 文档中关于结构化异常处理方面的内容从头到尾读一遍,一定会遇到下面所谓的“基于帧”的异常处理程序模型:
- __try
- {
- // 受保护的代码
- }
- __except(/*过滤表达式*/)
- {
- // 异常处理代码
- }
简单的说,某个函数的__try块中的所有代码是由_EXCEPTION_REGISTRATION_RECORD结构来保护的。该结构建立在此函数的堆栈帧上。在函数的入口处,这个新的_EXCEPTION_REGISTRATION_RECORD结构被放在异常处理程序链表的头部。在__try 块结束后,相应的_EXCEPTION_REGISTRATION_RECORD结构从这个链表的头部被移除。正如前面所说,异常处理程序链表的头部被保存在 FS:[0] 处。因此,如果在调试器中单步跟踪时能看到类似下面的指令:
- MOV DWORD PTR FS:[00000000],ESP
- 或者
- MOV DWORD PTR FS:[00000000],ECX
就能非常确定这段代码正在进入或退出一个__try/__except块。
事实上,上面讲述的并不完全正确。首先,使用SEH的函数无论拥有多少个__try/__except结构,它只创建一个_EXCEPTION_REGISTRATION_RECORD结构来保护。其次,并非在__try块结束后,相应的_EXCEPTION_REGISTRATION_RECORD结构从这个链表的头部被移除,而是当函数退出前的那一刻_EXCEPTION_REGISTRATION_RECORD结构从这个链表的头部被移除。最后,所有使用SEH的函数,经MS C/C++编译器(Visual Studio 2005/2008版本)编译后、它们的_EXCEPTION_REGISTRATION_RECORD结构所指向的异常处理程序Handler均是_except_handler4!_except_handler4在MS C/C++编译器的运行时库(msvcr80.dll或msvcr90.dll)中。正是这个函数调用过滤表达式来决定后面的大括号中的代码是否执行。
如果单个的_EXCEPTION_REGISTRATION_RECORD结构就能处理多个__try 块的话,很明显,这里面还有很多东西我们不知道。这个技巧是通过一个通常情况下看不到的表中的数据来完成的。由于本文的目的就是要深入探索结构化异常处理,那就让我们来看一看这些数据结构吧。
MS C/C++编译器扩展的异常帧
MS C/C++编译器的 SEH 实现并没有使用原始的_EXCEPTION_REGISTRATION_RECORD结构。它在这个结构的前后添加了一些附加数据。这些附加数据正是允许单个函数(__except_handler4)处理所有异常并将执行流程传递到相应的过滤器表达式和__except 块的关键。我们来看一下这个结构的定义(修正vc/crt/src/exsup.inc中的定义):
- //C/C++扩展SEH的异常帧结构:
- [ebp-18] _Esp
- [ebp-14] PEXCEPTION_POINTERS xpointers;
- struct _EXCEPTION_REGISTRATION{
- [ebp-10] struct _EXCEPTION_REGISTRATION *Next;
- [ebp-0C] _except_handler Handler;
- [ebp-08] struct _EH4_SCOPETABLE *ScopeTable;
- [ebp-04] int TryLevel;
- [ebp-00] int _Ebp;
- };
- //C/C++运行库使用的SCOPE TABLE结构
- struct _EH4_SCOPETABLE {
- DWORD GSCookieOffset;
- DWORD GSCookieXOROffset;
- DWORD EHCookieOffset;
- DWORD EHCookieXOROffset;
- struct _EH4_SCOPETABLE_RECORD ScopeRecord;
- };
- //C/C++运行库使用的SCOPE TABLE RECORD结构
- struct _EH4_SCOPETABLE_RECORD {
- DWORD EnclosingLevel; //上一层__try块
- PVOID FilterFunc; //过滤表达式
- union
- {
- PVOID HandlerAddress; //__except块代码
- PVOID FinallyFunc; //__finally块代码
- };
- };
前面两个成员Next和Handler我们都很熟悉,它们组成基本的_EXCEPTION_REGISTRATION_RECORD结构。后面的三个成员ScopeTable、TryLevel和_Ebp是新增的。ScopeTable指向_EH4_SCOPETABLE结构,&ScopeTable->ScopeRecord的位置又指向一个ScopeRecord数组。TryLevel 域实际上是这个数组的索引。最后一个域_Ebp,是 _EXCEPTION_REGISTRATION 结构创建之前栈帧指针(EBP)的值。
_Ebp 域成为扩展的 _EXCEPTION_REGISTRATION 结构的一部分并非偶然。它是通过 PUSH EBP 这条指令被包含进这个结构中的,而大多数函数开头都是这条指令(通常编译器并不为使用FPO优化的函数生成标准的堆栈帧,这样其第一条指令可能不是 PUSH EBP。但是如果使用了SEH的话,那么无论你是否使用了FPO优化,编译器一定生成标准的堆栈帧)。这条指令可以使 _EXCEPTION_REGISTRATION 结构中所有其它的域都可以用一个相对于栈帧指针(EBP)的负偏移来访问。例如 TryLevel 域在 [EBP-04]处,ScopeTable 指针在[EBP-08]处,等等。(也就是说,这个结构是从[EBP-10H]处开始的。)
紧跟着扩展的 _EXCEPTION_REGISTRATION 结构下面,MS C/C++编译器压入了另外两个值。紧跟着(即[EBP-14H]处)的一个DWORD,是为一个指向 EXCEPTION_POINTERS 结构(一个标准的Win32 结构)的指针所保留的空间。这个指针就是你调用 GetExceptionInformation 这个API时返回的指针。当我们调用GetExceptionInformation时,MS C/C++编译器生成以下代码:
- MOV EAX,DWORD PTR [EBP-14]
GetExceptionInformation 是一个编译器内联函数,与它相关的 GetExceptionCode 函数也是如此。此函数实际上只是返回 GetExceptionInformation 返回的数据结构(EXCEPTION_POINTERS)中的一个结构(EXCEPTION_RECORD)中的一个域(ExceptionCode)的值。下面是MS C/C++编译器为GetExceptionCode生成的代码:
- MOV EAX,DWORD PTR [EBP-14] ; 执行完毕,EAX指向EXCEPTION_POINTERS结构
- MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX指向EXCEPTION_RECORD结构
- MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX中是ExceptionCode的值
看完GetExceptionInformation和GetExceptionCode的内部指令,现在就能理解为什么SDK文档提醒我们要注意这两个函数的使用范围了。
现在我们回到扩展的 _EXCEPTION_REGISTRATION 结构上来。这个结构开始前的8字节处(即[EBP-18H]处),MS C/C++编译器保留了一个DWORD来保存所有prolog代码执行完毕之后的堆栈指针(ESP)的值(实际生成的指令为MOV DWORD PTR [EBP-18H],ESP)。这个DWORD中保存的值是函数执行时ESP寄存器的正常值(除了在准备调用其它函数时把参数压入堆栈这个过程会改变 ESP寄存器的值并在函数返回时恢复它的值外,函数在执行过程中一般不改变ESP寄存器的值)。好了,经过上面的解释,我们再回顾一下这个被编译器扩展后的信息结构:
- EBP-00 _Ebp
- EBP-04 TryLevel
- EBP-08 ScopeTable数组指针
- EBP-0C Handler函数地址
- EBP-10 Next,指向前一个_EXCEPTION_REGISTRATION结构
- EBP-14 xpointers,即GetExceptionInformation的返回值
- EBP-18 栈帧中的标准_Esp
_except_handler4 和 ScopeTable
前面讲过,使用SEH的函数无论拥有多少个__try/__except结构,MS C/C++ 编译器只创建一个_EXCEPTION_REGISTRATION_RECORD结构来保护。那么,编译器是如何做到的呢?这里我们要讲到扩展的 _EXCEPTION_REGISTRATION 结构中的ScopeTable指针。ScopeTable指针指向的是一个_EH4_SCOPETABLE结构,_EH4_SCOPETABLE结构中又有一个_EH4_SCOPETABLE_RECORD结构。事实上,&ScopeTable->ScopeRecord指向的是一个_EH4_SCOPETABLE_RECORD数组,这个数组的项数和使用SEH函数的__try/__except结构的个数一致。从_EH4_SCOPETABLE_RECORD结构中,我们很容易看出它是一个包含过滤表达式,__except块代码,__finally块代码的完整SEH处理结构。如果一个__try/__except(__finally)是一个完整SEH处理结构的话,那么我们怎么知道当前发生异常的是由_EH4_SCOPETABLE_RECORD数组上的哪个处理呢?这时,我们又引出了扩展的_EXCEPTION_REGISTRATION 结构中的TryLevel成员,TryLevel成员用于记载当前异常发生时_EH4_SCOPETABLE_RECORD所处的位置。当线程出现异常时,编译器会自动调用_EH4_SCOPETABLE_RECORD中的FilterFunc执行当前__try/__except的过滤表达式。下面代码演示了编译器在每次进入一个__try/__except(__finally)结构时对TryLevel所做的更新:
- /**
- *文件:TryLevel.cpp
- *编译:cl.exe TryLevel.cpp
- *
- **/
- #include <windows.h>
- int main()
- {
- __try
- {
- __try
- {
- __try
- {
- *((int *)0) = 0x1234;
- }
- __except(EXCEPTION_CONTINUE_SEARCH){}
- }
- __except(EXCEPTION_CONTINUE_SEARCH){}
- }
- __except(EXCEPTION_EXECUTE_HANDLER){}
- }
现在我们知道了TryLevel指向的是当前运行线程所在的__try块。一旦线程发生错误,操作系统通过TryLevel找到ScopeTable中的_EH4_SCOPETABLE_RECORD结构执行异常处理。再进一步,如果当前__try块处理不了这个异常,那么操作系统如何将控制转到其上一个__try块(如果存在的话)进行处理呢?我们回顾一下_EH4_SCOPETABLE_RECORD结构,这个结构中有一个EnclosingLevel成员,EnclosingLevel成员用于指定当前__try块的上一个__try块,也就是嵌套的情况。
前面还说过,所有使用SEH的函数,经MS C/C++编译器(Visual Studio 2005/2008版本)编译后、它们的_EXCEPTION_REGISTRATION_RECORD结构所指向的异常处理程序Handler均是_except_handler4!接下来我们来看一下_except_handler4是如何对异常进行处理的,请查阅:逆向分析MSVCR90D.dll!_except_handler4函数。
待续...