23.1 基础知识
23.1.1 Windows下的软件异常
(1)中断和异常
①中断是由外部硬件设备或异步事件产生的
②异常是由内部事件产生的,可分为故障、陷阱和终止三类。
(2)两种异常处理机制:SEH和VEH(WindowsXP以上新引进)
(3)结构化异常处理(SEH)是Windows操作系统提供的强大异常处理功能。而Visual C++中的__try{} __finally{}和__try{} __except{}结构本质上是对Windows提供的SEH的封装。
23.1.2 SEH的分类
(1)Per-Thread类型SEH(也称为线程异常处理),用来监视某线程代码是否发生异常。
(2)Final类型SEH(也称为进程异常处理、筛选器或顶层异常处理),用于监视整个进程中所有线程是否发生异常。在整个进程中,该类型异常处理过程只有一个,可通过SetUnhandledExceptionFilter设置。
23.1.3 SEH相关的数据结构
(1) 线程信息块TIB(Thread Information Block或TEB)
typedef struct _NT_TIB { struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //异常的链表 PVOID StackBase; PVOID StackLimit; PVOID SubSystemTib; union { PVOID FiberData; DWORD Version; }; PVOID ArbitraryUserPointer; struct _NT_TIB *Self; } NT_TIB;
Fs:[0]总是指向当前线程的TIB,其中0偏移的指向线程的异常链表,即ExceptionList是指向异常处理链表(EXCEPTION_REGISTRATION结构)的一个指针。
(2)EXCEPTION_REGISTRATION结构
typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Prev; //指向前一个EXCEPTION_REGISTRATION的指针 PEXCEPTION_ROUTINE Handler; //当前异常处理回调函数的地址 } EXCEPTION_REGISTRATION_RECORD;
(3)EXCEPTION_RECORD结构
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; //异常码,以STATUS_或EXCEPTION_开头,可自定义。(sehdef.inc) DWORD ExceptionFlags; //异常标志。0可修复;1不可修复;2正在展开,不要试图修复 struct _EXCEPTION_RECORD *ExceptionRecord; //指向嵌套的异常结构,通常是异常中又引发异常 PVOID ExceptionAddress; //异常发生的地址 DWORD NumberParameters; //下面ExceptionInformation所含有的dword数目 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //附加消息,如读或写冲突 } EXCEPTION_RECORD;
(4)CONTEXT结构(可通过GetThreadContext和SetThreadContext来读写)
typedef struct _CONTEXT { DWORD ContextFlags; //用来表示该结构中的哪些域有效 DWORD Dr0, Dr2, Dr3, Dr4, Dr5, Dr6, Dr7; //调试寄存器 FLOATING_SAVE_AREA FloatSave; //浮点寄存器区 DWORD SegGs, SegFs, SegEs, Seg Ds; //段寄存器 DWORD Edi, Esi, Ebx, Edx, Ecx, Eax; //通用寄存器组 DWORD Ebp, Eip, SegCs, EFlags, Esp, SegSs; //控制寄存器组 //扩展寄存器,只有特定的处理器才有 BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT;
(5)EXCEPTION_POINTERS结构(传递给顶层型异常处理回调函数的参数)
typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; //指向ExceptionRecord的指针 PCONTEXT ContextRecord; //指向Context结构的指针 } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
23.1.4 异常处理相关API
(1)SetErrorMode:控制错误模式,如是否出现错误对话框
(2)SetUnhandledExceptionFilter:设定顶层异常处理回调函数
(3)RaiseException:用于引发异常,可指定自己的异常代码及相关信息
(4)GetThreadContext/SetThreadContext:获取或设置线程环境
(5)RtlUnwind:栈展开操作(后面会详细介绍)
23.1.5 顶层异常处理
(1)顶层异常处理是进程相关的,只要没有由线程处理过程或调试器处理掉,最终均要交由顶层异常回调函数处理
(2)注册顶层异常处理:SetUnhandledExceptionFilter,每个进程只能注册一个。返回值为前一个异常回调函数的地址。
(3)回调函数
LONG UnhandledExceptionFilter(STRUCT _EXCEPTION_POINTERS *ExceptionInfo){ //三种返回值,决定系统下一步的动作 //1.EXCEPTION_EXECUTE_HANDLE(1):表示异常己被处理。程序可以优雅结束 //2.EXCEPTION_CONTINUE_SEARCH(0):表示顶层异常处理不能处理,需交给其他异常处理过程 //3.EXCEPTION_CONTINUE_EXECUTION(-1):表示顶层异常过程处理了异常,且程序应从原异常发生的指令重新继续执行一下。(可以通过改变CONTEXT内容来达到改变程序执行环境) }
23.1.6 线程异常处理(局部的,仅仅监视进程中某特定线程是否发生异常)
(1)线程异常处理特点
①Windows系统为每个线程单独提供了一种异常处理的方法,当一个线程出现错误时,操作系统调用用户定义的一系列回调函数,在这些回调函数中,可以进行修复错误或其它的一些操作,最后的返回值告系统系统下一步的动作(如继续搜索异常处理程序或终止程序等)。
②SEH是基于线程的,使用SEH可以为每个线程设置不同的异常处理程序(回调函数)而且可以为每个线程设置多个异常处理程序。
③ 由于SEH使用了与硬件平台相关的数据指针,所以不同硬件平台使用SHE的方法有所不同。
(2)回调函数原型
EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,//指向包含异常信息的EXCEPTION_RECORD结构 void* EstablisherFrame,//指向该异常相关的EXCEPTION_REGISTRATION结构 struct _CONTEXT *ContextRecord,//指向线程环境CONTEXT结构的指针 void* DispatcherContext){ //该域暂无意义 …… //4种返回值及含义 //1.ExceptionContinueExecution(0):回调函数处理了异常,可以从异常发生的指令处重新执行。 //2.ExceptionContinueSearch(1):回调函数不能处理该异常,需要要SEH链中的其他回调函数处理。 //3.ExceptionNestedException(2):回调函数在执行中又发生了新的异常,即发生了嵌套异常 //4.ExceptionCollidedUnwind(3):发生了嵌套的展开操作 return … }
(3)线程异常处理的注册
push _exception_handler //异常回调函数_exception_handler的地址,即handler push fs:[0] //保存前一个异常回调函数的地址,即prev mov fs:[0],esp //安装新的EXCEPTION_REGISTRATION结构(两个成员:prev,handler)。 //此时栈顶分别是prev和handler,为新的EXCEPTION_REGISTRATION结 //构,mov fs:[0],esp,就可以让fs:[0]指向该指构。
(4)异常回调函数的调用过程
①线程信息块(TIB),永远放在fs段选择器指定的数据段的0偏移处,即fs:[0]的地方就是TIB结构。对不同的线程fs寄存器的内容有所有不同,但fs:[0]都是指向当前线程的TIB结构体,所以fs:[0]是一个EXCEPTION_REGISTRATION结构体的指针。
②当异常发生时,系统从fs:[0]指向的内存地址处取出ExceptionList字段,然后从ExceptionList字段指向的EXCEPTION_REGISTRATION结构中取出handler字段,并根据其中的地址去调用异常处理程序(回调函数)。
【MySeh程序】安装线程异常处理程序
/************************************************************************ MYSEH - Matt Pietrek 1997 Microsoft Systems Journal,January 1997 cl.exe myseh.cpp /link -SAFESEH:NO 用IDE编译时须将项目的“链接器”→“高级”→“映像具有安全异常处理程序”设为SAFESEH:NO(Debug或Release版要分别设置) 用命令行CL MYSHE.CPP编译里可参考下面的MySeh.bat批处理的设置。 ************************************************************************/ //MySeh.bat //set PATH = %PATH%; E:\读书笔记\Windows核心编程\SEH\SEH; D:\VisualStudio\VC\bin //set include = C:\Program Files\Windows Kits\8.1\Include\um; C:\Program Files\Windows Kits\8.1\Include\shared; D:\VisualStudio\VC\include //set lib = C:\Program Files\Windows Kits\8.1\Lib\winv6.3\um\x86; D:\VisualStudio\VC\lib //cl.exe E : \读书笔记\Windows核心编程\SEH\SEH\myseh.cpp / link - SAFESEH:NO #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #include <stdlib.h> DWORD scratch; EXCEPTION_DISPOSITION __cdecl _except_handler(struct _EXCEPTION_RECORD *ExceptionRecord, void* EstablisherFrame, struct _CONTEXT *ContextRecord, void* DispatcherContext){ //指明是我们让流程转到我们的异常处理程序的 printf("进入Seh异常处理程序!\n"); //改变CONTEXT结构体EAX的值,以便它指向可以成功进行写操作的位置 ContextRecord->Eax = (DWORD)&scratch; printf("异常处理完毕!\n"); //告诉操作系统重新执行出错的指令 return ExceptionContinueExecution; } int main(){ DWORD handler = (DWORD)_except_handler; printf("安装seh异常回调函数!\n"); __asm{ //创建EXCEPTION_REGISTRATION结构: push handler //handler函数的地址 push fs:[0] //保存前一个handler函数地址,即EXCEPTION_REGISTRATION结构的prev mov fs:[0],ESP //安装新的EXCEPTION_REGISTRATION结构 } printf("安装完毕,向0地址写入数据,引发异常!\n"); __asm { xor eax, eax //EAX = 0 mov dword ptr[eax], 1234h //写EAX指向的内存从而故意引发一个异常! } printf("写入数据完毕,scratch=0x%x!\n",scratch); printf("卸载seh自定义的异常回调函数!\n"); __asm{ // 移去我们的EXECEPTION_REGISTRATION结构 mov eax, [ESP] // 此时栈顶为prev,后进先出原理。获取前一个EXECEPTION_REGISTRATION结构的指针 mov fs:[0], EAX // 安装前一个结构 add esp, 8 // 将我们的EXECEPTION_REGISTRATION弹出堆栈 } system("Pause"); return 0; }
(5)SEH链及异常的传递(通知调试器→SEH链→顶层异常处理→系统默认处理)
(1)系统查看产生异常的进程是否被正在被调试,如果正在被调试,那么向调试器发送EXCEPTION_DEBUG_EVENT事件。
(2)如果进程没有没有被调试或者调试器不去处理这个异常,那么系统检查异常所处的线程并在这个线程环境中查看fs:[0]来确定是否安装SEH异常处理回调函数,如果有则调用它。
(3)回调函数尝试处理这个异常,如果可以正确处理的话,则修正错误并将返回值设置为ExceptionContinueExecution,这时系统将结束整个查找过程。
(4)如果回调函数返回ExceptionContinueSearch,相当于告诉系统它无法处理这个异常,系统将根据SEH链中的prev字段得到前一个回调函数地址并重复步骤3,直至链中的某个回调函数返回ExceptionContinueExection为止,查找结束。
(5)如果到了SEH链的尾部却没有一个回调函数愿意处理这个异常,那么系统会再被检查进程是否正在被调试,如果被调试的话,则再一次通知调试器。
(6)如果调试器还是不去处理这个异常或进程没有被调试,那么系统检查有没有Final型的异常处理回调函数,如果有,就去调用它,当这个回调函数返回时,系统会根据这个函数的返回值做相应的动作。
(7)如果没有安装Final型回调函数,系统直接调用默认的异常处理程序终止进程,但在终止之前,系统再次调用发生异常的线程中的所有异常处理过程,目的是让线程异常处理过程获得最后清理未释放资源的机会,其后程序终止。
【MySEH2】SEH链及异常传递
/************************************************************************ MYSEH2 - Matt pietrek 1997 Microsoft Systems Journal,january 1997 File: MySEH2.cpp 用IDE编译时须将项目的“链接器”→“高级”→“映像具有安全异常处理程序”设为SAFESEH:NO(Debug或Release版要分别设置) ************************************************************************/ #include <windows.h> #include <stdio.h> //异常处理回调函数 EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext){ //显示异常信息 printf("HomeGrown异常处理回调函数:异常码(%08X) 标志(%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"); //我们不想处理这个异常,让其它SEH链的其他回调函数去处理 return ExceptionContinueSearch; } void HomeGrownFrame(void){ DWORD handler = (DWORD)_except_handler; __asm{ //创建EXCEPTION_REGISTRATION结构 push handler //handler函数的地址 push FS :[0] //保存前一个handler地址 mov FS :[0],ESP //安装新的EXCEPTION_REGISTRATION结构 } *(PDWORD)0 = 0; //向地址0数据,从而引发一个异常 printf("_except_handle函数不处理异常,此句永远不会被执行!\n"); __asm{ //移去我个的EXCEPTION_REGISTRATION结构 mov eax, [ESP] //获取前一个结构 mov FS : [0],eax //安装前一个结构 add esp,8 //把我们的EXCEPTION_REGISTRATION结构弹出 } } int main(){ //为了简化代码,使用编译器层面的异常处理,即__try{} __excetp{} __try{ HomeGrownFrame(); } __except (EXCEPTION_EXECUTE_HANDLER){ //这里处理了异常,不再向外层传递 printf("主函数main中的异常处理过程\n"); } return 0; }