avatar

Catalog
_try_except本质

前言

前一篇学习了,Windows平台的编译器了扩展了SEH,通过try_except块简化了挂入SEH的过程。本篇就来研究一下编译器是如何通过try_except将异常挂入SEH链表的。本篇依旧在Windows XP系统下的Visual C++6.0编译器上进行实验。

_try_except实现细节

手动挂入

首先我们来回顾一下手动挂入链表的过程:

由图,手动挂入链表需要自身SEH结构的地址赋值给FS:[0]的位置,这样相当于把自己挂到链表首位。当然,还需要将自己的next指针指向原先FS:[0]处所指向的地址,这里没有显示出。

自动挂入

以如下代码为例:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stdafx.h"
#include

void TestException(){
_try{

}
_except(1){

}
}

int main(int argc, char* argv[])
{
TestException(); //下断点

getchar();
return 0;
}

在TestException处下断点,观察反汇编。

可以看到,_try_except实际上的操作过程与手动挂入链表类似,同样是让FS:[0]指向新的链表头。接下来,我们来看另一种场景。

_try_except嵌套重复

修改TestException如下:

c
1
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
void TestException(){
_try
{
_try
{

}
_except(1)
{

}
}
_except(1)
{

}

_try
{

}
_except(1)
{

}
}

编译,下断点,观察反汇编

会神奇的发现,在try_except嵌套与重复的情况下(递归不包括,因为递归属于重复调用函数,会挂入多个SEH),经过编译器汇编后,仍然只有一个异常处理函数_except_handler3(该函数不同编译器不一样,此实验环境为VC6.0),并且只挂入了一次SEH。这是什么原因呢?

原来编译器扩展了SEH结构体,在原先Windows要求下,SEH结构体至少要包含2个字段(Next:指向下一个SEH块,Handler:异常处理函数),其堆栈结构如下所示:

但是经过扩展后的SEH结构体原型如下(稍后会分析该结构体):

c
1
2
3
4
5
6
7
struct _EXCEPTION_REGISTRATION{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};

这样一来,堆栈的结构也就发生了变化,也就可以对应上之前分析的汇编代码。

接下来,研究该结构体的额外字段的作用,便可了解编译器是如何通过只挂一个SEH实现所有嵌套重复try_except块的功能

扩展的_EXCEPTION_REGISTRATION结构体

再来看_EXCEPTION_REGISTRATION这个结构:

c
1
2
3
4
5
6
7
struct _EXCEPTION_REGISTRATION{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};

它多出了3个成员,其中最为重要的是scopetable和trylevel这两个字段,先来看scopetable。

ScopeTable

scopetable是一个指针,它指向一个结构体数组,结构体如下:

c
1
2
3
4
5
6
struct scopetable_entry
{
DWORD previousTryLevel //上一个try{}结构编号
PDWRD lpfnFilter //过滤函数的起始地址
PDWRD lpfnHandler //异常处理程序的地址
}

这三个成员的含义如何理解呢?根据注释,知道它是两个指针以及1个编号。下面用一个程序理清他们的作用。

编译运行如下代码(环境VC++6.0),并在a函数调用处设下断点。

c
1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "stdafx.h"
#include

int ExceptionFilter()
{
return EXCEPTION_CONTINUE_EXECUTION;
}

void a()
{
_try
{
//异常点A
}
_except(EXCEPTION_EXECUTE_HANDLER)
{
printf("异常处理函数A\n");
}

_try
{
//异常点B
_try
{
//异常点C
}
_except(GetExceptionCode() == 0xC0000094 ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("异常处理函数C \n");
}
}
_except(ExceptionFilter())
{
printf("异常处理函数B\n");
}

}

int main()
{
a();
}
  1. 进入反汇编,根据_EXCEPTION_REGISTRATION结构的位置,找到scopetable的值,并在内存中定位到scopetable指向的结构体数组地址,如下图所示:

  2. 将数组中各结构体的成员标出后如下图:

    这样来看,lpfnFilter与lpfnHandler的作用就清晰了很多:

    • lpfnFilter:指向except括号内的内容,在前一篇文章中提到过滤表达式,也就是except括号内常量值,这里编译器将其优化成了一段可以返回的代码(注意ret指令),所以当异常发生时,代码已经不是顺序执行的了,而是会经过多次跳转和返回。
    • lpfnHandler:指向异常处理函数,这就相当于默认SEH结构的Handler的值。

    综上,可以看出,之所以经过编译器扩展后仅有一个SEH块,原因是编译器通过对SEH块进行扩展,将每一个try_except块对应的过滤表达式与异常处理函数放到了scopetable指向的结构体数组中。这样就能在一个SEH块中容纳下多个try_except。

  3. 前面提到了scopetable中的两个指针成员lpfnFilter与lpfnHandler,还有一个成员previousTryLevel还未提。这个成员有什么用呢?再回顾一下,这3个try_except块对应的previousTryLevel的值

    结合之前的一张图,可以看出两个值为-1的previousTryLevel对应两个外层的try_except块,值为1的previousTryLevel则对应内嵌在第二个try_except中的try_except块。这样就能理解了,previousTryLevel指的是当前try_except块所在的外层try_except块的下标是多少。例如前两个try_except块,它们的外层已经没有try_except块了,因此值为-1。内嵌的try_except块,位于第二个try_except块中,这里说的第二个的意思就是在scopetable指向的结构体数组中位于第二个,也就是下标为1。因此内嵌的try_except的previousTryLevel的值为1

TryLevel

理解了scopetable及其指向的结构体内的字段后,trylevel也就好理解了。它指的是当前代码执行到哪个try_except块了。如何理解呢?来看下面代码:

c
1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "stdafx.h"
#include

int ExceptionFilter()
{
return 1;
}

void a()
{
_try
{
_try
{

}
_except(EXCEPTION_EXECUTE_HANDLER)
{
printf("异常处理函数\n");
}
}
_except(EXCEPTION_EXECUTE_HANDLER)
{
printf("异常处理函数\n");
}


_try
{
_try
{

}
_except(GetExceptionCode() == 0xC0000094 ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("异常处理函数\n");
}
}
_except(ExceptionFilter())
{
printf("异常处理函数\n");
}

}


int main()
{
a();
}

同样在调用函数a处下断点,并进入反汇编。

可以看到,在初始化SEH结构体时,trylevel的值被设置为-1,位于[ebp-4]的位置。

观察这几处trylevel值的变化,它在进入第一个try_except块时,被设置为0(该try_except块对应的结构体位于scopetable[0]),在进入第二个try_except块时,又被设置成了1,一旦离开第二个try_except块,回到第一个try_except块所在空间时,又被设置成了0,等到了离开第一个try_except块时,被设置成了-1。

同样,在执行到另外几个try_except块时,trylevel也会被设置成该try_except块位于scopetable指向的结构体数组中的下标。所以可以看出,trylevel的作用,就是用来表明,当前程序位于哪个try_except块中,若不位于try_except块中,则设置为-1。

ebp

ebp就是ebp,可以认为是寻址用的,例如[ebp-4]就是trylevel的值。

_except_handler3执行过程

至此,已经基本上了解一个异常处理的完整流程,这里就简单的回顾一遍这个过程。这里以用户触发除零异常为例

  1. CPU检测到异常
  2. 查询IDT表,执行中断处理函数
  3. CommonDispatchException
  4. KiDispatchException
  5. KiDebugRoutine(判断内核调试器)
  6. DbgkForwardException(判断用户调试器)
  7. KiUserExceptionDispatcher
  8. RtlDispatchException(3环)
  9. VEH
  10. SEH
  11. 执行_except_handler3函数
    • 根据trylevel选择scopetable数组中的结构体
    • 调用scopetable数组中对应结构体的lpfnFilter
      • EXCEPTION_EXECUTE_HANDLER(1) 执行except代码
      • EXCEPTION_CONTINUE_SEARCH(0) 寻找下一个
      • EXCEPTION_CONTINUE_EXECUTION(-1) 重新执行
    • 如果lpfnFilter函数返回0,则向上遍历,直到previousTryLevel为-1

参考资料

参考书籍:

  • 《软件调试 卷2:Windows平台调试》p249~p259 —— 张银奎

参考教程:

  • 海哥逆向中级班预习班

参考链接:

  1. https://blog.csdn.net/qq_38474570/article/details/104346489 (鬼手56-编译器扩展SEH学习笔记)
  2. https://blog.csdn.net/weixin_42052102/article/details/83551306 (My classmates-编译器扩展SEH学习笔记)
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/08/25/tryexcept%E6%9C%AC%E8%B4%A8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶