1.起因
C++程序员对try,catch,throw都应该很熟悉,能知道VC怎么实现它的人就不多了,不过网络世界使很多人知道了它与SEH (structured exception handling)有密切关系,我也不例外,也是在若干年前从网络知道了SEH,并且大致也知道 SEH的流程.但是和多数人一样在我的实践也很少直接使用SEH,对SEH也就仅限于网络上一些文章的介绍.曾经在用Windbg对某些软件作分析,我遇到了断点失效的情况,查找资料介绍是SEH中的Handler清除了调试寄存器,在分析SEH代码中由于VC没有SEH的源码,于是我产生了一种想法,详细完整地翻译VC的SEH的代码,一劳永逸的解决问题.C和C++的SEH有所不同,C++的要复杂些,我在此介绍的仅为C的SEH代码,也就是 __try,__finally,__except,__leave所产生的SEH代码,C++篇有时间的话我再作.我以前看过的资料大都以比较专业的语言介绍,在此我仅以我自己感觉比较通俗的语言介绍,希望能有更多的人能认识,认清SEH.
2.SEH术语:SEH中术语虽然不多,但由于没有SDK的明确定义有时很难区别,各家说的表述也不太统一,因此为此文特定义如下术语,这些术语可能与其它文献有点冲突或细微差别,有的也是我自己的定义:
A.SEH(structured exception handling): 在C语言中关键字是__try(_try),__finally (_finally),_except(__except),而C++使用的关键字是try,catch.在以下的表述中所有的try均不再特别声明为C 关键字,一律默认为C关键字.
B. EH3_List:_EH3_EXCEPTION_REGISTRATION链表,表头位于FS:[0],0xFFFFFFFF为链表结束标志.编译器在编译一个函数时只要检测到含有_try或__try则为此函数生成一个_EH3_EXCEPTION_REGISTRATION节点,并插入到表头.因为每个函数编译只生成一个节点,因此在一个函数中C和C++的SEH不能同时存在,如果代码中同时有catch和except则不能通过编译就是此原因.
C. EH3_ScopeTable:是一个由编译器在data section生成的一张表(数组),实质可看作是二叉树结构(可能有多个二叉树顺序存放),节点为_SCOPETABLE_ENTRY类型,其中_SCOPETABLE_ENTRY.ParentLevel是父节点在数组中的位置, EH3_ScopeTable[0]是根节点,_SCOPETABLE_ENTRY.ParentLevel=0xFFFFFFFF.由此可见 ParentLevel很重要,是SEH判断try嵌套层次的唯一依据.编译器从函数入口点开始遍历try,每遇到一个try生成一个节点 _SCOPETABLE_ENTRY,并放在表最后,注意节点的先后与try的嵌套层次无关.
D. filter handler:是异常发生后让用户决定是否认识此异常,通过修改异常语句的上下文环境(CONTEXT)可使应用程序能继续正常运行.其返回值有三.
EXCEPTION_EXECUTE_HANDLER(1): 去执行exception handler,然后进程终止,且不显示出错提示框.
EXCEPTION_CONTINUE_SEARCH(0): 不执行exception handler,显示出错提示框,进程终止或者进入调试器进行调试.
EXCEPTION_CONTINUE_EXECUTION(-1): 不执行exception handler,系统用CONTEXT重新设置CPU环境,进程继续执行,如果修改了EIP则从新的EIP开始执行,否则从原异常点开始执行.
E. exception handler:是异常发生后检测到该异常无法被处理,而进程终止前提醒应用程序执行的收尾工作
F. termination handler:如果try语句被过早终止,不管是正常离开或者是非正常离开,包括goto,leave及异常时均执行此handler,具体执行过程可查MSDN.
G. 展开(unwind):这个名词很让人费解,翻译也确实不好命名,我也沿用此名.展开的目的是执行finally对应的 termination handler,对照在下面的源代码中我们就能很容易理解MSDN关于finally的解释了.展开分为本地局部展开 (_local_unwind)和全局展开(_global_unwind);本地展开:展开此try所在函数try嵌套关系并分别执行其finally 对应的termination handler;全局展开:当exception handler不在本try函数时,执行 exception handler前需要先执行这之前的termination handler,全局展开就是查找这些 termination handler并执行它.需要说明的是全局展开不含本地展开.
3.SEH数据结构
typedef struct // (sizeof=0xC)
{
DWORD ParentLevel ; // 当前Handler父层TRY在EH3_ScopeTable中的位置,根没有上一层,故值=-1
// 形成TRY层次的二叉树结构,与_EH3_EXCEPTION_REGISTRATION.TryLevel物理意义一样
DWORD FilterFunc; // 非NULL则HandlerFunc是exception handler,否则termination handler
DWORD HandlerFunc; // exception handler or termination handler
} _SCOPETABLE_ENTRY;
typedef struct // (sizeof=0x10)
{
_EH3_EXCEPTION_REGISTRATION* pPrev;// 栈上一级EH3_List节点,=0xFFFFFFFF则为最后一个节点
EXCE_HANDLER ExceptionHandler; // VC7.1中统一指向_except_handler3
_SCOPETABLE_ENTRY* pScopeTable; // 指向一个_SCOPETABLE_ENTRY数组,函数有n个TRY,则数组有n个元素
// p[0]->Try0为根,p[1]->Try1,p[2]->Try2...
DWORD TryLevel; // 指示当前指令在Try的层次级别,-1未进入TRY,进第一个TRY为0,第二个为1,...
// 但它与嵌套层次无关,在编译时确定,从函数代码开始处开始计数
} _EH3_EXCEPTION_REGISTRATION;
typedef struct // (sizeof=0x10)还有待进一步分析其用处
{
DWORD unKnown; // 未知:被编译器赋值
DWORD HandlerFunc; // _SCOPETABLE_ENTRY.HandlerFunc
DWORD firstPara; // Try所在函数第一个参数:crtMain!EBP+8
DWORD TryEBP; // Try所在函数EBP
}_NLGDestination;
// 以下在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,*PEXCEPTION_RECORD;
typedef struct _CONTEXT
{
... // 依据CPU类型有不同定义,具体可见winnt.h
} CONTEXT,*PCONTEXT;
typedef struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS,*PEXCEPTION_POINTERS;
4.SEH汇编要点
A. 函数中try的数据模型:VC将会为有
C++程序员对try,catch,throw都应该很熟悉,能知道VC怎么实现它的人就不多了,不过网络世界使很多人知道了它与SEH (structured exception handling)有密切关系,我也不例外,也是在若干年前从网络知道了SEH,并且大致也知道 SEH的流程.但是和多数人一样在我的实践也很少直接使用SEH,对SEH也就仅限于网络上一些文章的介绍.曾经在用Windbg对某些软件作分析,我遇到了断点失效的情况,查找资料介绍是SEH中的Handler清除了调试寄存器,在分析SEH代码中由于VC没有SEH的源码,于是我产生了一种想法,详细完整地翻译VC的SEH的代码,一劳永逸的解决问题.C和C++的SEH有所不同,C++的要复杂些,我在此介绍的仅为C的SEH代码,也就是 __try,__finally,__except,__leave所产生的SEH代码,C++篇有时间的话我再作.我以前看过的资料大都以比较专业的语言介绍,在此我仅以我自己感觉比较通俗的语言介绍,希望能有更多的人能认识,认清SEH.
2.SEH术语:SEH中术语虽然不多,但由于没有SDK的明确定义有时很难区别,各家说的表述也不太统一,因此为此文特定义如下术语,这些术语可能与其它文献有点冲突或细微差别,有的也是我自己的定义:
A.SEH(structured exception handling): 在C语言中关键字是__try(_try),__finally (_finally),_except(__except),而C++使用的关键字是try,catch.在以下的表述中所有的try均不再特别声明为C 关键字,一律默认为C关键字.
B. EH3_List:_EH3_EXCEPTION_REGISTRATION链表,表头位于FS:[0],0xFFFFFFFF为链表结束标志.编译器在编译一个函数时只要检测到含有_try或__try则为此函数生成一个_EH3_EXCEPTION_REGISTRATION节点,并插入到表头.因为每个函数编译只生成一个节点,因此在一个函数中C和C++的SEH不能同时存在,如果代码中同时有catch和except则不能通过编译就是此原因.
C. EH3_ScopeTable:是一个由编译器在data section生成的一张表(数组),实质可看作是二叉树结构(可能有多个二叉树顺序存放),节点为_SCOPETABLE_ENTRY类型,其中_SCOPETABLE_ENTRY.ParentLevel是父节点在数组中的位置, EH3_ScopeTable[0]是根节点,_SCOPETABLE_ENTRY.ParentLevel=0xFFFFFFFF.由此可见 ParentLevel很重要,是SEH判断try嵌套层次的唯一依据.编译器从函数入口点开始遍历try,每遇到一个try生成一个节点 _SCOPETABLE_ENTRY,并放在表最后,注意节点的先后与try的嵌套层次无关.
D. filter handler:是异常发生后让用户决定是否认识此异常,通过修改异常语句的上下文环境(CONTEXT)可使应用程序能继续正常运行.其返回值有三.
EXCEPTION_EXECUTE_HANDLER(1): 去执行exception handler,然后进程终止,且不显示出错提示框.
EXCEPTION_CONTINUE_SEARCH(0): 不执行exception handler,显示出错提示框,进程终止或者进入调试器进行调试.
EXCEPTION_CONTINUE_EXECUTION(-1): 不执行exception handler,系统用CONTEXT重新设置CPU环境,进程继续执行,如果修改了EIP则从新的EIP开始执行,否则从原异常点开始执行.
E. exception handler:是异常发生后检测到该异常无法被处理,而进程终止前提醒应用程序执行的收尾工作
F. termination handler:如果try语句被过早终止,不管是正常离开或者是非正常离开,包括goto,leave及异常时均执行此handler,具体执行过程可查MSDN.
G. 展开(unwind):这个名词很让人费解,翻译也确实不好命名,我也沿用此名.展开的目的是执行finally对应的 termination handler,对照在下面的源代码中我们就能很容易理解MSDN关于finally的解释了.展开分为本地局部展开 (_local_unwind)和全局展开(_global_unwind);本地展开:展开此try所在函数try嵌套关系并分别执行其finally 对应的termination handler;全局展开:当exception handler不在本try函数时,执行 exception handler前需要先执行这之前的termination handler,全局展开就是查找这些 termination handler并执行它.需要说明的是全局展开不含本地展开.
3.SEH数据结构
typedef struct // (sizeof=0xC)
{
DWORD ParentLevel ; // 当前Handler父层TRY在EH3_ScopeTable中的位置,根没有上一层,故值=-1
// 形成TRY层次的二叉树结构,与_EH3_EXCEPTION_REGISTRATION.TryLevel物理意义一样
DWORD FilterFunc; // 非NULL则HandlerFunc是exception handler,否则termination handler
DWORD HandlerFunc; // exception handler or termination handler
} _SCOPETABLE_ENTRY;
typedef struct // (sizeof=0x10)
{
_EH3_EXCEPTION_REGISTRATION* pPrev;// 栈上一级EH3_List节点,=0xFFFFFFFF则为最后一个节点
EXCE_HANDLER ExceptionHandler; // VC7.1中统一指向_except_handler3
_SCOPETABLE_ENTRY* pScopeTable; // 指向一个_SCOPETABLE_ENTRY数组,函数有n个TRY,则数组有n个元素
// p[0]->Try0为根,p[1]->Try1,p[2]->Try2...
DWORD TryLevel; // 指示当前指令在Try的层次级别,-1未进入TRY,进第一个TRY为0,第二个为1,...
// 但它与嵌套层次无关,在编译时确定,从函数代码开始处开始计数
} _EH3_EXCEPTION_REGISTRATION;
typedef struct // (sizeof=0x10)还有待进一步分析其用处
{
DWORD unKnown; // 未知:被编译器赋值
DWORD HandlerFunc; // _SCOPETABLE_ENTRY.HandlerFunc
DWORD firstPara; // Try所在函数第一个参数:crtMain!EBP+8
DWORD TryEBP; // Try所在函数EBP
}_NLGDestination;
// 以下在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,*PEXCEPTION_RECORD;
typedef struct _CONTEXT
{
... // 依据CPU类型有不同定义,具体可见winnt.h
} CONTEXT,*PCONTEXT;
typedef struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS,*PEXCEPTION_POINTERS;
4.SEH汇编要点
A. 函数中try的数据模型:VC将会为有