前言
前面学习了VEH以及SEH,相较于全局链表的VEH,局部链表SEH有一个特别不方便的一点是,需要手动构造SEH结构体,并手动将其挂入SEH链表中,过程较为繁琐。本篇学习的编译器扩展SEH,就是在SEH的基础上,通过编译器的支持,简化了构造SEH结构体以及将其挂入链表的步骤。
需要注意一点,SEH以及VEH机制都是Windows系统下的异常处理机制,其它操作系统对于异常的处理方式并不一定相同,因此这里谈到的SEH扩展,主要是Windows平台相关编程语言主打的编译器对SEH功能的适配和优化。
编译器对SEH的支持
首先来回顾一下,原先挂一个SEH到链表上的步骤是怎样的。
再来看看编译器扩展SEH后是如何实现的,以Visual C++ 6.0为例(本篇均以此编译器为例)
可以看到,简化了非常多,关键字try形成了一个区块,只需要将可能出现异常的代码放入这个try块里,编译器会替我们将异常处理程序嵌入到SEH结构体中,并将其挂入局部SEH链表内。
过滤表达式
except使用过滤表达式来判断当前异常处理程序是否可以处理该异常。
取值
except里的过滤表达式,只能是以下3个值中的一个:
- EXCEPTION_EXECUTE_HANDLER(1):执行except中的异常处理代码 。
- EXCEPTION_CONTINUE_SEARCH(0):寻找下一个异常处理函数。
- EXCEPTION_CONTINUE_EXECUTION(-1):返回至异常错误处并重新执行。
形式
过滤表达式有3种写法:
直接写常量值:这个比较好理解,就是填过滤表达式取值中的一个。
表达式:
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16_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");
}表达式的写法相较于常量值,就是换了一种形式,它并没有做处理,但是多了逻辑判断。
调用函数写法:
c1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//参数根据需要来写,可以不要参数
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();
}调用函数的方式,和前一篇手动挂入SEH链表处理异常的方式比较类似,过滤表达式的值其实就是异常处理函数的返回值,因此采用调用函数的方式,就相当于编写SEH的handler。那么问题来了,在采用调用函数的写法下,已经有异常处理函数了,那么except内部的处理会执行吗?
这要取决于异常处理函数的返回值了,也就是过滤表达式的取值。以上面的代码为例,由于返回值是EXCEPTION_CONTINUE_EXECUTION,所以异常处理函数执行完后,会回到异常处执行,这时由于修改了Ecx的值,因此再次执行时不会发生异常,也就不会再跳转到except处执行了。
那么问题又来了,既然异常处理函数已经实现了功能,为什么还要有except区块呢?
其实这里的关键在于GetExceptionInformation函数,调用它可以获取到异常结构的指针,从而获取到异常发生时记录的信息以及上下文环境,若没有异常处理函数,那么在except函数内部也可以调用GetExceptionInformation获取到相关的参数,从而对异常进行处理。同样,如果异常处理函数的返回值为EXCEPTION_CONTINUE_HANDLER,则会在异常函数处理完之后,跳转到except块中执行。大部分情况下,使用except块进行处理异常是足够的,它可以让3环程序员打印出必要的信息,起到调试的作用,并不会像异常处理函数那样,修改真正的寄存器的值,使得程序重新执行。也因此,大部分表达式的值,最终都是1,这样就直接进入except执行。
参考资料
参考书籍:
- 《软件调试 卷2:Windows平台调试》p249~p259 —— 张银奎
参考教程:
- 海哥逆向中级班预习班
参考链接:
- https://blog.csdn.net/qq_38474570/article/details/104346489 (鬼手56-编译器扩展SEH学习笔记)
- https://blog.csdn.net/weixin_42052102/article/details/83551306 (My classmates-编译器扩展SEH学习笔记)