操作系统或程序在运行,难免会遇到各种各样的错误,如除零,非法内存访问,文件打开错误,内存不足,磁盘读写错误,外设操作失败。为了保证系统在遇到错误时不至于崩溃,仍能够健壮稳定地继续运行下去,windows会对运行在其中的程序提供一次补救的机会来处理错误这种机制就是异常处理机制。 S.E.H即异常处理结构体(Structure Exception Handler),它是windows异常处理机制所采用的重要数据结构,每个S.E.H包含两个DWORD指针:S.E.H链表指针和异常处理函数句柄,共8个字节,如下图
下面分别对用户模式下的原始型SEH和封装型SEH 进行讨论。
原始型SEH
SEH 的进程相关类型是整个进程作用范围的异常处理函数,通过WIN32 API 函数SetUnhandledExceptionFilter 进行注册,而操作系统内部使用一个全局变量来记录这个顶层的处理函数,因此只能有一个全局性的异常处理函数。而线程相关类型的作用范围是本线程内,并且可注册多个,甚至可以嵌套注册。两者相比线程相关类型在实际应用中使用较为广泛,因此此处重点对此类型进行研究。
当线程初始化时,会自动向栈中安装一个异常处理结构,作为线程默认的异常处理。SEH 最基本的数据结构是保存在堆栈中的称为EXCEPTION_REGISTRATION 的结构体,结构体包括2个元素:第1个元素是指向下一个EXCEPTION_REGISTRATION 结构的指针(prev),第2个元素是指向异常处理程序的指针(handler)。这样一来,基于堆栈的异常处理程序就相互连接成一个链表。异常处理结构在堆栈中的典型分布如图1 所示。最顶端的异常处理结构通过线程控制块(TEB)0 Byte 偏移处指针标识,即FS:[0]处地址。
用于进行实际异常处理的函数原型可表示如下:
EXCEPTION_DISPOSITION __cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
)
该函数的最重要的2个参数是指向_EXCEPTION_RECORD 结构的ExceptionRecord 参数和指向_CONTEXT 结构的ContextRecord 参数,前者主要包括异常类别编码、异常发生地址等重要信息;后者主要包括异常发生时的通用寄存器、调试寄存器和指令寄存器的值等重要的线程执行环境。而用于注册异常处理函数的典型汇编代码可表示如下:
PUSH handler ; handler 是新的异常处理函s数地址
PUSH FS:[0] ;指向原来的处理函数的地址压入栈内
MOV FS:[0],ESP ;注册新的异常处理结构
当异常发生时,操作系统的异常分发函数在进行初始处理后,如果异常没有被处理就会开始在上图所示的线程堆栈上遍历异常处理链,直到异常被处理,如果仍没有注册函数处理异常,则将异常交给缺省处理函数或直接结束产生异常的进程。
封装型SEH
通过使用_try{}/_except(){}/_finally{}等关键字,使开发人员更方便地在软件中使用SEH 是封装型SEH 的主要特点。该机制的异常处理数据结构定义如下:
struct VC_EXCEPTION_REGISTRATION
{
VC_EXCEPTION_REGISTRATION* prev;
FARPROC handler;
scopetable_entry* scopetable; //指向scopetable 数组指针
int _index; //在scopetable_entry 中索引
DWORD _ebp; //当前EBP 值
}
显而易见,结构体中后3 个成员是新增的。而scopetable_entry 的结构如下所示:
struct scopetable_entry
{
DWORD prev_entryindex; //前一scopetable_entry 的索引
FARPROC lpfnFilter; //过滤函数地址
FARPROC lpfnHandler; //处理异常代码地址
}
封装型SEH 的基本思想是为每个函数内的_try{}块建立一scopetable表,每个_try{}块对应于scopetable中的一项,该项指向_try{}块对应的scopetable_entry 结构,该结构含有与_except(){}/_finally{}对应的过滤函数和处理函数。若有_try{}块嵌套,则在scopetable_entry 结构的prev_entryindex成员中指明,多层嵌套形成单向链表。而每个函数只注册一个VC_EXCEPTION_REGISTRATION 结构, 该结构中的handler 成员是一个重要的运行时库函数_except_handler3。该异常处理回调函数负责对结构中的成员进行设置,查找处理函数并根据处理结果决定是继续执行还是让系统继续遍历外层SEH 链。为了弄清看似复杂的封装型SEH 原理,此处通过分析一个简单的使用封装型SEH 的函数的反汇编实现,从而深入地了解封装型SEH 的实现过程。该函数的C 语言实现如下:
void A()
{
__try // 0 号try 块
{
__try // 1 号try 块
{
*(PDWORD)0 = 0;
}
__except(EXCEPTION_CONTINUE_SEARCH)
{
printf("Exception Handler!");
}
}
__finally
{
puts("in finally");
}
}
对应该函数的序言部分反汇编代码如下:
push ebp
mov ebp, esp
push -1
push offset _A_scopetable
push offset _except_handler3
mov eax, large fs:0
push eax
mov large fs:0, esp
显而易见, 压入堆栈的结构与VC_EXCEPTION_REGISTRATION 结构是一致的。查找scopetable 的地址为0x00422048,在调试器中查找该地址起始的内容如下:
FFFFFFFF ;scopetable_entry0 的prev_entryindex 值
00000000 ;lpfnFilter 地址值,为0,对应_finally{}
004010EE ;lpfnHandler 地址值
00000000 ;scopetable_entry1 的prev_entryindex 值
004010C6 ;lpfnFilter 地址值
004010C9 ;lpfnHandler 地址值
…
第1 组值对应0 号try 块,而该块对应_finally{}块,所以过滤函数地址为0;第2 组值对应1 号try 块,而该块对应_except(){}块,所以有过滤函数和处理函数的地址。进一步查看0x004010EE 地址处的反汇编代码如下:
PUSH OFFSET ??_C@0L@PEFD@in?5finally?$AA@; ”in finally”
CALL puts
ADD ESP,4
RETN
显然上述语句与_finally{}块中的C 语言语句是对应的,而其他的地址经过查找也是分别对应的。在进入0 号try 块时的反汇编语句如下:
MOV DWORD PTR SS:[EBP-4],0 ;对应第1 个_try 语句
MOV DWORD PTR SS:[EBP-4],1 ;对应第2 个_try 语句
而在退出_try 块时对应的反汇编语句如下:
…
MOV DWORD PTR SS:[EBP-4],0 ;退出第2 个_try 块
MOV DWORD PTR SS:[EBP-4],-1 ;退出第1 个_try 块
…
根据异常处理的堆栈结构可知, [EBP-4] 处值就是VC_EXCEPTION_REGISTRATION 结构中_index 的值。异常处理机制在进入和退出每个_try 块前设置相应的_index 值,这样就可正确处理封装型SEH 内发生的各种异常。以上通过实例进一步验证和明确了封装型SHE 的内部机理,它只是扩展了原始型SEH 的功能,简化了软件开发人员的工作。
利用
未完。。。