历史:
v1.0.0, 2011-10-05:最初版本。
v1.0.1, 2011-10-06:补充总结之前的资源泄漏示例。
v1.0.2, 2011-10-31:调整若干描述语句;增加 ExceptionNestedException 返回值的补充说明。
注:本帖内容没有同步更新,请下载附件。
[不介意转载,但请注明出处 boxcounter.com
附件里有本文的原始稿,一样的内容,更好的高亮和排版。分别是 html 和 rtl 格式。
后面的部分代码可能会因为自动换行变得很乱,需要的朋友手动复制到自己的代码编辑器就可以正常显示了]
这两天琢磨了下SEH,这里记录一下自己琢磨的一些心得。
SEH 这个概念我就不啰嗦了,任何一个介绍 SEH 的资料都有讲。我主要记录一些自己的理解。可能有一些概念理解的不够清晰,有一些说法比较狭隘,欢迎看到本文的朋友一起讨论、修正,非常感谢。
首先,SEH 是针对于异常的一种处理机制,这个异常分为硬件异常和软件异常,这里所说的硬件异常是狭义的异常,也就是 CPU 产生的异常。比如除零操作,CPU 执行除零操作时候,会自主启动异常处理机制。软件异常,就是程序模拟的异常,比如调用 RaiseException 函数。软件异常是可以随意触发的,windows 系统内部遇到问题会触发,开发人员高兴了也可以触发。
抛出了问题,就要有解决方案。那这么多问题和解决方案,如何管理呢?windows 系统当仁不让的提供了它管理方案 —— SEH。我看一些资料有详细的讨论 SEH 的确切含义,这里我不参与讨论,而只是简单的理解为“系统提供的异常处理机制,以及编译器对其进行增强的部分”。
来说说系统提供的异常处理机制。
windows 提供的异常处理机制实际上只是一个简单的框架,一般情况下开发人员都不会直接用到。咱通常所用的异常处理(比如 C++ 的 throw、try、catch)都是编译器在系统提供的异常处理机制上进行加工了的增强版本。这里先抛开增强版的不提,继续说原始版本。
原始版本的机制很简单:谁都可以触发异常,谁都可以处理异常(只要它能看得见)。但是不管是触发还是处理都得先登记。系统把这些登记信息保存在一个链表里,并且这个链表保存在线程的数据结构里。也就是说,异常所涉及的一些行为都是线程相关的。比如,线程 T1 触发的异常就只能由线程 T1 来处理,其他线程根本就不知道 T1 发生了什么事,更不会狗拿耗子。
等登记完毕后,线程就可以抛出或处理异常了,系统也可以做相应的管理工作了。(这里啰嗦一句,系统提供的 SEH 其实是一个针对于“触发异常-解决异常”的管理机制,系统自身是不提供任何具体异常的解决方案的。解决方案还是要由用户自身来提供(增强版里编译器也会来提供解决方案,来帮“不负责”的程序猿擦屁股,这是后话))
系统提供的管理工作简单来说包括(但不限于):找到触发异常的线程的异常处理链表(前头登记的那个),然后按照规则(具体的规则后续再说)对该异常进行分发,根据分发后的处理结果再进行下一步的分发或者结束处理。
系统管理所使用的数据结构和宏:
[font=Consolas][color=#000000] [/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_CHAIN_END [/color][color=#000080](([/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_REGISTRATION_RECORD [/color][color=#000080]* [/color][color=#000000]POINTER_32[/color][color=#000080])-[/color][color=#800080]1[/color][color=#000080])
[/color][color=#0000FF]typedef enum [/color][color=#000000]_EXCEPTION_DISPOSITION [/color][color=#000080]{
[/color][color=#000000]ExceptionContinueExecution[/color][color=#000080],
[/color][color=#000000]ExceptionContinueSearch[/color][color=#000080],
[/color][color=#000000]ExceptionNestedException[/color][color=#000080],
[/color][color=#000000]ExceptionCollidedUnwind
[/color][color=#000080]} [/color][color=#000000]EXCEPTION_DISPOSITION[/color][color=#000080];
[/color][color=#0000FF]typedef struct [/color][color=#000000]_EXCEPTION_RECORD [/color][color=#000080]{
[/color][color=#000000]DWORD ExceptionCode[/color][color=#000080];
[/color][color=#000000]DWORD ExceptionFlags[/color][color=#000080];
[/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_RECORD [/color][color=#000080]*[/color][color=#000000]ExceptionRecord[/color][color=#000080];
[/color][color=#000000]PVOID ExceptionAddress[/color][color=#000080];
[/color][color=#000000]DWORD NumberParameters[/color][color=#000080];
[/color][color=#000000]ULONG_PTR ExceptionInformation[/color][color=#000080][[/color][color=#000000]EXCEPTION_MAXIMUM_PARAMETERS[/color][color=#000080]];
} [/color][color=#000000]EXCEPTION_RECORD[/color][color=#000080];
[/color][color=#0000FF]typedef [/color][color=#000000]EXCEPTION_RECORD [/color][color=#000080]*[/color][color=#000000]PEXCEPTION_RECORD[/color][color=#000080];
[/color][color=#0000FF]typedef
[/color][color=#000000]EXCEPTION_DISPOSITION
[/color][color=#000080](*[/color][color=#000000]PEXCEPTION_ROUTINE[/color][color=#000080]) (
[/color][color=#000000]IN [/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_RECORD [/color][color=#000080]*[/color][color=#000000]ExceptionRecord[/color][color=#000080],
[/color][color=#000000]IN PVOID EstablisherFrame[/color][color=#000080],
[/color][color=#000000]IN OUT [/color][color=#0000FF]struct [/color][color=#000000]_CONTEXT [/color][color=#000080]*[/color][color=#000000]ContextRecord[/color][color=#000080],
[/color][color=#000000]IN OUT PVOID DispatcherContext
[/color][color=#000080]);
[/color][color=#0000FF]typedef struct [/color][color=#000000]_EXCEPTION_REGISTRATION_RECORD [/color][color=#000080]{
[/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_REGISTRATION_RECORD [/color][color=#000080]*[/color][color=#000000]Next[/color][color=#000080];
[/color][color=#000000]PEXCEPTION_ROUTINE Handler[/color][color=#000080];
} [/color][color=#000000]EXCEPTION_REGISTRATION_RECORD[/color][color=#000080];
[/color][color=#0000FF]typedef [/color][color=#000000]EXCEPTION_REGISTRATION_RECORD [/color][color=#000080]*[/color][color=#000000]PEXCEPTION_REGISTRATION_RECORD[/color][color=#000080];
[/color][/font]
其中 EXCEPTION_REGISTRATION_RECORD 结构就是登记信息。来介绍下它的成员:
1. EXCEPTION_REGISTRATION_RECORD::Next 域指向下一个 EXCEPTION_REGISTRATION_RECORD,由此构成一个异常登记信息(从字面上说,应该叫做“异常注册记录”更恰当)链表。链表中的最后一个结点会将 Next 置为 EXCEPTION_CHAIN_END,表示链表到此结束。
2. EXCEPTION_REGISTRATION_RECORD::Handler 指向异常处理函数。
前面有简单的说过原始版本 SEH 的管理工作,这里再根据以上列出的相关数据结构稍微详细一点说说。
当接收到异常后,系统找到当前线程(还记不记得,前面有说过,异常是线程相关的。系统接收到的异常就是当前正在运行的线程触发的。其实这个说法还不准确,DPC 也会触发异常,而它是线程无关的,这里为了方便理解,先只考虑线程)的异常链表,从链表中的第一个结点开始遍历,找到一个 EXCEPTION_REGISTRATION_RECORD 就调用它的 Handler,并把该异常(由第一个类型为 EXCEPTION_RECORD 的参数表示)传递给该 Handler,Handler 处理并返回一个类型为 EXCEPTION_DISPOSITION 的枚举值。该返回值指示系统下一步该做什么:
ExceptionContinueExecution 表示:“我已修正了此异常的故障,请你从事发点重新执行,谢谢”。
ExceptionContinueSearch 表示:“我没有处理此异常,请你继续搜索其他的解决方案,抱歉”。
ExceptionNestedException 和 ExceptionCollidedUnwind 这里先不做解释,后面会细说。
这样系统根据不同的返回值来继续遍历异常链表或者回到触发点继续执行。
需要说明一下,本文主要以内核模式下的异常来说,因为相比用户模式下的异常处理流程,内核模式少了模式切换、栈切换以及反向回调等步骤。
我们现在来看看详细的内核异常流程。
首先,CPU 执行的指令触发了异常,CPU 改执行 IDT 中 KiTrap??,KiTrap?? 会调用 KiDispatchException。该函数原型如下:
VOID
KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
);
其名称明白的说明了函数的主要功能:分派异常。其实现可以参考 $wrk-v1.2\base\ntos\ke\i386\exceptn.c:1033。我的笔记:
在当前栈中分配一个 CONTEXT,调用 KeContextFromKframes 初始化它。
检查 ExceptionRecord->ExceptionCode,如果:
是 STATUS_BREAKPOINT,那么将 CONTEXT::Eip 减一;
是 KI_EXCEPTION_ACCESS_VIOLATION,那么将检查是否是由 AtlThunk 触发(这个小环节没有深究),如果是触发 NX(不可执行),那么将 ExceptionRecord->ExceptionInformation [0] 置为 0(貌似表示触发操作的类型,0表示读、1表示写);
如果 PreviousMode 是 KernelMode,那么,
如果 FirstChance 为 TRUE,那么将该异常传达给内核调试器,如果内核调试器没有处理,那么调用 RtlDispatchException 进行处理。
如果 FirstChance 为 FALSE,那么再次将该异常传达给内核调试器,如果内核调试器没有处理,那么 BUGCHECK。
如果 PreviousMode 是 UserMode,那么,
如果 FirstChance 为 TRUE,那么将该异常传达给内核调试器,如果内核调试器没有处理,那么将异常传达给应用层调试器。如果仍然没有处理,那么将 KTRAP_FRAME 和 EXCEPTION_RECORD 拷贝到 UserMode 的栈中,并设置 KTRAP_FRAME::Eip 设置为 ntdll!KiUserExceptionDispatcher,返回(将该异常交由应用层异常处理程序进行处理)。
如果 FirstChance 为 FALSE,那么再次将异常传达给应用层调试器,如果仍然没有处理,那么调用 ZwTerminateProcess 结束进程,并 BUGCHECK。
抛开应用层异常不说,我们来看 PreviousMode 是 KernelMode 的情况,其重点是调用 RtlDispatchException 的操作。我们来看一下这个函数:
BOOLEAN
RtlDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PCONTEXT ContextRecord
);
它的实现可以参考 $wrk-v1.2\base\ntos\rtl\i386\exdsptch.c:126。我的笔记:
遍历当前线程的异常链表,挨个调用 RtlpExecuteHandlerForException,RtlpExecuteHandlerForException 会调用异常处理函数。再根据返回值做出不同的处理:
对于 ExceptionContinueExecution,结束遍历,返回。(对于标记为‘EXCEPTION_NONCONTINUABLE’的异常,会调用 RtlRaiseException。)
对于 ExceptionContinueSearch,继续遍历下一个结点。
对于 ExceptionNestedException,则从指定的新异常继续遍历。
只有正确处理 ExceptionContinueExecution 才会返回 TRUE,其他情况都返回 FALSE。
在继续讲述异常处理机制之前,咱们需要先来认识一下异常链表。
之前有提到过:系统将异常链表头保存在线程结构里。来看看具体的数据结构:
线程的内核数据结构体现是 _ETHREAD,从它开始进入,直到咱们关注的异常链表。
kd> dt _ETHREAD
ntdll!_ETHREAD
+0x000 Tcb : _KTHREAD
... 省略之后的成员
kd> dt _KTHREAD
ntdll!_KTHREAD
... 省略的域成员
+0x074 Teb : Ptr32 Void
... 省略的域成员
Teb 成员的类型实际是 _TEB,来看看
kd> dt _TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
... 省略的域成员
kd> dt _NT_TIB
ntdll!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB
_NT_TIB 的第一个域成员 ExceptionList 就是异常链表头。
但是系统不是这么一步一步找的,而是借助 FS 寄存器来加速寻找。先来说说系统对 FS 的使用。
在应用层,FS 寄存器“指向”当前执行线程的 _TEB 结构体。在内核层,FS 寄存器“指向”另一个跟 CPU 相关的结构体:_KPCR,来看看它的结构,
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
... 省略的域成员
与 _TEB 一样,它的第一个域成员也是 _NT_TIB,只不过此时是 nt!_NT_TIB,而在应用层是 ntdll!_NT_TIB,但它们的结构是一样的。
这样,不论在应用层还是在内核层,系统都可以使用 FS:[0] 找到异常链表。
到这里,咱们已经聊完了 CPU 触发的异常的处理流程,总结一下它的调用流程:
CPU 检测到异常 -> KiTrap?? -> KiDispatchException -> RtlDispatchException -> RtlpExecuteHandlerForException
这是硬件异常,咱们再来看看软件异常。
软件异常跟硬件异常的处理流程非常接近,只有触发点的不同,调用流程是:
RtlRaiseException -> RtlDispatchException -> RtlpExecuteHandlerForException
后面两个被调用的函数咱已经聊过了,主要来看看 RtlRaiseException。这个函数从其名字上就能看出是用来触发异常的。原型如下:
VOID
RtlRaiseException (
IN PEXCEPTION_RECORD ExceptionRecord
);
其实现可以参考 $wrk-v1.2\base\ntos\rtl\i386\raise.asm:71。我的笔记:
RtlRaiseException 首先调用 RtlDispatchException 分发异常,如果 RtlDispatchException 成功分发(有处理函数处理了这个异常),那么结束本函数。
如果没有成功分发,那么调用 ZwRaiseException 再次触发该异常,这次传入的异常的 FirstChance 被置为 FALSE。
到这里,系统提供的 SEH 机制(本文又称之为原始版本)大致讲解完毕。咱可以回味一下:
1. 原始版本的实现较简单,代码量不大,而且 wrk 基本上有所有关键函数的实现代码。
2. 原始版本的功能过于简单,实际过程中很难直接使用。整个异常处理过程无非就是遍历异常链表,挨个调用异常注册信息的处理函数,如果其中有某个处理函数处理了该异常(返回值为 ExceptionContinueExecution),那么就从异常触发点(如果是断点异常,则要回退一个字节的指令(int 3 指令本身))重新执行。否则不管是整个链表中没有找到合适的处理函数(返回值为 ExceptionContinueSearch),或者遍历过程中出现问题(返回值为 ExceptionNestedException),系统都会简单粗暴的 BUGCHECK。而这也带来一个问题:
线程运行过程中会调用很多个函数,每个函数都有可能注册异常处理,它们提供的异常处理函数既可能处理该函数自身触发的异常,又可能需要处理其子孙函数触发的异常。前者还好说,自己出了问题,多少还有可能自己修复。而后者就很头疼了,它无法了解所有其调用的子孙函数内部的实现,要想修复子孙函数触发的异常,太困难了。而一旦没有正确处理,或者没人处理,系统就崩掉。这个后果太严重。于是实际上现实程序设计中,基本上没有直接使用原始版本的 SEH,而是使用编译器提供的增强版本。
下面咱们就来聊聊编译器提供的增强版本。
首先要说明,增强版本有很多个,不同的编译器提供的的 SEH 增强版本或多或少都有不同处。但是,他们一般都是基于 windows 系统提供的原始版本进行完善的。一个典型的增强版就是微软的编译器(后面简称为 MSC)里提供的 __try、__finally,__except。咱们接下来就用这个增强版作为目标进行分析。我使用的 MSC 是 WDK 7600.16385.1,内置的 cl 的版本是15.00.30729.207,link 的版本是9.00.30729.207,测试虚拟机系统为 32位 Win2k3sp1 + wrk。
咱们先看看增强版的数据结构,跟之前的原始版本有很多相似之处:
[font=Consolas][color=#000000] [/color][color=#0000FF]typedef struct [/color][color=#000000]_EXCEPTION_REGISTRATION PEXCEPTION_REGISTRATION[/color][color=#000080];
[/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_REGISTRATION[/color][color=#000080]{
[/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_REGISTRATION [/color][color=#000080]*[/color][color=#000000]prev[/color][color=#000080];
[/color][color=#0000FF]void [/color][color=#000080](*[/color][color=#000000]handler[/color][color=#000080])([/color][color=#000000]PEXCEPTION_RECORD[/color][color=#000080], [/color][color=#000000]PEXCEPTION_REGISTRATION[/color][color=#000080], [/color][color=#000000]PCONTEXT[/color][color=#000080], [/color][color=#000000]PEXCEPTION_RECORD[/color][color=#000080]);
[/color][color=#0000FF]struct [/color][color=#000000]scopetable_entry [/color][color=#000080]*[/color][color=#000000]scopetable[/color][color=#000080];
[/color][color=#0000FF]int [/color][color=#000000]trylevel[/color][color=#000080];
[/color][color=#0000FF]int [/color][color=#000000]_ebp[/color][color=#000080];
[/color][color=#000000]PEXCEPTION_POINTERS xpointers[/color][color=#000080];
};[/color][/font]
这个 EXCEPTION_REGISTRATION 在增强版中就相当于原始版本中的 EXCEPTION_REGISTRATION_RECORD。可以这么理解它:
[font=Consolas][color=#000000] [/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_REGISTRATION[/color][color=#000080]{
[/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_REGISTRATION_RECORD ExceptionRegistrationRecord[/color][color=#000080];
[/color][color=#0000FF]struct [/color][color=#000000]scopetable_entry [/color][color=#000080]*[/color][color=#000000]scopetable[/color][color=#000080];
[/color][color=#0000FF]int [/color][color=#000000]trylevel[/color][color=#000080];
[/color][color=#0000FF]int [/color][color=#000000]_ebp[/color][color=#000080];
[/color][color=#000000]PEXCEPTION_POINTERS xpointers[/color][color=#000080];
}; [/color][color=#008000]// 注:本结构体只用于理解原始版和增强版的区别,实际代码中并没有这种形式的定义 [/color][/font]
也就是说它沿用了老版本的注册信息结构,只是在域成员名称上做了些改动,把 Next 改名为 prev,把 Handler 改为 handler。除此之外,在原始版本基础上增加了4个域成员(scopetable、trylevel、_ebp、xpointers),用来支持它的增强功能。
需要说明的是,这结构体来源于 MSC 的 crt 源码里的 exsup.inc,这个文件使用的是汇编语法,该结构体定义是从该文件的注释中提取出来。在实际的分析过程中,发现它的定义有一些问题:最后一个域成员 xpointers 实际上存放在 prev 之前,也就是说,实际中 __try 增强版用的结构体是这样的:
[font=Consolas][color=#000000]
[/color][color=#0000FF]typedef struct [/color][color=#000000]_EXCEPTION_REGISTRATION PEXCEPTION_REGISTRATION[/color][color=#000080];
[/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_REGISTRATION[/color][color=#000080]{
[/color][color=#000000]PEXCEPTION_POINTERS xpointers[/color][color=#000080];
[/color][color=#0000FF]struct [/color][color=#000000]_EXCEPTION_REGISTRATION [/color][color=#000080]*[/color][color=#000000]prev[/color][color=#000080];
[/color][color=#0000FF]void [/color][color=#000080](*[/color][color=#000000]handler[/color][color=#000080])([/color][color=#000000]PEXCEPTION_RECORD[/color][color=#000080], [/color][color=#000000]PEXCEPTION_REGISTRATION[/color][color=#000080], [/color][color=#000000]PCONTEXT[/color][color=#000080], [/color][color=#000000]PEXCEPTION_RECORD[/color][color=#000080]);
[/color][color=#0000FF]struct [/color][color=#000000]scopetable_entry [/color][color=#000080]*[/color][color=#000000]scopetable[/color][color=#000080];
[/color][color=#0000FF]int [/color][color=#000000]trylevel[/color][color=#000080];
[/color][color=#0000FF]int [/color][color=#000000]_ebp[/color][color=#000080];
};[/color][/font]
相关的宏和结构
[font=Consolas][color=#000000] TRYLEVEL_NONE [/color][color=#0A246A]equ -[/color][color=#800080]1
[/color][color=#000000]TRYLEVEL_INVALID [/color][color=#0A246A]equ -[/color][color=#800080]2[/color][/font]
scopetable_entry
+0x000 previousTryLevel : Uint4B
+0x004 lpfnFilter : Ptr32 int
+0x008 lpfnHandler : Ptr32 int
咱们先来简单的看一下增强版中出现的几个新域成员。
EXCEPTION_REGISTRATION::scopetable 是类型为 scopetable_entry 的数组。
EXCEPTION_REGISTRATION::trylevel 是数组下标,用来索引 scopetable 中的数组成员。
_ebp 是包含该 _EXCEPTION_REGISTRATION 结构体的函数的栈帧指针。对于没有 FPO 优化过的函数,一开头通常有个 push ebp 的操作,_ebp 的值就是被压入的 ebp 的值,后续咱们通过代码就再看实际的应用。
按照原始版本的设计,每一对“触发异常-处理异常”都会有一个注册信息即 EXCEPTION_REGISTRATION_RECORD。也就是说,如果按照原始的设计,每一个 __try/__except(__finally) 都应该对应一个 EXCEPTION_REGISTRATION。但是实际的 MSC 实现不是这样的。
真正的实现是:
每个使用 __try/__except(__finally) 的函数,不管其内部嵌套或反复使用多少 __try/__except(__finally),都只注册一遍,即只将一个 EXCEPTION_REGISTRATION 挂入当前线程的异常链表中(对于递归函数,每一次调用都会创建一个 EXCEPTION_REGISTRATION,并挂入线程的异常链表中,这是另外一回事)。
那如何处理函数内部出现的多个 __try/__except(__finally) 呢?这多个 __except 代码块的功能可能大不相同,而注册信息 EXCEPTION_REGISTRATION 中只能提供一个处理函数 handler,怎么办?
MSC 的做法是,MSC 提供一个处理函数,即 EXCEPTION_REGISTRATION::handler 被设置为 MSC 的某个函数,而不是程序猿提供的 __except 代码块。程序猿提供的多个 __except 块被存储在 EXCEPTION_REGISTRATION::scopetable 数组中。我们看看上面的 scopetable_entry 定义,由于我没有找到它的定义代码,所以就贴了 windbg 中 dt 输出结果。
其中 scopetable_entry::lpfnHandler 就是程序猿提供的 __except 异常处理块代码。而 lpfnFilter 就是 __except 的过滤块代码。对于 __finally 代码块,其 lpfnFilter 被置为 NULL,lpfnHandler 就是其包含的代码块。
下面,我们用一小段简单的伪代码来详细说明。
[font=Consolas][color=#000000] [/color][color=#800080]1 [/color][color=#000000]VOID SimpleSeh[/color][color=#000080]()
[/color][color=#800080]2 [/color][color=#000080]{
[/color][color=#800080]3 [/color][color=#000000]__try
[/color][color=#800080]4 [/color][color=#000080]{
[/color][color=#800080]5 [/color][color=#000080]}
[/color][color=#800080]6 [/color][color=#000000]__except[/color][color=#000080]([/color][color=#000000]ExceptionFilter_0[/color][color=#000080](...))
[/color][color=#800080]7 [/color][color=#000080]{
[/color][color=#800080]8 [/color][color=#000000]ExceptCodeBlock_0[/color][color=#000080];
[/color][color=#800080]9 [/color][color=#000080]}
[/color][color=#800080]10
11 [/color][color=#000000]__try
[/color][color=#800080]12 [/color][color=#000080]{
[/color][color=#800080]13 [/color][color=#000000]__try
[/color][color=#800080]14 [/color][color=#000080]{
[/color][color=#800080]15 [/color][color=#000080]}
[/color][color=#800080]16 [/color][color=#000000]__except[/color][color=#000080]([/color][color=#000000]ExceptionFilter_1[/color][color=#000080](...))
[/color][color=#800080]17 [/color][color=#000080]{
[/color][color=#800080]18 [/color][color=#000000]ExceptCodeBlock_1[/color][color=#000080];
[/color][color=#800080]19 [/color][color=#000080]}
[/color][color=#800080]20 [/color][color=#000080]}
[/color][color=#800080]21 [/color][color=#000000]__except[/color][color=#000080]([/color][color=#000000]ExceptionFilter_2[/color][color=#000080](...))
[/color][color=#800080]22 [/color][color=#000080]{
[/color][color=#800080]23 [/color][color=#000000]ExceptCodeBlock_2[/color][color=#000080];
[/color][color=#800080]24 [/color][color=#000080]}
[/color][color=#800080]25 [/color][color=#000080]}[/color][/font]
编译时,编译器会为 SimpleSeh 分配一个 EXCEPTION_REGISTRATION 和一个拥有3个成员的 scopetable 数组,并将 EXCEPTION_REGISTRATION::scopetable 指向该数组(请留意:EXCEPTION_REGISTRATION::scopetable 只是一个指针,不是数组)。然后按照 __try 关键字出现的顺序,将对应的__except/__finally 都存入该数组,步骤如下:
scopetable[0].lpfnFilter = ExceptionFilter_0;
scopetable[0].lpfnHandler = ExceptCodeBlock_0;
scopetable[1].lpfnFilter = ExceptionFilter_1;
scopetable[1].lpfnHandler = ExceptCodeBlock_1;
scopetable[2].lpfnFilter = ExceptionFilter_2;
scopetable[2].lpfnHandler = ExceptCodeBlock_2;
我们假象当前开始执行 SimpleSeh 函数,在行14和行15之间触发了异常。
根据之前我们的讨论的流程:RtlRaiseException -> RtlDispatchException -> RtlpExecuteHandlerForException。
RtlpExecuteHandlerForException 会调用注册信息中的处理函数,即 EXCEPTION_REGISTRATION::handler。该函数是由 MSC 提供的,内部会依次调用 scopetable 中的 lpfnHandler。
那咱们来模拟执行一下,在14和15行之前触发异常,那应该先从 scopetable[2] 的 ExceptionFilter_2 开始执行,假设该函数返回 EXCEPTION_CONTINUE_SEARCH。那接下来应该是 scopetable[1],假设 ExceptionFilter_1 也返回 EXCEPTION_CONTINUE_SEARCH。那么接下来是不是就应该轮到 scopetable[0] 了?不是。咱们再看看上面的伪代码,行14和行15之间的代码并没处于第一个 __try/__except 的范围中,该异常轮不到 scopetable[0] 来处理。那怎么办?SimpleSeh 执行的过程中怎么知道到 scopetable[1] 就应该停止?
MSC 是通过 scopetable_entry::previousTryLevel 来解决这个问题的。上面数组的设置,完整的形式其实是这样:
scopetable[0].previousTryLevel = TRYLEVEL_NONE;
scopetable[0].lpfnFilter = ExceptionFilter_0;
scopetable[0].lpfnHandler = ExceptCodeBlock_0;
scopetable[1].previousTryLevel = TRYLEVEL_NONE;
scopetable[1].lpfnFilter = ExceptionFilter_1;
scopetable[1].lpfnHandler = ExceptCodeBlock_1;
scopetable[2].previousTryLevel = 1;
scopetable[2].lpfnFilter = ExceptionFilter_2;
scopetable[2].lpfnHandler = ExceptCodeBlock_2;
scopetable_entry::previousTryLevel 包含的意思是“下一个该轮到数组下标为 previousTryLevel 的单元了”。当 scopetable_entry::previousTryLevel 等于 TRYLEVEL_NONE(-1) 时,就会停止遍历 scopetable。
咱再来模拟执行一遍,当14和15行之间触发异常时,首先遍历到 scopetable[2],处理完后,找到 scopetable[2].previousTryLevel,发现其值为1,那么遍历到 scopetable[1],处理完后,找到 scopetable[1].previousTryLevel,发现其值为 TRYLEVEL_NONE,于是停止遍历。
好像挺圆满的,是吧。
咱们再假设下,如果行4和行5之间触发了同样的异常,执行流程应该如何。首先,执行 scopetable[2],然后在 scopetable[1],然后……(省略若干同上字)。停!这次的异常是在第一个 __try/__except 中触发的,轮不到 scopetable[2] 来处理,怎么办?
这个时候就轮到 EXCEPTION_REGISTRATION::trylevel 出场了~。EXCEPTION_REGISTRATION::trylevel 的作用就是标识从那个数组单元开始遍历。
与 scopetable_entry::previousTryLevel 不同,EXCEPTION_REGISTRATION::trylevel 是动态变化的,也就是说,这个值在 SimpleSeh 执行过程中是会经常改变的。比如,
执行到行4和行5之间,该值就会被修改为0;
执行到第12行,该值被修改为1;
执行到14行,该值为2。
这样,当异常触发时候,MSC 就能正确的遍历 scopetable 了。
这里我画了一幅草图来帮助理解:
图中下方是低地址端,上方是高地址端。
(boxcounter: 这幅图是我借助 vim 的列操作手绘的,哪位朋友知道有专门画这类文本图的工具吗(除了 emacs 的图操作模式,这玩意太臃肿了,我不太喜欢)?欢迎告知我 ns.boxcounter[a]gmail.com。非常感谢。)
4G |--------------------------| ...
| ... | |
--> |--------------------------| |
/ | ret_addr | |
func1 | _EXCEPTION_REGISTRATION | _EXCEPTION_REGISTRATION / previousTryLevel = TRYLEVEL_NONE \
\ | ... | | -> scopetable[0] | lpfnFilter = ExceptionFilter_0 |
--> |--------------------------| | / \ lpfnHandler = ExceptionCodeBlock_0 /
/ | ret_addr | | / / previousTryLevel = TRYLEVEL_NONE \ <-
func2 | _EXCEPTION_REGISTRATION | _EXCEPTION_REGISTRATION scopetable[1] | lpfnFilter = ExceptionFilter_1 | |
\ | ... | | \ \ lpfnHandler = ExceptionCodeBlock_1 / |
--> |--------------------------| | \ / previousTryLevel = 1 \ -^
/ | ret_addr | | -> scopetable[2] | lpfnFilter = ExceptionFilter_2 |
func3 | _EXCEPTION_REGISTRATION | _EXCEPTION_REGISTRATION \ lpfnHandler = ExceptionCodeBlock_2 /
\ | ... | |
--> |--------------------------| |
/ | ret_addr | |
func4 | _EXCEPTION_REGISTRATION | _EXCEPTION_REGISTRATION
\ | ... | ^
--> | | |
| | FS:[0]
0 -> |--------------------------|
这幅图中的函数关系是: func1 -> func2 -> func3 -> func4
到目前位置,咱们已经熟悉了增强版的概要流程。下面结合真实代码来分析。代码分为三块:SEH 创建代码、MSC 提供的 handler 函数,以及展开函数。
在开始看分析代码之前,先把后面分析过程中需要用的宏和结构体列出来:
[font=Consolas][color=#000000] [/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_NONCONTINUABLE [/color][color=#800080]0x1 [/color][color=#008000]// Noncontinuable exception
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_UNWINDING [/color][color=#800080]0x2 [/color][color=#008000]// Unwind is in progress
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_EXIT_UNWIND [/color][color=#800080]0x4 [/color][color=#008000]// Exit unwind is in progress
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_STACK_INVALID [/color][color=#800080]0x8 [/color][color=#008000]// Stack out of limits or unaligned
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_NESTED_CALL [/color][color=#800080]0x10 [/color][color=#008000]// Nested exception handler call
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_TARGET_UNWIND [/color][color=#800080]0x20 [/color][color=#008000]// Target unwind in progress
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_COLLIDED_UNWIND [/color][color=#800080]0x40 [/color][color=#008000]// Collided exception handler call
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_UNWIND [/color][color=#000080]([/color][color=#000000]EXCEPTION_UNWINDING [/color][color=#000080]| [/color][color=#000000]EXCEPTION_EXIT_UNWIND [/color][color=#000080]| [/color][color=#000000]\
EXCEPTION_TARGET_UNWIND [/color][color=#000080]| [/color][color=#000000]EXCEPTION_COLLIDED_UNWIND[/color][color=#000080])
[/color][color=#000000]nt[/color][color=#000080]![/color][color=#000000]_EXCEPTION_RECORD
[/color][color=#000080]+[/color][color=#800080]0x000 [/color][color=#000000]ExceptionCode [/color][color=#000080]: [/color][color=#000000]Int4B
[/color][color=#000080]+[/color][color=#800080]0x004 [/color][color=#000000]ExceptionFlags [/color][color=#000080]: [/color][color=#000000]Uint4B
[/color][color=#000080]+[/color][color=#800080]0x008 [/color][color=#000000]ExceptionRecord [/color][color=#000080]: [/color][color=#000000]Ptr32 _EXCEPTION_RECORD
[/color][color=#000080]+[/color][color=#800080]0x00c [/color][color=#000000]ExceptionAddress [/color][color=#000080]: [/color][color=#000000]Ptr32 Void
[/color][color=#000080]+[/color][color=#800080]0x010 [/color][color=#000000]NumberParameters [/color][color=#000080]: [/color][color=#000000]Uint4B
[/color][color=#000080]+[/color][color=#800080]0x014 [/color][color=#000000]ExceptionInformation [/color][color=#000080]: [[/color][color=#800080]15[/color][color=#000080]] [/color][color=#000000]Uint4B
[/color][color=#0000FF]typedef enum [/color][color=#000000]_EXCEPTION_DISPOSITION [/color][color=#000080]{
[/color][color=#000000]ExceptionContinueExecution[/color][color=#000080],
[/color][color=#000000]ExceptionContinueSearch[/color][color=#000080],
[/color][color=#000000]ExceptionNestedException[/color][color=#000080],
[/color][color=#000000]ExceptionCollidedUnwind
[/color][color=#000080]} [/color][color=#000000]EXCEPTION_DISPOSITION[/color][color=#000080];
[/color][color=#008000]// scopetable_entry::lpfnFilter 的返回值,也就是 __except 过滤块的返回值
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_EXECUTE_HANDLER [/color][color=#800080]1
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_CONTINUE_SEARCH [/color][color=#800080]0
[/color][color=#0000FF]#define [/color][color=#000000]EXCEPTION_CONTINUE_EXECUTION [/color][color=#000080]-[/color][color=#800080]1
[/color][/font]
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课