July 18, 2022

C++ EXCEPTION LEARNING

关于为了复现Inflated从而看了几篇异常处理文章,由于太菜,笔记几乎是复制回来,加了自己的注释)

第一篇!

https://www.cnblogs.com/catch/p/3604516.html

C++异常处理(1)

异常抛出后,发生了什么事情?

  1. 如果当前函数没有catch,就沿着函数的调用链继续往上抛,然后出现两种情况

    • 在某个函数中找到相应的catch

    • 没找到相应的catch,调用 std::terminate() (这个函数是把程序abort)

  2. 如果想找到了相应的catch,执行相应的操作

    • 程序中catch的代码块有个专有名词:Landing pad
  3. 从抛异常到开始 -> 执行Landing pad代码 这整个过程叫作Stack unwind

Stack unwind

  1. 从抛异常函数开始,对调用链上的函数逐个往前查找Landing pad
  2. 如果没有找到Landing pad则把程序abort,如果找到则记下Landing pad的位置,再重新回到抛异常的函数那里开始,一帧一帧地清理调用链上各个函数内部的局部变量,直到 landing pad 所在的函数为止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void func1()
{
cs a; // stack unwind时被析构。
throw 3;
}

void func2()
{
cs b;
func1();
}

void func3()
{
cs c;
try
{
func2();
}
catch (int)
{
//进入这里之前, func1, func2已经被unwind.
}
}

stack unwind的过程可以简单看成函数调用的逆过程,这个过程在实现上由一个专门的stack unwind库来实现

Itanium C++ ABI

ltanium C++ ABI定义了一系列函数以及数据结构来建立整个异常处理的流程及框架,主要函数包括以下列:

1
2
3
4
5
6
7
8
9
10
_Unwind_RaiseException,
_Unwind_Resume,
_Unwind_DeleteException,
_Unwind_GetGR,
_Unwind_SetGR,
_Unwind_GetIP,
_Unwind_SetIP,
_Unwind_GetRegionStart,
_Unwind_GetLanguageSpecificData,
_Unwind_ForcedUnwind

其中 _Unwind_RaiseException() 函数进行stack unwind,它在用户执行throw的时被调用

主要功能

personality routine

显然可以发现 personality routine 干的事情不就是stack unwind干的事情,所以说stack unwind主要就是通过personality routine来完成,相当于一个callback函数

1
2
3
4
5
6
_Unwind_Reason_Code (*__personality_routine)
(int version,
_Unwind_Action actions,
uint64 exceptionClass,
struct _Unwind_Exception *exceptionObject,
struct _Unwind_Context *context);

Stack unwind的两个阶段

具体到调用链上的函数来说,每个函数在 unwind 过程中都会被 personality routine 遍历两次

以下伪代码展示 _Unwind_RaiseException() 内部的大概实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_Unwind_RaiseException(exception)
{
bool found = false;
// 第一个循环找到相应的catch断 找不到就abort
while (1)
{
// 建立上个函数的上下文
context = build_context();
if (!context) break;
found = personality_routine(exception, context, SEARCH);
if (found or reach the end) break;
}

// 找到了就清理函数内部
while (found)
{
context = build_context();
if (!context) break;
personality_routine(exception, context, UNWIND);
if (reach_catch_function) break;
}
}

ABI 中的函数使用到了两个自定义的数据结构,用来传递一些内部的信息(指的是 personality routine的两个参数

1
2
3
4
5
6
7
8
struct _Unwind_Context;

struct _Unwind_Exception {
uint64 exception_class;
_Unwind_Exception_Cleanup_Fn exception_cleanup;
uint64 private_1;
uint64 private_2;
};

_Unwind_Context

(接口中没找到定义,这是gcc的源码里的定义)

1
2
3
4
5
6
7
8
9
struct _Unwind_Context
{
void *reg[DWARF_FRAME_REGISTERS+1];
void *cfa;
void *ra;
void *lsda;
struct dwarf_eh_bases bases;
_Unwind_Word args_size;
};

_Unwind_Exception

unwind库里用于表示一个异常

C++ ABI

基于前面介绍的 ltanium ABI,编译器层面也定义了一系列 ABI 与之交互

当我们在代码中写下 throw xxx,编译器会分配一个数据结构 __cxa_exception 来表示该异常,该异常也有一个头部,定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct __cxa_exception 
{
std::type_info * exceptionType;
void (*exceptionDestructor) (void *);
unexpected_handler unexpectedHandler;
terminate_handler terminateHandler;
__cxa_exception * nextException;

int handlerCount;
int handlerSwitchValue;
const char * actionRecord;
const char * languageSpecificData;
void * catchTemp;
void * adjustedPtr;

_Unwind_Exception unwindHeader;
};

注意最后一个变量!**_Unwind_Exception unwindHeader**,这个变量就是前面 ltanium 接口里提到的接口内部用的结构体

当用户 throw 一个异常时,编译器会帮我们调用相应的函数分配出如下的结构

image-20220717215150054

其中 _cxa_exception 就是头部,exception_obj 则是 “throw xxx” 中的 xxx,这两部分在内存中是连续的。

当我们在程序里执行了抛出异常的操作,编译器为我们做了如下的事情:

  1. 调用 __cxa_allocate_exception 函数,分配一个异常对象(数据结构如上)
  2. 调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化
  3. __cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind
  4. _Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine
  5. 该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理
  6. _Unwind_RaiseException() 将控制权转到相应的catch代码
  7. unwind 完成,用户代码继续执行

总结的太Bravo!!

从C++的角度看,一个完整的异常处理流程就完成了,当然省略了很多细节

如 personality routine

只需要大概明白,personality routine本身也不知道,只有编译器知道!

因此在编译阶段编译器会建立一些表项来保存相应的信息,使得 personality routine 可以在运行时通过这些事,先建立起来的信息进行相应的查询

从源码看 Unwind 过程

unwind 的过程是从 __cxa_throw() 里开始的,请看如下源码:

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
extern "C" void
__cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo,
void (_GLIBCXX_CDTOR_CALLABI *dest) (void *))
{
PROBE2 (throw, obj, tinfo);

// Definitely a primary.
__cxa_refcounted_exception *header = __get_refcounted_exception_header_from_obj (obj);
header->referenceCount = 1;
header->exc.exceptionType = tinfo;
header->exc.exceptionDestructor = dest;
header->exc.unexpectedHandler = std::get_unexpected ();
header->exc.terminateHandler = std::get_terminate ();
__GXX_INIT_PRIMARY_EXCEPTION_CLASS(header->exc.unwindHeader.exception_class);
header->exc.unwindHeader.exception_cleanup = __gxx_exception_cleanup;

// see here!! It is call RaiseException to unwind stack!
#ifdef _GLIBCXX_SJLJ_EXCEPTIONS
_Unwind_SjLj_RaiseException (&header->exc.unwindHeader);
#else
_Unwind_RaiseException (&header->exc.unwindHeader);
#endif

// Some sort of unwinding error. Note that terminate is a handler.
__cxa_begin_catch (&header->exc.unwindHeader);
std::terminate ();
}

我们可以看到 __cxa_throw 最终调用了 _Unwind_RaiseException(),stack unwind 就此开始

如前面所述,unwind 分为两个阶段,分别进行搜索 catch 及清理调用栈

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/* Raise an exception, passing along the given exception object.  */

_Unwind_Reason_Code
_Unwind_RaiseException(struct _Unwind_Exception *exc)
{
struct _Unwind_Context this_context, cur_context;
_Unwind_Reason_Code code;

uw_init_context (&this_context);
cur_context = this_context;

/* Phase 1: Search. Unwind the stack, calling the personality routine
with the _UA_SEARCH_PHASE flag set. Do not modify the stack yet. */
while (1)
{
_Unwind_FrameState fs;

code = uw_frame_state_for (&cur_context, &fs);

if (code == _URC_END_OF_STACK)
/* Hit end of stack with no handler found. */
return _URC_END_OF_STACK;

if (code != _URC_NO_REASON)
/* Some error encountered. Ususally the unwinder doesn't
diagnose these and merely crashes. */
return _URC_FATAL_PHASE1_ERROR;

/* Unwind successful. Run the personality routine, if any. */
if (fs.personality)
{
code = (*fs.personality) (1, _UA_SEARCH_PHASE, exc->exception_class,
exc, &cur_context);
if (code == _URC_HANDLER_FOUND)
break;
else if (code != _URC_CONTINUE_UNWIND)
return _URC_FATAL_PHASE1_ERROR;
}

uw_update_context (&cur_context, &fs);
}

/* Indicate to _Unwind_Resume and associated subroutines that this
is not a forced unwind. Further, note where we found a handler. */
exc->private_1 = 0;
exc->private_2 = uw_identify_context (&cur_context);

cur_context = this_context;
code = _Unwind_RaiseException_Phase2 (exc, &cur_context);
if (code != _URC_INSTALL_CONTEXT)
return code;

uw_install_context (&this_context, &cur_context);
}


static _Unwind_Reason_Code
_Unwind_RaiseException_Phase2(struct _Unwind_Exception *exc,
struct _Unwind_Context *context)
{
_Unwind_Reason_Code code;

while (1)
{
_Unwind_FrameState fs;
int match_handler;

code = uw_frame_state_for (context, &fs);

/* Identify when we've reached the designated handler context. */
match_handler = (uw_identify_context (context) == exc->private_2
? _UA_HANDLER_FRAME : 0);

if (code != _URC_NO_REASON)
/* Some error encountered. Usually the unwinder doesn't
diagnose these and merely crashes. */
return _URC_FATAL_PHASE2_ERROR;

/* Unwind successful. Run the personality routine, if any. */
if (fs.personality)
{
code = (*fs.personality) (1, _UA_CLEANUP_PHASE | match_handler,
exc->exception_class, exc, context);
if (code == _URC_INSTALL_CONTEXT)
break;
if (code != _URC_CONTINUE_UNWIND)
return _URC_FATAL_PHASE2_ERROR;
}

/* Don't let us unwind past the handler context. */
if (match_handler)
abort ();

uw_update_context (context, &fs);
}

return code;
}

如上两个函数分别对应了 unwind 过程中的这两个阶段,注意其中的:

1
2
3
uw_init_context()
uw_frame_state_for()
uw_update_context()

这几个函数主要是用来重建函数调用现场的,大概原理是:

对于调用链上的函数来说,它们的很大一部分上下文是可以从堆栈上恢复,如 ebp, esp, 返回地址等

编译器为了让 unwinder 可以从栈上获取这些信息,它的编译代码的时候,建立了很多表项用于记录每个可以抛异常的函数的相关信息,这些信息在重建上下文时将指导程序怎么去搜索栈上的东西

Some interesting things

说了一大堆,下面写个测试的程序简单回顾一下前面所说的关于异常处理的大概流程:

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
#include <iostream>
using namespace std;

void test_func3()
{
throw 3;

cout << "test func3" << endl;
}

void test_func2()
{
cout << "test func2" << endl;
try
{
test_func3();
}
catch (int)
{
cout << "catch 2" << endl;
}
}

void test_func1()
{
cout << "test func1" << endl;
try
{
test_func2();
}
catch (...)
{
cout << "catch 1" << endl;
}
}

int main()
{
test_func1();
return 0;
}

上面的程序运行起来后,我们可以在 __gxx_personality_v0 里下一个断点,可以看出流程

1
2
3
4
5
6
7
8
9
10
Breakpoint 2, 0x00dd0a46 in __gxx_personality_v0 () from /usr/lib/libstdc++.so.6
(gdb) bt
#0 0x00dd0a46 in __gxx_personality_v0 () from /usr/lib/libstdc++.so.6
#1 0x00d2af2c in _Unwind_RaiseException () from /lib/libgcc_s.so.1
#2 0x00dd10e2 in __cxa_throw () from /usr/lib/libstdc++.so.6
#3 0x08048979 in test_func3 () at exc.cc:6
#4 0x080489ac in test_func2 () at exc.cc:16
#5 0x08048a52 in test_func1 () at exc.cc:29
#6 0x08048ad1 in main () at exc.cc:39
(gdb)

(由于基本都不知道,所以基本是对链接文章的注释版复制)

第二篇!

https://www.cnblogs.com/catch/p/3619379.html

C++异常处理(2)

前一篇文章简单介绍C++异常处理的流程,但在一些细节 上一带而过

比如

相关的数据结构

unwind 的进行需要编译器生成一定的数据来支持,这些数据保存了与每个可能抛异常的函数相关的信息以供运行时查询

那么,编译保存了什么信息?根据 Itanium ABI 的定义,主要包括以下三类:

  1. unwind table,这个表记录了与函数相关的信息,共三个字段:函数的起始地址,函数的结束地址,一个 info block 指针
  2. unwind descriptor table,这个列表用于描述函数中需要 unwind 的区域的相关信息
  3. 语言相关的数据(language specific data area),用于上层语言内部的处理

以上数据结构的描述来自 Itanium ABI 的标准定义,但在具体实现时,这些数据是怎么组织以及放到了哪里则是由编译器来决定的

对于 GCC 来说,所有与 unwind 相关的数据都放到了 .eh_frame 及 .gcc_except_table 这两个 section 里面了,而且它的格式与内容和标准的定义稍稍有些不同

.eh_frame区域

.eh_frame 的格式与 .debug_frame 是很相似的(不完全相同),属于 DWARF 标准中的一部分

所有由 GCC 编译生成的需要支持异常处理的程序都包含了 DWARF 格式的数据与字节码

这些数据与字节码的主要作用有两个:

  1. 描述函数调用栈的结构(layout)
  2. 异常发生后,指导 unwinder 怎么进行 unwind

DWARF

这个字节码功能很强大,它是图灵完备的,这意味着仅仅通过 DWARF 就可以做任何事情(理论上),由于数据结构较为复杂,这里只记录了与其异常处理相关的staff.

本质上来讲,en_frame 像一张表,它根据程序中的某一条指令来设置相关的寄存器,从而返回到当前函数的调用函数

该表中:

image-20220718142215745

具体的实现上,en_frame 由一个CIE (Common Information Entry) 及多个 FDE (Frame Description Entry) 组成

它们在内存中是连续存放的:

image-20220718143252434

CIE 及 FDE 格式的定义参考如下:

CIE 结构

image-20220718143415367

FDE结构

image-20220718143437431

注意标注红色的字段:

  1. CIE->Initial Instructions, FDE->Call Frame Instructions 这两字段里放的就是所谓的 DWARF 字节码

    • 比如:DW_CFA_def_cfa R OFF,表示通过寄存器 R 及位移 OFF 来计算 CFA,其功能类似于前面的表格中第二列指明的内容
  2. FDE->PC begin, PC range, 这两个字段联合起来表示该 FDE 所能覆盖的指令的范围, eh_frame 中所有的 FDE 最后会按照 pc begin 排序进行存放

  3. 如果 CIE 中的 Augmentation String 中包含有字母 “P”,则相应的 Augmentation Data 中包含有指向 personality routine 的指针

  4. 如果 CIE 中的 Augmentation String 中包含有有字母“L”,则 FDE 中 Aumentation Data 包含有 language specific data 的指针

对一个elf文件通过如下命令:readelf -Wwf xxx,可以读取其中关于 .eh_frame 的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
The section .eh_frame contains:

00000000 0000001c 00000000 CIE
Version: 1
Augmentation: "zPL"
Code alignment factor: 1
Data alignment factor: -8
Return address column: 16
Augmentation data: 00 d8 09 40 00 00 00 00 00 00

DW_CFA_def_cfa: r7 ofs 8 ##以下为字节码
DW_CFA_offset: r16 at cfa-8

00000020 0000002c 00000024 FDE cie=00000000 pc=00400ac8..00400bd8
Augmentation data: 00 00 00 00 00 00 00 00
#以下为字节码
DW_CFA_advance_loc: 1 to 00400ac9
DW_CFA_def_cfa_offset: 16
DW_CFA_offset: r6 at cfa-16
DW_CFA_advance_loc: 3 to 00400acc
DW_CFA_def_cfa_reg: r6
DW_CFA_nop
DW_CFA_nop
DW_CFA_nop

对于由 GCC 编译出来的程序来说,CIE, FDE 是其在 unwind 过程中恢复现场时所依赖的全部东西,而且是完备的,这里所说的恢复现场指的是恢复调用当前函数的函数的现场

比如,func1 调用 func2,然后我们可以在 func2 里通过查询 CIE,FDE 恢复 func1 的现场

CIE,FDE 存在于每一个需要处理异常的 ELF 文件中,当异常发生时,runtime 根据当前 PC 值调用 dl_iterate_phdr() 函数就可以把当前程序所加载的所有模块轮询一遍,从而找到该 PC 所在模块的 eh_frame

1
2
3
4
5
6
7
8
9
10
11
12
13
for (n = info->dlpi_phnum; --n >= 0; phdr++)
{
if (phdr->p_type == PT_LOAD)
{
_Unwind_Ptr vaddr = phdr->p_vaddr + load_base;
if (data->pc >= vaddr && data->pc < vaddr + phdr->p_memsz)
match = 1;
}
else if (phdr->p_type == PT_GNU_EH_FRAME)
p_eh_frame_hdr = phdr;
else if (phdr->p_type == PT_DYNAMIC)
p_dynamic = phdr;
}

找到 eh_frame 也就找到 CIE,找到了 CIE 也就可以去搜索相应的 FDE,找到FDE及CIE后,就可以从这两数据表中提取相关的信息

并执行DWARF 字节码,从而得到当前函数的调用函数的现场,参看如下用于重建函数帧的函数:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
static _Unwind_Reason_Code
uw_frame_state_for (struct _Unwind_Context *context, _Unwind_FrameState *fs)
{
struct dwarf_fde *fde;
struct dwarf_cie *cie;
const unsigned char *aug, *insn, *end;

memset (fs, 0, sizeof (*fs));
context->args_size = 0;
context->lsda = 0;

// 根据context查找FDE。
fde = _Unwind_Find_FDE (context->ra - 1, &context->bases);
if (fde == NULL)
{
/* Couldn't find frame unwind info for this function. Try a
target-specific fallback mechanism. This will necessarily
not provide a personality routine or LSDA. */
#ifdef MD_FALLBACK_FRAME_STATE_FOR
MD_FALLBACK_FRAME_STATE_FOR (context, fs, success);
return _URC_END_OF_STACK;
success:
return _URC_NO_REASON;
#else
return _URC_END_OF_STACK;
#endif
}

fs->pc = context->bases.func;

// 获取对应的CIE.
cie = get_cie (fde);

// 提取出CIE中的信息,如personality routine的地址。
insn = extract_cie_info (cie, context, fs);
if (insn == NULL)
/* CIE contained unknown augmentation. */
return _URC_FATAL_PHASE1_ERROR;

/* First decode all the insns in the CIE. */
end = (unsigned char *) next_fde ((struct dwarf_fde *) cie);

// 执行dwarf字节码,从而恢复相应的寄存器的值。
execute_cfa_program (insn, end, context, fs);

// 定位到fde的相关数据
/* Locate augmentation for the fde. */
aug = (unsigned char *) fde + sizeof (*fde);
aug += 2 * size_of_encoded_value (fs->fde_encoding);
insn = NULL;
if (fs->saw_z)
{
_Unwind_Word i;
aug = read_uleb128 (aug, &i);
insn = aug + i;
}

// 读取language specific data的指针
if (fs->lsda_encoding != DW_EH_PE_omit)
aug = read_encoded_value (context, fs->lsda_encoding, aug,
(_Unwind_Ptr *) &context->lsda);

/* Then the insns in the FDE up to our target PC. */
if (insn == NULL)
insn = aug;
end = (unsigned char *) next_fde (fde);

// 执行FDE中的字节码。
execute_cfa_program (insn, end, context, fs);

return _URC_NO_REASON;
}

通过如上的操作,unwinder 就已经把调用函数的现场给重建起来了,这些现场信息包括:

1
2
3
4
5
6
7
8
9
struct _Unwind_Context
{
void *reg[DWARF_FRAME_REGISTERS+1]; //必要的寄存器。
void *cfa; // canoniacl frame address, 前面提到过,基地址。
void *ra;// 返回地址。
void *lsda;// 该函数对应的language specific data,如果存在的话。
struct dwarf_eh_bases bases;
_Unwind_Word args_size;
};

实现 Personality routine

Peronality routine 的作用主要有两个:

  1. 检查当前函数是否有相应的 catch 语句。

  2. 清理当前函数中的局部变量

然而这两件事情仅仅依靠 运行时 也是没法完成的,必须依靠编译器在 编译时 建立起相关的数据进行协助

对于GCC来说,这些与抛异常的函数具体相关的信息全部放在 .gcc_except_table 区域里去了,这些信息会作为Itanium ABI 接口中所谓的 language specific data 在 unwinder 与 C++ ABI 之间传递

根据前面的介绍,我们知道在 FDE 中保存有指向 language specific data 的指针(前面的源码中),因此 unwinder 在重建现场的时候就已经把这些数据读取了出来,c++ 的 ABI 只要调用 _Unwind_GetLanguageSpecificData() 就可以得到指向该数据的指针

下图来源于网络,展示了gcc_except_table 及 language specific data 的格式:

image-20220718155130981

由上图所示,LSDA 由一个表头以及三个表组成

1.LSDA Header:

该表头主要用来保存接下来三张表的相关信息,如编码,及表的位移等,该表头主要包含六个域:

1)LPStart encoding: Landing pad 起始地址的编码方式,长度为一个字节。

2)LPStart: landing pad 起始地址,这是可选的,只有当前面指明的编码方式不等于 DW_EH_PE_omit 时,这个字段才存在,此时读取这个字段就需要根据前面指定的编码方式进行读取,长度不固定,如果这个字段不存在,则 landing pad 的起始地址需要通过调用 _Unwind_GetRegionStart() 来获得,得到其实就是当前模块加载的起始地址,这是最常见的形式。

3)TType format: type table 的编码方式,长度为一个字节。

4)TTBase: type table 的位移,类型为 unsigned LEB128,这个字段是可选的,只有3)中编码方式不等于 DW_EH_PE_omit 时,这个才存在。

5)Call Site format: call site table 的编码方式,长度为一个字节。

6)Call Site format size: call site table 的长度,一个 unsigned LEB128 的值。

(不经想起第一次学PE文件数据结构的时候,一堆名词打在脸上,这次显然比上次的接受度高多了)

2.call site table

LSDA 表头之后紧跟着的是 call site table,该表用于记录程序中哪些指令有可能会抛异常,表中每条记录共有4个字段

1)call site position: 可能会抛异常的指令的地址,该地址是距 Landing pad 起始地址的偏移,编码方式由 LSDA 表头中第一个字段指明。

2)call the length: 可能抛异常的指令的区域长度,该字段与 1)一起表示一系列连续的指令,编码方式与 1)相同。

3)landing pad position: 用于处理上述指令的 Landing pad 的位移,这个值如果为 0 则表示不存在相应的 landing pad。

4)first action: 指明要采取哪些 action,这是一个 unsigned LEB128 的值,该值减1后作为下标获取 action table 中相应记录。

PS: call site table 中的记录按第一个字段也就是指令起始地址进行排序存放,因此 unwind 的时候可以加快对该表的搜索

unwind 的过程中:

(这里应该就是指向ttypeindex的关键了)

3. Action table

action table 中每一条记录是一个二元组

表示一个 catch 语句所对应的异常,或者表示当前函数所允许抛出的异常 (exception specification),该列表每条记录包含两个字段

1)type filter: 这是一个 unsigned LEB128 的数值,用于指向 type table 中的记录,该值有可能是负数。(这就是Inflated题中的ttypeindex?)

2)offset to next action: 指向下一个 action table 中的下一条记录,这是当函数中有多个 catch 或 exception specification 有多个时,将各个 action 记录链接起来。

4. Type Table

type table 中存放的是异常类型的指针:

1
std::type_info* type_tables[];

这个表被分成两部分,一部分是各个 catch 所对应的异常的类型,另一部分是该函数允许抛出的异常类型:

1
2
3
void func() throw(int, string)
{
}

type table中这两部分分别通过正负下标来进行索引:

img

有了如上这些数据,personality routine 只需要根据当前的 pc 值及当前的异常类型,不断在上述表中查找

最后就能找到当前函数是否有 landing pad,如果有则返回 _URC_INSTALL_CONTEXT,指示 unwinder 跳过去执行相应的代码。

什么是 Landing pad

在前面一篇博文里,提到了Landing pad:指的是能够 catch 当前异常的 catch 语句。这个说法其实不确切,准确来说,landing pad 指的是 unwinder 之外的“用户代码”:

1)用于 catch 相应的 exception,对于一个函数来说,如果该函数中有 catch 语句,且能够处理当前的异常,则该 catch 就是 landing pad。

2)如果当前函数没有 catch 或者 catch 不能处理当前 exception,则意味着异常还要从当前函数继续往上抛,因而 unwind 当前函数时有可能要进行相应的清理,此时这些清理局部变量的代码就是 landing pad

从名字上来看,顾名思议,landing pad 指的是程序的执行流程在进入当前函数后,最后要转到这里去(可以是用户写的catch代码也可以是清理局部变量的代码),很恰当的描述。

当 landing pad 是 catch 语句时,这个比较好理解,前面我们一直说清理局部变量的代码,这是什么意思呢?这些清理代码又放在哪里?为了说明这个问题,我们看一下如下代码:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <stddef.h>
using namespace std;

class cs
{
public:

explicit cs(int i) :i_(i) { cout << "cs constructor:" << i << endl; }
~cs() { cout << "cs destructor:" << i_ << endl; }

private:

int i_;
};

void test_func3()
{
cs c(33);
cs c2(332);

throw 3;

cs c3(333);
cout << "test func3" << endl;
}

void test_func3_2()
{
// 注意这
cs c(32);
cs c2(322);

test_func3();

cs c3(323);

test_func3();
}

void test_func2()
{
cs c(22);

cout << "test func2" << endl;
try
{
test_func3_2();

cs c2(222);
}
catch (int)
{
cout << "catch 2" << endl;
}
}

void test_func1()
{
cout << "test func1" << endl;
try
{
test_func2();
}
catch (...)
{
cout << "catch 1" << endl;
}
}

int main()
{
test_func1();
return 0;
}

对于函数 test_func3_2() 来说,当 test_func3() 抛出异常后,在 unwind 的第二阶段,我们知道 test_func3_2() 中的局部变量 c 及 c2 是需要清理的,而 c3 则不用,那么编译器是怎么生成代码来完成这件事情的呢?

当异常发生时,运行时是没有办法知道当前哪些变量是需要清理的,因为这个原因编译器在生成代码的时候,在函数的末尾设置了多个出口,使得当异常发生时,可以直接跳到某一段代码就能清理相应的局部变量,我们看看 test_func3_2() 编译后生成的对应的汇编代码:

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
51
52
53
54
55
56
57
58
59
void test_func3_2()
{
400ca4: 55 push %rbp
400ca5: 48 89 e5 mov %rsp,%rbp
400ca8: 53 push %rbx
400ca9: 48 83 ec 48 sub $0x48,%rsp
cs c(32);
400cad: 48 8d 7d e0 lea 0xffffffffffffffe0(%rbp),%rdi
400cb1: be 20 00 00 00 mov $0x20,%esi
400cb6: e8 9f 02 00 00 callq 400f5a <_ZN2csC1Ei>
cs c2(322);
400cbb: 48 8d 7d d0 lea 0xffffffffffffffd0(%rbp),%rdi
400cbf: be 42 01 00 00 mov $0x142,%esi
400cc4: e8 91 02 00 00 callq 400f5a <_ZN2csC1Ei>

test_func3();
400cc9: e8 5a ff ff ff callq 400c28 <_Z10test_func3v>

cs c3(323);
400cce: 48 8d 7d c0 lea 0xffffffffffffffc0(%rbp),%rdi
400cd2: be 43 01 00 00 mov $0x143,%esi
400cd7: e8 7e 02 00 00 callq 400f5a <_ZN2csC1Ei>

test_func3();
400cdc: e8 47 ff ff ff callq 400c28 <_Z10test_func3v>
400ce1: eb 17 jmp 400cfa <_Z12test_func3_2v+0x56>
400ce3: 48 89 45 b8 mov %rax,0xffffffffffffffb8(%rbp)
400ce7: 48 8b 5d b8 mov 0xffffffffffffffb8(%rbp),%rbx
400ceb: 48 8d 7d c0 lea 0xffffffffffffffc0(%rbp),%rdi #c3的this指针
400cef: e8 2e 02 00 00 callq 400f22 <_ZN2csD1Ev>
400cf4: 48 89 5d b8 mov %rbx,0xffffffffffffffb8(%rbp)
400cf8: eb 0f jmp 400d09 <_Z12test_func3_2v+0x65>
400cfa: 48 8d 7d c0 lea 0xffffffffffffffc0(%rbp),%rdi #c3的this指针
400cfe: e8 1f 02 00 00 callq 400f22 <_ZN2csD1Ev>
400d03: eb 17 jmp 400d1c <_Z12test_func3_2v+0x78>
400d05: 48 89 45 b8 mov %rax,0xffffffffffffffb8(%rbp)
400d09: 48 8b 5d b8 mov 0xffffffffffffffb8(%rbp),%rbx
400d0d: 48 8d 7d d0 lea 0xffffffffffffffd0(%rbp),%rdi #c2的this指针
400d11: e8 0c 02 00 00 callq 400f22 <_ZN2csD1Ev>
400d16: 48 89 5d b8 mov %rbx,0xffffffffffffffb8(%rbp)
400d1a: eb 0f jmp 400d2b <_Z12test_func3_2v+0x87>
400d1c: 48 8d 7d d0 lea 0xffffffffffffffd0(%rbp),%rdi #c2的this指针
400d20: e8 fd 01 00 00 callq 400f22 <_ZN2csD1Ev>
400d25: eb 1e jmp 400d45 <_Z12test_func3_2v+0xa1>
400d27: 48 89 45 b8 mov %rax,0xffffffffffffffb8(%rbp)
400d2b: 48 8b 5d b8 mov 0xffffffffffffffb8(%rbp),%rbx
400d2f: 48 8d 7d e0 lea 0xffffffffffffffe0(%rbp),%rdi #c的this指针
400d33: e8 ea 01 00 00 callq 400f22 <_ZN2csD1Ev>
400d38: 48 89 5d b8 mov %rbx,0xffffffffffffffb8(%rbp)
400d3c: 48 8b 7d b8 mov 0xffffffffffffffb8(%rbp),%rdi
400d40: e8 b3 fc ff ff callq 4009f8 <_Unwind_Resume@plt> #c的this指针
400d45: 48 8d 7d e0 lea 0xffffffffffffffe0(%rbp),%rdi
400d49: e8 d4 01 00 00 callq 400f22 <_ZN2csD1Ev>
}
400d4e: 48 83 c4 48 add $0x48,%rsp
400d52: 5b pop %rbx
400d53: c9 leaveq
400d54: c3 retq
400d55: 90 nop

注意其中标红色的代码,_ZN2csD1Ev 即是类 cs 的析构函数

Unwind_Resume() 则是当清理完成时,用来从 landing pad 返回的代码

test_func3_2() 中只有 3 个 cs 对象,但调用析构函数的代码却出现了 6 次。这里其实就是设置了多个出口函数,分别对应不同情况下,处理各个局部变量的析构,对于我们上面的代码来说,test_func3_2() 函数中的 landing pad 就是从地址:400d09 开始的,这些代码做了如下事情:

1)先析构 c2,然后 jump 到 400d2b 析构 c.

2)最后调用 _Unwind_Resum()

由此可见当程序中有多个可能抛异常的地方时,landing pad 也相应地会有多个,该函数的出口将更复杂,这也算是异常处理的一个 overhead 了

总结

太强了,实在是太强了,我感觉很久没看过这么干货满满的文章,感觉是我异常处理认识的一个升华,不过新名词太多,也需要反复阅读理解方可行

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.