参考大牛总结自己:http://www.cppblog.com/weiym/archive/2015/02/27/209884.html
- 当一个线程出现错误时,操作系统给你一个机会被告知这个错误。说得更明白一些就是,当一个线程出现错误时,操作系统调用用户定义的一个回调函数。这个回调函数可以做它想做的一切。例如它可以修复错误,或者它也可以播放一段音乐。无论回调函数做什么,它最后都要返回一个值来告诉系统下一步做什么。(这不是十分准确,但就此刻来说非常接近。)
- 当你的某一部分代码出错时,系统再回调你的其它代码,那么这个回调函数看起来是什么样子呢?换句话说,你想知道关于异常什么类型的信息呢?实际上这并不重要,因为Win32已经替你做了决定。异常的回调函数的样子如下:
//TIB的 第一个DWORD是一个指向线程的EXCEPTION_REGISTARTION结构的指针。 //在基于Intel处理器的Win32平台上,FS寄存器总是 指向当前的TIB。 //因此在FS:[0]处你可以找到一个指向EXCEPTION_REGISTARTION结构的指针 typedef struct _EXCEPTION_REGISTRATION_RECORD { PEXCEPTION_REGISTRATION_RECORD Next; PEXCEPTION_DISPOSITION Handler; } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD; enum EXCEPTION_DISPOSITION typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution = 0, ExceptionContinueSearch = 1, ExceptionNestedException = 2, ExceptionCollidedUnwind = 3 } EXCEPTION_DISPOSITION; //异常的回调函数的样子如下: EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext );
- 这个原型来自标准的Win32头文件EXCPT.H,乍看起来有些费解。但如果你仔细看,它并不是很难理解。首先,忽略掉返回值的类型(EXCEPTION_DISPOSITION)。你得到的基本信息就是它是一个叫作_except_handler并且带有四个参数的函数。
- 这个函数的第一个参数是一个指向EXCEPTION_RECORD结构的指针。这个结构在WINNT.H中定义,如下所示:
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
- 这个结构中的 ExcepitonCode成员是赋予异常的代码。通过在WINNT.H中搜索以“STATUS_”开头的#define定义,你可以得到一个异常代码列 表。例如所有人都非常熟悉的STATUS_ACCESS_VIOLATION的代码是0xC0000005。一个更全面的异常代码列表可以在 Windows NT DDK的NTSTATUS.H中找到。此结构的第四个成员是异常发生的地址。其它成员暂时可以忽略。
- _except_handler函数的第二个参数是一个指向establisher帧结构的指针。它是SEH中一个至关重要的参数,但是现在你可以忽略它。
- except_handler回调函数的第三个参数是一个指向CONTEXT结 构的指针。此结构在WINNT.H中定义,它代表某个特定线程的寄存器值。图1显示了CONTEXT结构的成员。当用于SEH时,CONTEXT结构表示 异常发生时寄存器的值。顺便说一下,这个CONTEXT结构就是GetThreadContext和SetThreadContext这两个API中使用 的那个CONTEXT结构。
typedef struct _CONTEXT { DWORD ContextFlags; DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; FLOATING_SAVE_AREA FloatSave; DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; DWORD Ebp; DWORD Eip; DWORD SegCs; DWORD EFlags; DWORD Esp; DWORD SegSs; } CONTEXT;
- _except_handler回调函数的第四个参数被称为DispatcherContext。它暂时也可以被忽略。
- 到现在为止,你头脑中已经有了一个当异常发生时会被操作系统调用的回调函数的模型了。这个回调函数带四个参数,其中三个指向其它结构。在这些结构中,一些域比较重要,其它的就不那么重要。这里的关键是_exept_handler回调函数接收到操作系统传递过来的许多有价值的信息,例如异常的类型和发生的地址。使用这些信息,异常回调函数就能决定下一步做什么。
- 对 我来说,现在就写一个能够显示_except_handler作用的样例程序是再诱人不过的了。但是我们还缺少一些关键信息。特别是,当错误发生时操作系 统是怎么知道到哪里去调用这个回调函数的呢?答案是还有一个称为EXCEPTION_REGISTRATION的结构。通篇你都会看到这个结构,所以不要 跳过这一部分。我唯一能找到的EXCEPTION_REGISTRATION结构的正式定义是在Visual C++运行时库源代码中的EXSUP.INC文件中:
typedef struct _EXCEPTION_REGISTRATION_RECORD { PEXCEPTION_REGISTRATION_RECORD Next; PEXCEPTION_DISPOSITION Handler; } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
- 这 个结构在WINNT.H的NT_TIB结构的定义中被称为_EXCEPITON_REGISTARTION_RECORD。唉,没有一个地方能够找到 _EXCEPTION_REGISTRATION_RECORD的定义,所以我不得不使用EXSUP.INC中这个汇编语言的结构定义。这是我前面所说 SEH未公开的一个证据。(读者可以使用内核调试器,如KD或SoftICE并加载调试符号来查看这个结构的定义。
- 下图是在KD中的结果:
- 下图是在SoftICE中的结果:
- 当异常发生时,操作系统是如何知道到哪里去调用回调函数的呢?实际 上,EXCEPTION_REGISTARTION结构由两个域组成,第一个你现在可以忽略。第二个域handler,包含一个指向 _except_handler回调函数的指针。这让你离答案更近一点,但现在的问题是,操作系统到哪里去找 EXCEPTION_REGISTATRION结构呢?
- 要回答这个问题,记住结构化异常处理是基于线程的这一点是非常有用的。也就是说,每个线程有它自己的异常处理回调函数。在1996年五月的Under The Hood专栏中,我介绍了一个关键的Win32数据结构——线程信息块(Thread Information/Environment Block,TIB或TEB)。这个结构的某些域在Windows NT、Windows 95、Win32s和OS/2上是相同的。TIB的 第一个DWORD是一个指向线程的EXCEPTION_REGISTARTION结构的指针。在基于Intel处理器的Win32平台上,FS寄存器总是 指向当前的TIB。因此在FS:[0]处你可以找到一个指向EXCEPTION_REGISTARTION结构的指针。
- 到 现在为止,我们已经有了足够的认识。当异常发生时,系统查找出错线程的TIB,获取一个指向EXCEPTION_REGISTRATION结构的指针。在 这个结构中有一个指向_except_handler回调函数的指针。现在操作系统已经知道了足够的信息去调用_except_handler函数,如图 2所示
typedef struct _TEB { NT_TIB Tib; /* 00h */ typedef struct _NT_TIB { struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; typedef struct _EXCEPTION_REGISTRATION_RECORD { PEXCEPTION_REGISTRATION_RECORD Next; PEXCEPTION_DISPOSITION Handler; enum EXCEPTION_DISPOSITION typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution = 0, ExceptionContinueSearch = 1, ExceptionNestedException = 2, ExceptionCollidedUnwind = 3 } EXCEPTION_DISPOSITION; //异常的回调函数的样子如下: EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD; void * EstablisherFrame, struct _CONTEXT *ContextRecord, typedef struct _CONTEXT { DWORD ContextFlags; DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; FLOATING_SAVE_AREA FloatSave; DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; DWORD Ebp; DWORD Eip; DWORD SegCs; DWORD EFlags; DWORD Esp; DWORD SegSs; } CONTEXT; void * DispatcherContext ); } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD; PVOID StackBase; PVOID StackLimit; PVOID SubSystemTib; _ANONYMOUS_UNION union { PVOID FiberData; DWORD Version; } DUMMYUNIONNAME; PVOID ArbitraryUserPointer; struct _NT_TIB *Self; } NT_TIB,*PNT_TIB; PVOID EnvironmentPointer; /* 1Ch */ CLIENT_ID Cid; /* 20h */ //进程ID PVOID ActiveRpcHandle; /* 28h */ PVOID ThreadLocalStoragePointer; /* 2Ch */ struct _PEB *ProcessEnvironmentBlock; /* 30h */ //指向PEB ULONG LastErrorValue; /* 34h */ ULONG CountOfOwnedCriticalSections; /* 38h */ PVOID CsrClientThread; /* 3Ch */ struct _W32THREAD* Win32ThreadInfo; /* 40h */ ULONG User32Reserved[0x1A]; /* 44h */ ULONG UserReserved[5]; /* ACh */ PVOID WOW32Reserved; /* C0h */ LCID CurrentLocale; /* C4h */ ULONG FpSoftwareStatusRegister; /* C8h */ PVOID SystemReserved1[0x36]; /* CCh */ LONG ExceptionCode; /* 1A4h */ struct _ACTIVATION_CONTEXT_STACK *ActivationContextStackPointer; /* 1A8h */ UCHAR SpareBytes1[0x28]; /* 1ACh */ GDI_TEB_BATCH GdiTebBatch; /* 1D4h */ CLIENT_ID RealClientId; /* 6B4h */ PVOID GdiCachedProcessHandle; /* 6BCh */ ULONG GdiClientPID; /* 6C0h */ ULONG GdiClientTID; /* 6C4h */ PVOID GdiThreadLocalInfo; /* 6C8h */ ULONG Win32ClientInfo[62]; /* 6CCh */ PVOID glDispatchTable[0xE9]; /* 7C4h */ ULONG glReserved1[0x1D]; /* B68h */ PVOID glReserved2; /* BDCh */ PVOID glSectionInfo; /* BE0h */ PVOID glSection; /* BE4h */ PVOID glTable; /* BE8h */ PVOID glCurrentRC; /* BECh */ PVOID glContext; /* BF0h */ NTSTATUS LastStatusValue; /* BF4h */ UNICODE_STRING StaticUnicodeString; /* BF8h */ WCHAR StaticUnicodeBuffer[0x105]; /* C00h */ PVOID DeallocationStack; /* E0Ch */ PVOID TlsSlots[0x40]; /* E10h */ LIST_ENTRY TlsLinks; /* F10h */ PVOID Vdm; /* F18h */ PVOID ReservedForNtRpc; /* F1Ch */ PVOID DbgSsReserved[0x2]; /* F20h */ ULONG HardErrorDisabled; /* F28h */ PVOID Instrumentation[14]; /* F2Ch */ PVOID SubProcessTag; /* F64h */ PVOID EtwTraceData; /* F68h */ PVOID WinSockData; /* F6Ch */ ULONG GdiBatchCount; /* F70h */ BOOLEAN InDbgPrint; /* F74h */ BOOLEAN FreeStackOnTermination; /* F75h */ BOOLEAN HasFiberData; /* F76h */ UCHAR IdealProcessor; /* F77h */ ULONG GuaranteedStackBytes; /* F78h */ PVOID ReservedForPerf; /* F7Ch */ PVOID ReservedForOle; /* F80h */ ULONG WaitingOnLoaderLock; /* F84h */ ULONG SparePointer1; /* F88h */ ULONG SoftPatchPtr1; /* F8Ch */ ULONG SoftPatchPtr2; /* F90h */ PVOID *TlsExpansionSlots; /* F94h */ ULONG ImpersionationLocale; /* F98h */ ULONG IsImpersonating; /* F9Ch */ PVOID NlsCache; /* FA0h */ PVOID pShimData; /* FA4h */ ULONG HeapVirualAffinity; /* FA8h */ PVOID CurrentTransactionHandle; /* FACh */ PTEB_ACTIVE_FRAME ActiveFrame; /* FB0h */ PVOID FlsData; /* FB4h */ UCHAR SafeThunkCall; /* FB8h */ UCHAR BooleanSpare[3]; /* FB9h */ } TEB, *PTEB;
- 把 这些小块知识拼凑起来,我写了一个小程序来演示上面这个对操作系统层面的结构化异常处理的简化描述,如图3的MYSEH.CPP所示。它只有两个函数。 main函数使用了三个内联汇编块。第一个内联汇编块通过两个PUSH指令(“PUSH handler”和“PUSH FS:[0]”)在堆栈上创建了一个EXCEPTION_REGISTRATION结构。PUSH FS:[0]这条指令保存了先前的FS:[0]中的值作为这个结构的一部分,但这在此刻并不重要。重要的是现在堆栈上有一个8字节的 EXCEPTION_REGISTRATION结构。紧接着的下一条指令(MOV FS:[0],ESP)使线程信息块中的第一个DWORD指向了新的EXCEPTION_REGISTRATION结构。(注意堆栈操作)。
#include "stdafx.h" #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> DWORD scratch; EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) { unsigned i; // 指明是我们让流程转到我们的异常处理程序的 printf( "Hello from an exception handler " ); // 改变CONTEXT结构中EAX的值,以便它指向可以成功进写操作的位置 ContextRecord-> = (DWORD)&scratch; // 告诉操作系统重新执行出错的指令 return ExceptionContinueExecution; } int main() { DWORD handler = (DWORD)_except_handler; __asm { // 创建EXCEPTION_REGISTRATION结构: push handler // handler函数的地址 push FS:[0] // 前一个handler函数的地址 mov FS:[0],ESP // 安装新的EXECEPTION_REGISTRATION结构 } __asm { mov eax,0 // 将EAX清零 mov [eax], 1 // 写EAX指向的内存从而故意引发一个错误 } printf( "After writing! " ); __asm { // 移去我们的EXECEPTION_REGISTRATION结构 mov eax,[ESP] // 获取前一个结构 mov FS:[0], EAX // 安装前一个结构 add esp, 8 // 将我们的EXECEPTION_REGISTRATION弹出堆栈 } return 0; }
- 如 果你想知道我为什么把EXCEPTION_REGISTRATION结构创建在堆栈上而不是使用全局变量,我有一个很好的理由可以解释它。实际上,当你使 用编译器的__try/__except语法结构时,编译器自己也把EXCEPTION_REGISTRATION结构创建在堆栈上。我只是简单地向你展 示了如果使用__try/__except时编译器做法的简化版。
- 回 到main函数,第二个__asm块通过先把EAX寄存器清零(MOV EAX,0)然后把此寄存器的值作为内存地址让下一条指令(MOV [EAX],1)向此地址写入数据而故意引发一个错误。最后的__asm块移除这个简单的异常处理程序:它首先恢复了FS:[0]中先前的内容,然后把 EXCEPTION_REGISTRATION结构弹出堆栈(ADD ESP,8)。
- 现 在假若你运行MYSEH.EXE,就会看到整个过程。当MOV [EAX],1这条指令执行时,它引发一个访问违规。系统在FS:[0]处的TIB中查找,然后发现了一个指向 EXCEPTION_REGISTRATION结构的指针。在MYSEH.CPP中,在这个结构中有一个指向_except_handler函数的指针。 系统然后把所需的四个参数(我在前面已经说过)压入堆栈,接着调用_except_handler函数。
- 一 旦进入_except_handler,这段代码首先通过一个printf语句表明“哈!是我让它转到这里的!”。接着,_except_handler 修复了引发错误的问题——即EAX寄存器指向了一个不能写的内存地址(地址0)。修复方法就是改变CONTEXT结构中的EAX的值使它指向一个允许写的 位置。在这个简单的程序中,我专门为此设置了一个DWORD变量(scratch)。_except_handler函数最后的动作是返回 ExceptionContinueExecution这个值,它在EXCPT.H文件中定义。
- 当 操作系统看到返回值为ExceptionContinueExecution时,它将其理解为你已经修复了问题,而引起错误的那条指令应该被重新执行。由 于我的_except_handler函数已经让EAX寄存器指向一个合法的内存,MOV [EAX],1指令再次执行,这次main函数一切正常。看,这也并不复杂,不是吗?