C++ exception handling ABI
2020-12-12 17:00:00 Author: maskray.me(查看原文) 阅读量:350 收藏

C++ exceptions是stack unwinding的一个应用。有多种ABI,其中应用最广泛的是Itanium C++ ABI: Exception Handling

Itanium C++ ABI: Exception Handling

分成Level 1 Base ABI and Level 2 C++ ABI

Base ABI描述了语言无关的stack unwinding部分,定义了_Unwind_* API。常见实现是:

  • libgcc_s.so.1libgcc_eh.a
  • 多个名称为libunwind的库(libunwind.solibunwind.a)。使用Clang的话可以用--rtlib=compiler-rt --unwindlib=libunwind选择链接libunwind,可以用llvm-project/libunwind或nongnu.org/libunwind

C++ ABI则和C++语言相关,提供interoperability of C++ implementations。定义了__cxa_* API(__cxa_allocate_exception, __cxa_throw, __cxa_begin_catch等)。常见实现是:

  • libsupc++,libstdc++的一部分
  • llvm-project中的libc++abi

libc++可以接入libc++abi、libcxxrt或libsupc++,推荐libc++abi。

值得注意的是,libc++abi提供的<exception> <stdexcept>定义(如logic_error runtime_error等的destructors)都是特意和libsupc++兼容的。 libsupc++和libc++abi不使用inline namespace,有冲突的符号名,因此通常一个libc++/libc++abi应用无法使用某个动态链接libstdc++.so的shared object。

如果花一些工夫,还是能解决这个问题的:编译libstdc++中非libsupc++的部分得到自制libstdc++.so.6。可执行档链接libc++abi提供libstdc++.so.6需要的C++ ABI符号。

Personality routines是沟通Level 1 Base ABI和Level 2 C++ ABI的桥梁。不同的语言、实现或架构可能使用不同的personality routines。

数据表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14

struct _Unwind_Exception {
_Unwind_Exception_Class exception_class;
_Unwind_Exception_Cleanup_Fn exception_cleanup;
_Unwind_Word private_1;
_Unwind_Word private_2;
} __attribute__((aligned));


struct __cxa_exception {
...
int handlerCount;
_Unwind_Exception unwindHeader;
};

exception_classexception_cleanup可以被Level 1和Level 2实现访问。personality可以读取exception_class(判断是否native exception、判断是否dependent exception)、会设置exception_cleanup(用于_Unwind_DeleteException)。

  • Normal unwind把private_2用于缓存phase 1的stack pointer
  • Forced unwind则把private_2用作stop function的参数
  • personality不应访问private_1private_2

对于如下代码:

1
2
3
void foo() { throw 0xB612; }
void bar() { B b; foo(); }
void qux() { try { A a; bar(); } catch (int x) { puts(""); } }

编译得到的汇编概念上长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
void foo() {
__cxa_exception *thrown = __cxa_allocate_exception(4);
*thrown = 42;
__cxa_throw(thrown, &typeid(int), nullptr);
}
void bar() {
B b; foo(); return;
landing_pad: b.~B(); _Unwind_Resume();
}
void qux() {
A a; bar(); return;
landing_pad: __cxa_begin_catch(obj); puts(""); __cxa_end_catch(obj);
}

运行流程:

  • qux调用bar,bar调用foo,foo抛出exception
  • foo动态分配内存块,存放抛出的int和__cxa_exception header。执行__cxa_throw
  • __cxa_throw填充__cxa_exception的其他字段,调用_Unwind_RaiseException进行stack unwinding
  • _Unwind_RaiseException执行phase 1: search phase
    • 追溯foo的调用链
    • 对于每个栈帧,如果没有personality routine(C++一般是__gxx_personality_v0)则跳过;有则调用(action设置为UA_SEARCH_PHASE)。personality告诉phase 1是否找到了一个catching handler(_URC_HANDLER_FOUND),是则停止unwind,没有则跳过
    • 在这里例子中,bar只有destructor(_URC_CONTINUE_UNWIND),qux的stack pointer会被标记(保存在private_2中)并停止搜索
  • _Unwind_RaiseException执行phase 2: cleanup phase
    • 追溯foo的调用链
    • 对于每个栈帧,如果没有personality routine则跳过;有则调用
    • 对于没有被标记(stack pointer不等于private_2)的中间栈帧,跳转到landing pad执行清理工作(destructor)。landing pad会调用_Unwind_Resume交还控制流
    • 对于被phase 1标记的栈帧,调用personality时action设置为_UA_CLEANUP_PHASE|_UA_HANDLER_FRAME。landing pad会调用__cxa_begin_catch,然后执行catch block中的代码,最后调用__cxa_end_catch销毁exception物件

下面代码描述涉及的几个核心函数:

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
static _Unwind_Reason_Code unwind_phase1(unw_context_t *uc, _Unwind_Context *ctx,
_Unwind_Exception *obj) {


unw_init_local(uc, ctx);
for(;;) {
if (ctx->fdeMissing) return _URC_END_OF_STACK;
if (!step(ctx)) return _URC_FATAL_PHASE1_ERROR;
ctx->getFdeAndCieFromIP();
if (!ctx->personality) continue;
switch (ctx->personality(1, _UA_SEARCH_PHASE, obj->exception_class, obj, ctx)) {
case _URC_CONTINUE_UNWIND: break;
case _URC_HANDLER_FOUND:
unw_get_reg(ctx, UNW_REG_SP, &obj->private_2);
return _URC_NO_REASON;
default: return _URC_FATAL_PHASE1_ERROR;
}
}
return _URC_NO_REASON;
}

static _Unwind_Reason_Code unwind_phase2(unw_context_t *uc, _Unwind_Context *ctx,
_Unwind_Exception *obj) {


unw_init_local(uc, ctx);
for(;;) {
if (ctx->fdeMissing) return _URC_END_OF_STACK;
if (!step(ctx)) return _URC_FATAL_PHASE2_ERROR;
ctx->getFdeAndCieFromIP();
if (!ctx->personality) continue;
_Unwind_Action action = _UA_CLEANUP_PHASE;
size_t sp;
unw_get_reg(ctx, UNW_REG_SP, &sp);
if (sp == obj->private_2) action |= _UA_HANDLER_FRAME;
switch (ctx->personality(1, action, obj->exception_class, obj, ctx)) {
case _URC_CONTINUE_UNWIND:
break;
case _URC_INSTALL_CONTEXT:
unw_resume(ctx);
return _URC_FATAL_PHASE2_ERROR;
default: return _URC_FATAL_PHASE2_ERROR;
}
}
return _URC_FATAL_PHASE2_ERROR;
}

_Unwind_Reason_Code _Unwind_RaiseException(_Unwind_Exception *obj) {
unw_context_t uc;
_Unwind_Context ctx;
__unw_getcontext(&uc);
_Unwind_Reason_Code phase1 = unwind_phase1(&uc, &ctx, obj);
if (phase1 != _URC_NO_REASON) return phase1;
unwind_phase2(&uc, &ctx, obj);
}

void __cxa_throw(void *thrown, std::type_info *tinfo, void (*destructor)(void *)) {
uncaughtExceptions++;
__cxa_exception *hdr = (__cxa_exception *)thrown - 1;
hdr->exceptionType = tinfo; hdr->destructor = destructor;
_Unwind_RaiseException(&hdr->unwindHeader);

__cxa_begin_catch(&hdr->unwindHeader); std::terminate();
}
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
main:                                   # @main
.Lfunc_begin0:
.cfi_startproc
.cfi_personality 3, __gxx_personality_v0
.cfi_lsda 3, .Lexception0
# %bb.0: # %entry
pushq %rax
.cfi_def_cfa_offset 16
.Ltmp0:
callq _Z2fbv # try region
.Ltmp1:
.LBB0_2:
xorl %eax, %eax
popq %rcx
.cfi_def_cfa_offset 8
retq
.LBB0_1: # landing pad
.cfi_def_cfa_offset 16
.Ltmp2:
movq %rax, %rdi
callq __cxa_begin_catch
movl (%rax), %esi
movl $.L.str, %edi
xorl %eax, %eax
callq printf
callq __cxa_end_catch
jmp .LBB0_2
.Lfunc_end0:
.size main, .Lfunc_end0-main
.cfi_endproc

Personality

之前提到了,personality routines是沟通Level 1 Base ABI和Level 2 C++ ABI的桥梁。常用的personality如下:

  • __gxx_personality_v0: C++
  • __gxx_personality_sj0: sjlj
  • __gcc_personality_v0: C -fexceptions,用于__attribute__((cleanup(...)))
  • __CxxFrameHandler3: Windows MSVC
  • __gxx_personality_seh0: MinGW-w64 -fseh-exceptions
  • __objc_personality_v0: MacOSX环境ObjC

ELF系统C++最常用的是__gxx_personality_v0,其实现在:

  • GCC: libstdc++-v3/libsupc++/eh\_personality.cc
  • libc++abi: src/cxa\_personality.cpp

流程:

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
_unwind_Reason_Code __gxx_personality_v0(int version, _Unwind_Action action, uint64_t exceptionClass, _Unwind_Exception *exc, _Unwind_Context *ctx) {
if (action & _UA_SEARCH_PHASE) {
scan_result res = scan_eh_tab(action, is_native, exc, ctx);
if (res.reason == _URC_HANDLER_FOUND) {
if (is_native) {
auto *hdr = (__cxa_exception *)(exc+1) - 1;

}
return _URC_HANDLER_FOUND;
}
return res.reason;
} else if (action & _US_CLEANUP_PHASE) {
if (action & _UA_HANDLER_FRAME) {
if (is_native) {
auto *hdr = (__cxa_exception *)(exc+1) - 1;

} else {
scan_result res = scan_eh_tab(action, is_native, exc, ctx);
if (res.reason != _URC_HANDLER_FOUND)
terminate();
}

_Unwind_SetGR(...);
_Unwind_SetGR(...);
_Unwind_SetIP(ctx, res.landingPad);
return _URC_INSTALL_CONTEXT;
}
scan_result res = scan_eh_tab(action, is_native, exc, ctx);
if (res.reason == _URC_HANDLER_FOUND) {

_Unwind_SetGR(...);
_Unwind_SetGR(...);
_Unwind_SetIP(ctx, res.landingPad);
return _URC_INSTALL_CONTEXT;
}
return res.reason;
}
return _URC_FATAL_PHASE1_ERROR;
}

在三种情况下会解析.gcc_except_table

  • action & _UA_SEARCH_PHASE
  • action & _UA_CLEANUP_PHASE && action & _UA_HANDLER_FRAME && !is_native: native的情况应用cached result
  • action & _UA_CLEANUP_PHASE && !(action & _UA_HANDLER_FRAME)

rethrow

Rethrow exception执行__cxa_rethrow,personality会返回_URC_INSTALL_CONTEXT回到rethrow所在call site code range对应的landing pad,执行__cxa_end_catch; _Unwind_Resume

通常caught exception会在__cxa_end_catch销毁,因此__cxa_rethrow会标记exception object并增加handlerCount

C++11 引入了Exception Propagation (N2179; std::rethrow_exception etc),libstdc++中使用__cxa_dependent_exception实现。 设计参见https://gcc.gnu.org/legacy-ml/libstdc++/2008-05/msg00079.html

1
2
3
4
struct __cxa_dependent_exception {
void *reserve;
void *primaryException;
};

std::current_exceptionstd::rethrow_exception会增加引用计数。

LLVM IR

待补充

  • nounwind: cannot unwind
  • unwtables: force generation of the unwind table regardless of nounwind
1
2
3
4
5
6
7
if uwtables
if nounwind
CantUnwind
else
Unwind Table
else
do nothing

Behavior

clang

  • -fno-exceptions && -fno-asynchronous-unwind-tables => no .eh_frame
  • -fno-exceptions => no .gcc_except_table
  • noexcept && -fexceptions => call __clang_call_terminate

no .eh_frame => __cxa_throw calls std::terminate since _Unwind_RaiseException returns .eh_frame + empty .gcc_except_table => __gxx_personality_v0 calls std::terminate since no call site code range matches .eh_frame without .gcc_except_table => pass-through

Limitation

  • Clang .gcc_except_table is inefficient for pass-through frames. GCC produces header-only LSDA (4 bytes).
  • Clang/LLD interop: garbage collect unused not within COMDAT groups
  • Efficient (space/performance) (very difficult; (current) compact unwinding has lots of limitations; )

文章来源: http://maskray.me/blog/2020-12-12-c++-exception-handling-abi
如有侵权请联系:admin#unsafe.sh