结构化异常处理(SEH)是Windows操作系统提供的强大异常处理功能。而Visual C++中的__try{}/__finally{}和__try{}/__except{}结构本质上是对Windows提供的SEH的封装。
一、分类
- Per-Thread类型SEH(也称为线程异常处理),用来监视某线程代码是否发生异常。
- Final类型SEH(也称为进程异常处理、筛选器或顶层异常处理),用于监视整个进程中所有线程是否发生异常。在整个进程中,该类型异常处理过程只有一个,可通过SetUnhandledExceptionFilter设置。
二、SEH相关的数据结构和API
2.1、相关结构
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;
2.2、相关API
(1)SetErrorMode:控制错误模式,如是否出现错误对话框
(2)SetUnhandledExceptionFilter:设定顶层异常处理回调函数
(3)RaiseException:用于引发异常,可指定自己的异常代码及相关信息
(4)GetThreadContext/SetThreadContext:获取或设置线程环境
(5)RtlUnwind:栈展开操作
三、线程异常处理
局部的,仅仅监视进程中某特定线程是否发生异常。
3.1、线程异常处理特点
①Windows系统为每个线程单独提供了一种异常处理的方法,当一个线程出现错误时,操作系统调用用户定义的一系列回调函数,在这些回调函数中,可以进行修复错误或其它的一些操作,最后的返回值告系统系统下一步的动作(如继续搜索异常处理程序或终止程序等)。
②SEH是基于线程的,使用SEH可以为每个线程设置不同的异常处理程序(回调函数)而且可以为每个线程设置多个异常处理程序。
③ 由于SEH使用了与硬件平台相关的数据指针,所以不同硬件平台使用SHE的方法有所不同。
3.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.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]指向该指构。
3.4、异常回调函数的调用过程
①线程信息块(TIB),永远放在fs段选择器指定的数据段的0偏移处,即fs:[0]的地方就是TIB结构。对不同的线程fs寄存器的内容有所有不同,但fs:[0]都是指向当前线程的TIB结构体,所以fs:[0]是一个EXCEPTION_REGISTRATION结构体的指针。
②当异常发生时,系统从fs:[0]指向的内存地址处取出ExceptionList字段,然后从ExceptionList字段指向的EXCEPTION_REGISTRATION结构中取出handler字段,并根据其中的地址去调用异常处理程序(回调函数)。
3.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型回调函数,系统直接调用默认的异常处理程序终止进程,但在终止之前,系统再次调用发生异常的线程中的所有异常处理过程,目的是让线程异常处理过程获得最后清理未释放资源的机会,其后程序终止。
四、异常处理的堆栈展开(Stack Unwind)
(1)什么是展开操作
①当发生异常时,系统遍历EXCEPTION_REGISTRATION结构链表,从链表头,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次重新遍历这个链表,直到处理这个异常的节点为止(即返回ExceptionContinueExecution节点)。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义 为EH_UNWINDING
②注意展开操作是发生在出现异常时,这个异常回调函数拒绝处理的时候(即返回ExceptionContinueSearch)。这里系统会从链表头开始遍历(因异常的嵌套,可理解为由内层向外层),所以各异常回调函数会第1次被依次调用,直到找到同意处理的节点。然后,再重新从链表头开始(即由内向外)第2次调用以前那些曾经不处理异常的节点,直到同意处理的那个异常的节点为止。
③当一个异常处理程序拒绝处理某个异常时,它实际上也就无权决定流程最终将从何处恢复。只有处理某个异常的异常处理程序才能决定待所有异常处理代码执行完毕之后流程最终将从何处恢复。即,当异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程从处理异常的那个回调函数决定的地方开始继续执行。
④展开操作完成后,同意处理异常的回调函数也必须负责把Fs:[0]恢复到处理这个异常的EXCEPTION_REGISTRATION上,即展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,这个异常处理也就成了SEH链表的第1个节点。
(2)为什么要进行堆栈展开
①第1个原因是告知回调函数将被卸掉,以让被卸载的回调函数有机会进行一些清理未释放资源的机会。因为一个函数发生异常时,执行流程通常不会从这个函数正常退出。所以可以导致资源未被正确释放(如C++类的对象析构函数没被调用等)。
②第2个原因是如果不进行堆栈展开,可能会引发未知的错误。(见《软件加密技术内幕》第132-134页)。
(3)如何展开:RtlUnwind(lpLastStackFrame,lpCodelabel,lpExceptionRecord,dwRet);
①lpLastStackFrame:当遍历到这个帧时就停止展开异常帧。为NULL时表示展开所有回调函数。
②lpCodeLabel:指向该函数返回的位置。如果指定为NULL,表示函数使用正常的方式返回。
③lpExceptionRecord:指定一个EXCPETION_RECORD结构。这个结构将在展开操作的时候被传给每一个被调用的回调函数,一般建议使用NULL来让系统自动生成代表展开操作的EXCEPTION_RECORD结构。
④dwRet一般不被使用。
五、编译器层面对系统SEH机制的封装
5.1 扩展的EXCEPTION_REGISTRATION级相关结构:VC_EXCEPTION_REGISTRATION
(1)VC_EXCEPTION_REGISTRATION结构
struct VC_EXCEPTION_REGISTRATION { VC_EXCEPTION_REGISTRATION* prev; //前一个结构体的指针 FARPROC handler; //永远指向_exception_handler4回调函数 scopetable_entry* scopetable;//指向scpoetable数组的指针 int _index; //有的书上也叫tryLevel。scopetable项的当前项 DWORD _ebp; //当前ebp值,用于访问各成员 }
(2)scopetable_entry结构体
struct scopetable_entry { DWORD prev_entryindex;//指向前一个scopetable_entry在scopetable中的索引 FARPROC lpfnFilter; //对应于__except块后小括号内的过滤函数;__finally时为NULL FARPROC lpfnHandler;//__exception或__finally后花括号{}内的代码地址。 }
(3)VC异常帧堆栈布局及VC默认的异常处理
5.2 数据结构组织
(1)每个函数只注册一个VC_EXCEPTION_REGISTRATION结构(也叫异常帧,如图中的有5个Frame,即有5个函数调用)。可见,该SEH异常链从链表头部到链表尾共有5节点,分对应于5个异常处理帧。但需注意的是,通过VC安装的节点为VC_EXCEPTION_REGISTRATION结构,图中有3个,对应的回调函数为VCSHE!_exception_handler4(0x00E0178),而系统安装的是EXCEPTION_REGISTRATION结构的帧,位于链表尾部最后的两个节点,对应的回调函数分别为ntdll!_exception_handler4。
(2)VC为每个函数内的所有__try块建立一个scopetable表,其中每个__try块对应于scopetable中的一项。(用scopetable_entry结构体来表示这个__try项,结构里分别用lpfnFilter和lpfnHandler来表示__except/__finally的过滤函数和处理函数,其中_finally没有过滤函数,只有异常处理函数)。
(3)若有__try块嵌套,则在scopetable_entry结构里的prev_entryindex或指明,多层嵌套形成单向链表。
(4)对于VC的异常处理,其每个异常帧的回调处理函数都统一设为_except_handler4。每进入一个try块里,编译器会将VC_EXCEPTION_REGISTRATION中tryLevel赋值为相应的值。一旦该try块异常发生,系统会先从VC_EXCEPTION_REGISTRATION的handler域中找到_exception_handler4函数(C运行时库函数),然后根据当前tryLevel的值找到scopetable表中这个__try块相应的过滤函数和处理函数对异常进行相应的处理。
(5)与_except块不同,_finally块的lpfnFilter为NULL,即没有过滤函数。
5.3 _exception_handler4函数的执行流程
(1)异常发生时,根据index找到scopetable项,并调用lfpnFilter。如果过滤函数lpfnFilter返回EXCEPTION_EXECUTE_HANDLER,则执行全局展开之后调用lpfnHandler函数。如果过滤函数lpfnFilter返回EXCEPTION_CONTINUE_EXECUTION,则_except_handler4简单地返回EXCEPTION_CONTINUE_EXECUTION,交由系统恢复线程的执行。
(2)如果lpfnFilter返回EXCEPTION_CONTINUE_SEARCH时,此时_except_handler4查看previndex是否是0xFFFFFFFE,若是则_except_handler4返回ExceptionContinueSearch让系统继续遍历外层SEH链或由系统直接处理。否则_except_handler4根据previndex找到相应的过滤函数,根据其返回值重复上面的动作。直到异常被处理或previndex为0xFFFFFFFE为止。
5.4 小结:异常处理流程及全局展开
参考:
https://www.cnblogs.com/5iedu/p/5205428.html
https://www.cnblogs.com/5iedu/p/5223846.html