exception handling

简单整理一下g++在x86/64上的exception的实现

Overview

1
2
3
4
5
6
7
8
|	f3	|
---------
| f2 |
--------- ^
| f1 | | direction of stack growth
---------
| f0 |
---------

上图为4个函数调用对应的4个调用帧

1
2
3
4
5
6
7
8
9
10
11
12
void f3 () {
throw 1;
}
void f2 () {
f3();
}
void f1 () {
f2();
}
void f0 () {
f1();
}

overview:当f3抛出异常时,应该顺着函数调用栈从顶部往下,即f3->f2->f1->f0的顺序检查每一个函数是否能处理这个异常。

这意味着哪个函数应该处理抛出的异常是运行时候确定而非编译器确定的。这说明编译器必须为每一个函数生成必要的信息以使得运行时可以确定这个函数对应的调用是否能处理抛出的异常。

Compiler support

编译器生成了CFI(call frame information)使得追踪每一个调用帧成为可能,同时编译器也为每一个函数生成

LSDA(language specific data area)也叫 .gcc_except_table,LSDA说明该函数具有的

  • 所有try/catch块(一个try块叫call site)的try的起始地址
  • 所有try/catch块的try块长度
  • 所有try/catch块的第一个catch块的起始位置
    • catch块也叫landing pad,因为当try块中抛出的异常可以捕捉时,会跳转到catch开始执行即landing
  • 所有catch块对应的action(就是每一个块catch的exception的类型, RTTI用于类型比对)
    • 因为一个try可以有多个catch块,每一个catch块对应一个action

可以用下面的数据结构描述提到的两个重要数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LSDA call site table entry
struct LSDA_Call_Site {
// Offset into function from which we could handle a throw
uint8_t start;
// Length of the block that might throw
uint8_t len;
// Landing pad
uint8_t lp;
// Offset into action table + 1 (0 means no action)
uint8_t action;
};

// action table entry
struct Action {
// An index into the types table
int type_index;
// dont care
int next_offset;
// dont care
LSDA_ptr raw_action_ptr;

} current_action;

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void f() {
// cs1
try{ // cs1.start
// cs1.len
}catch(e1){ // cs1.lp, action11

}catch(e2) { // action12

}

// cs2
try{ // cs2.start
// cs2.len
}catch(e1){ // cs2.lp, action21

}catch(e2) { // action22

}
}

有了以上信息之后,可以使用unwind库来获取以上定义的信息以帮助执行exception handling

unwind库是用于操作程序的调用链(call chain),通常用于exception handling和debug等。提供了

1
2
3
4
5
6
7
_Unwind_GetIP // 获取当前帧即将调用下一帧之前的执行地址+1
_Unwind_GetRegionStart // 获取调用帧对应的函数的开头地址
_Unwind_GetLanguageSpecificData // 获取LSDA数据
_Unwind_RaiseException // 遍历调用帧执行personality函数
_Unwind_SetGR // 设定unwind重新执行时候的通用寄存器内容
_Unwind_SetIP // 设定unwind重新执行时候的ip地址
...

等函数。

下面详细说具体的流程

Details

  1. throw被翻译成cxa_allocate与cxa_throw。cxa_allocate分配exception对象的内存空间,cxa_throw调用unwind_raiseException函数。

  2. unwind_raiseException遍历调用栈上的调用帧,对每一个调用帧分两阶段调用personality_v0函数。

    2.1 第一阶段执行search,判断该调用帧是否能handle指定的exception,判断的方法是根据call site table判定发生exception的指令是否处于该函数的任意try块(start + len > exception_ip > start)中,如果是则返回_URC_HANDLER_FOUND,否则查找下一个调用帧。如果在某一个调用帧中找到能处理exception的函数,执行第二阶段,否则执行terminate。

    2.2 第二阶段执行cleanup,再次重复执行第一个阶段的判定,对所有不符合的调用帧执行cleanup code,即清理栈上静态分配的函数,调用析构函数,这确保了RAII正确的工作。对能处理的调用帧,找到对应的action,返回_URC_INSTALL_CONTEXT跳转到对应的landing pad。unwind函数库会根据匹配的exception type和landing pad地址执行对应的catch block。

    分成两阶段的原因是希望在没有找到能处理exception的帧的情况下能保留栈帧给terminate。

上述提到的函数都是在libc++里定义好。如果不借助libc++自己实现一个exception handling,则需要提供上面提到的所有函数…

Reference