文章目录
编译器扩展的SEH
为什么需要编译器扩展的SEH
之前我们已经了解过SEH的异常处理流程,SEH的处理流程实际上就是在当前的堆栈中挂一个链表。
这种处理方式有一个比较麻烦的地方,如果我们想使用SEH这种方式来处理异常,那就意味着我们必须要做如下的几件事:
- 创建一个结构体,并把它挂到当前TEB的ExceptionList链表里
- 编写异常处理函数
这些操作相对来说比较复杂,而编译器对这一异常处理机制进行了拓展。
编译器支持的SEH
_try 1) 挂入链表
{
}
_except(过滤表达式) 2) 异常过滤
{
异常处理程序 3) 异常处理程序
}
我们只需要将可能出现异常的代码放到try块里,编译器就会替我们把异常的处理程序挂到Exception链表。
过滤表达式
except里的过滤表达式用于异常过滤,只能有以下三个值:
- EXCEPTION_EXECUTE_HANDLER(1) 执行except代码
- EXCEPTION_CONTINUE_SEARCH(0) 寻找下一个异常处理函数
- EXCEPTION_CONTINUE_EXECUTION(-1) 返回出错位置重新执行
过滤表达式只能有三种写法:
- 直接写常量值
- 表达式
- 调用函数
表达式写法
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,10
idiv ecx //EDX = EAX /ECX
}
}
//如果异常码为0xC0000094返回1否则返回0
_except(GetExceptionCode() == 0xC0000094 ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("如果出现异常 此处处理\n");
}
调用函数写法:
//参数根据需要来写,可以不要参数
int ExceptFilter(LPEXCEPTION_POINTERS pExceptionInfo)
{
pExceptionInfo->ContextRecord->Ecx = 1;//异常处理
return EXCEPTION_CONTINUE_EXECUTION;//返回出错位置重新执行
}
int main()
{
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,10
idiv ecx //EDX = EAX /ECX
}
}//GetExceptionInformation获取异常结构指针
_except(ExceptFilter(GetExceptionInformation()))
{
printf("如果出现异常 此处处理\n");
}
getchar();
}
try_except的实现细节
下面我们来了解一下编译器是如何通过try_except将异常处理函数挂入链表的
手动挂入链表
_asm
{
mov eax,FS:[0]
mov temp,eax
lea ecx,myException
mov FS:[0],ecx
}
自动挂入链表
示例代码如下:
void TestException()
{
_try
{
}
_except(1)
{
}
}
将程序载入OD,分析编译器生成的代码。
编译器会自动帮我们修改FS:[0],将异常处理函数挂入SEH链表
_try_except嵌套重复
每个使用_try_except
的函数,不管其内部嵌套或反复使用多少次,_try_except
都只注册一遍,即只将一个EXCEPTION_REGISTRATION_RECORD
挂入当前线程的SEH链表中
对于递归函数,每一次调用都会创建一个 _EXCEPTION_REGISTRATION_RECORD,并挂入线程的异常链表中
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
示例代码如下:
_try
{
_try
{
}
_except(1)
{
}
}
_except(1)
{
}
_try
{
}
_except(1)
{
}
只要挂一次SEH链就会修改一次 FS[0]
push ebp
mov ebp,esp
push 0FFh
push offset string "i386\\chkesp.c"+0FFFFFFD4h (00422020)
push offset __except_handler3 (00401224)
mov eax,fs:[00000000]
push eax
mov dword ptr fs:[0],esp
add esp,0B8h
push ebx
push esi
push edi
mov dword ptr [ebp-18h],esp
lea edi,[ebp-58h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
_try
mov dword ptr [ebp-4],0
{
_try
mov dword ptr [ebp-4],1
{}
mov dword ptr [ebp-4],0
jmp $L16979+0Ah (0040107c)
_except(1)
mov eax,1
ret
mov esp,dword ptr [ebp-18h]
{}
mov dword ptr [ebp-4],0
}
mov dword ptr [ebp-4],0FFFFFFFFh
jmp $L16975+0Ah (00401095)
_except(1)
mov eax,1
ret
mov esp,dword ptr [ebp-18h]
{}
mov dword ptr [ebp-4],0FFFFFFFFh
_try
mov dword ptr [ebp-4],2
{}
mov dword ptr [ebp-4],0FFFFFFFFh
jmp $L16983+0Ah (004010b5)
_except(1)
mov eax,1
ret
mov esp,dword ptr [ebp-18h]
{}
mov dword ptr [ebp-4],0FFFFFFFFh
mov ecx,dword ptr [ebp-10h]
mov dword ptr fs:[0],ecx
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
不管其内部嵌套或反复使用多少try_except,都只挂一次,指向的异常处理函数也是确定的。3个异常处理函数就应该有3个链,它只挂了一个,这时怎么实现的呢?
拓展的_EXCEPTION_REGISTRATION_RECORD结构体
struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};
第一个成员和第二个成员和原来的EXCEPTION_REGISTRATION_RECORD
含义一样,后面多了三个成员。堆栈中的SEH链也发生了变化
堆栈中的结构对应汇编代码如图:
接下来分析学习的三个成员的含义,第三个成员比较好理解,就是当前堆栈的ebp,剩下的两个成员才是关键
struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};
scopetable成员分析
这个成员是一个结构体指针,结构体包含以下三个成员
struct scopetable_entry
{
DWORD previousTryLevel //上一个try{}结构编号
PDWRD lpfnFilter //过滤函数的起始地址
PDWRD lpfnHandler //异常处理程序的地址
}
try_finally
之前的课程我们讲解了__try __except
的的执行细节,但编译器还提供了另外一组程序块
__try
{
//可能出错的代码
}
__finally
{
//一定要执行的代码
}