avatar

Catalog
局部展开与全局展开

前言

前一篇学习了try_except块的实现过程,了解了编译器是如何将多个try_except块的内容集中在一个SEH块上的,以及异常发生时_except_handler3函数是如何找到对应的异常处理函数的。本篇将学习在编译器扩展SEH基础上的另一个格式:try_finally块。

_try_finally

格式

c
1
2
3
4
5
6
_try{
//可能出错的代码
}
_finally{
//一定要执行的代码
}

特点

正如注释所示,try_finally块的特点就是在finally块里的代码一定会得到执行

观察上图,无论使用continue,break这样的控制语句,还是return这样的返回语句,亦或是触发异常,finally块内的语句始终会被执行。正如同前一篇分析讨论try_except的实现细节一样,本篇也将从反汇编的角度,分析try_finally的实现细节。

局部展开

scopetable

以return为例,观察反汇编,看程序是如何在return执行语句的控制下,仍然执行了finally语句块的内容。

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

void test()
{
_try{
return;
printf("其它代码\n");
}
_finally{
printf("一定会执行的代码\n");
}
}


int main()
{
test();
getchar();
return 0;
}

在test()函数处下断点,编译并执行函数(环境Visual C++6.0)

由图,根据scopetable指向的地址,会发现它指向的结构体与try_except时不太一样。因为try_finally没有过滤表达式,因此第二个成员的值是空的。再看第三个成员,指向finally块内程序的地址。这下我们就知道finally块内的语句从哪里可以找到。那么问题来了,编译器是如何保证在return掉程序之前,执行finally块内的语句的呢?

_local_unwind2

继续观察反汇编,注意到,在执行return语句之前,调用了一个名为_local_unwind2的函数。这里需要说明一点,为什么这个过程被称作局部展开,原因就是在于这个函数翻译成中文就是局部展开的意思,没有别的含义。这地方很容易产生误解,局部展开不是一种技术,就是一个函数名而已。

进入_local_unwind2函数,继续分析

注意最后一行,_local_unwind2调用了一个地址,单步到这条指令,根据寄存器内的值,可以很容易的算出,这个地址(看前一张图),刚好就是finally块内语句的地址。也就是说,local_unwind2函数的作用就是执行finally块内的语句。又因为这个local_unwind2会在return执行前被调用(break, continue, 异常同理),因此finally块内的语句一定会被执行。

全局展开

有局部,就会有全局,就像SEH与VEH一样。那什么是全局展开呢?查看下面一种情况:

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
#include "stdafx.h"
#include

void test()
{
_try
{
_try
{

_try
{
*(int*)0 = 10;
}
_finally
{
printf("一定会执行的代码A\n");
}
}
_finally
{
printf("一定会执行的代码B\n");
}
}
_except(1)
{
printf("异常处理函数\n");
}

}

int main()
{
test();
getchar();
return 0;
}

在前面学习了并了解了try_except的本质后,以_except_handler3执行的角度来看这段代码:异常发生在最内层的try块中,此时except_handler3函数会根据当前trylevel的值找到对应的结构体并寻找异常处理函数,这时发现,结构体的第二个成员的值为空,说明这是一个finally块,不会处理异常,因此它将根据当前结构体的previousTryLevel的值,去找中间一层的try块对应的结构体。同样,在中间一层的try块中也没找到异常处理函数,这时它就会去最外层的try块中找,这时,找到了。

按照之前了解的_except_handler3的执行流程,由于过滤表达式的值为1,因此会执行except内的代码,那么一旦执行完except的代码,程序将退出try块,那么问题来了,内层try块的语句,不就得不到执行了吗?

可以看到,finally块内的语句都得到了执行,接下来,跟进汇编,看看编译器的如何做到的。在_except(1)处下断,运行程序:

在执行完过滤表达式的语句后,可以单步到这里。发现,在这里调用了一个global_unwind2(全局展开)函数,并且接下来也调用了2次local_unwind2,这些都发生在执行except块的代码之前。这样就好理解了,当_except_handler3函数发现except内的过滤表达式值为1时,它会先执行global_unwind2,global_unwind2会从触发异常的那个try开始,依次调用局部展开,这样就可以保证finally块语句一定会得到执行。

参考资料

参考书籍:

  • 软件调试 卷2:Windows平台调试》p250~p252 —— 张银奎

参考教程:

  • 海哥逆向中级班预习班

参考链接:

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/08/27/%E5%B1%80%E9%83%A8%E5%B1%95%E5%BC%80%E4%B8%8E%E5%85%A8%E5%B1%80%E5%B1%95%E5%BC%80/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶