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.1
或libgcc_eh.a
- 多个名称为libunwind的库(
libunwind.so
或libunwind.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 |
|
exception_class
和exception_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_1
和private_2
对于如下代码:
1 | void foo() { throw 0xB612; } |
编译得到的汇编概念上长这样:
1 | void foo() { |
运行流程:
- 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 | static _Unwind_Reason_Code unwind_phase1(unw_context_t *uc, _Unwind_Context *ctx, |
1 | main: # @main |
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 | _unwind_Reason_Code __gxx_personality_v0(int version, _Unwind_Action action, uint64_t exceptionClass, _Unwind_Exception *exc, _Unwind_Context *ctx) { |
在三种情况下会解析.gcc_except_table
:
action & _UA_SEARCH_PHASE
action & _UA_CLEANUP_PHASE && action & _UA_HANDLER_FRAME && !is_native
: native的情况应用cached resultaction & _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 | struct __cxa_dependent_exception { |
std::current_exception
和std::rethrow_exception
会增加引用计数。
LLVM IR
待补充
- nounwind: cannot unwind
- unwtables: force generation of the unwind table regardless of nounwind
1 | if uwtables |
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; )