C++ Exception Handling ABI, part 1
C++ 异常处理的第一篇,这一篇先对栈展开的流程建立一个基本概念,下一篇再补充一些深入细节。
Itanium C++ ABI
要聊 C++ 的异常处理,首先我们得了解 Itanium C++ ABI。这个术语中,Itanium 是一个曾经想取代 x86,但目前已经退出市场的处理器架构。虽然处理器架构失败了,但其副产品 Itanium C++ ABI 早就不只服务于 Itanium 架构,而是逐渐演化成类 Unix 平台上 C++ ABI 的事实标准。今天我们在 x86-64、AArch64 等平台上看到的 C++ 异常处理、RTTI、dynamic_cast 等运行时行为,都遵循这套规范。
对于异常处理,虽然 Itanium ABI 最初是为 C++ 设计的,但它是建立在更底层的组件之上,这些组件都是语言无关的:
- System V ABI
- DWARF
- libunwind
因此,任何语言只要按照 Itanium ABI 来实现,都可以使用同样的异常处理 ABI 机制。 所以 Itanium C++ ABI 实际可以拆分成两层:
- Level 1: Base ABI,定义语言无关的栈展开机制
- Level 2: C++ ABI,在 Level 1 之上补充 C++ 语义,比如
throw/catch等
Base ABI 描述语言无关的栈展开过程,并定义 _Unwind_* API。常见实现如下所示,它们也就是通常所说的 unwinder :
libgcc中的libgcc_s.so.1(libgcc_s是gcc运行时一部分)libunwind: https://github.com/libunwind/libunwindllvm中的libunwind: https://github.com/llvm/llvm-project/tree/main/libunwind
C++ ABI则和 C++ 语言本身相关,定义了 __cxa_* API(例如 __cxa_allocate_exception、__cxa_throw、__cxa_begin_catch 等),以及如何通过这些 API,实现 C++ 的 throw / catch 语法。常见实现有:
libstdc++中的libsupc++(support library for C++): 除了上面提到的__cxa_*API 之外,还提供 RTTI 以及dynamic_cast。libsupc++提供了 C++ 中所有动态类型相关的实现llvm中的c++abi: https://github.com/llvm/llvm-project/tree/main/libcxxabi
这些库的名字很容易让人混淆。
libstdc++= C++ 标准库 +libsupc++,其中libsupc++提供了异常处理所需的__cxa_*API。libgcc则是编译器生成代码时依赖的底层运行时支持库,其中包含异常处理需要的_Unwind_*API。
| ABI 层级 | 作用 | GCC 常见实现 | LLVM 常见实现 |
|---|---|---|---|
| Level 1: Base ABI(语言无关) | 定义_Unwind_* API,负责栈展开 |
libgcc_s.so.1(隶属 libgcc) |
libunwind |
| Level 2: C++ ABI(语言相关) | 负责将 throw / catch 语义转换为对应__cxa_* API调用 |
libsupc++(隶属 libstdc++) |
libc++abi |
下面会先简单描述两层 API 的主要作用,之后再分章节详细介绍。
第一层规定了如何做栈展开(stack unwinding),是一套语言无关的通用接口。它主要包括以下几部分(具体内容先看不懂也没关系):
_Unwind_Exception异常对象结构_Unwind_*API,这里只列出最重要的两个:_Unwind_RaiseException_Unwind_Resume
- 如何进行栈展开,整个过程分成搜索和清理两个阶段,后面会详细介绍
personality:在栈展开过程中,unwinder会询问当前栈帧的personality能不能处理这个异常:- 如果能,应该跳到哪个
catch块,从哪条指令开始继续执行 - 如果不能,当前栈帧是否需要额外清理工作,比如清理栈上对象
- 如果能,应该跳到哪个
概括一下,这一层定义的是:由 _Unwind_RaiseException 负责执行两阶段栈展开,而栈展开过程中涉及的语言相关概念,例如 catch 块、离开作用域后的对象析构等,都由 personality 封装。正因为如此,这套 ABI 才能支持多语言,并允许它们和 C++ 一起工作。
第二层就是我们通常所说的 C++ ABI。它定义的是 C++ 语言特性在运行时的实现规则和接口,大致可以分成几部分:
__cxa_exception: C++ 的异常对象结构,其中包含 Level 1 里的_Unwind_Exception__cxa_* API: 异常运行时 API。它本质上是把 C++ 异常相关的语法(throw/catch)翻译成运行时的函数调用__cxa_begin_catch__cxa_end_catch__cxa_allocate_exception__cxa_throw
- RTTI:动态类型相关的支持,主要包括:
std::type_infotypeid运算符- 类型比较方式
dynamic_cast
异常处理过程中需要比较
throw出来的异常类型和catch声明的类型是否匹配,因此 RTTI 也会参与异常处理。
Level 1: Base ABI
_Unwind_Exception
数据结构如下:
// Level 1
struct _Unwind_Exception {
_Unwind_Exception_Class exception_class; // an identifier, used to tell whether the exception is native
_Unwind_Exception_Cleanup_Fn exception_cleanup;
_Unwind_Word private_1; // zero: normal unwind; non-zero: forced unwind, the _Unwind_Stop_Fn function
_Unwind_Word private_2; // saved stack pointer
} __attribute__((aligned));
exception_class 和 exception_cleanup 由 Level 2 中负责抛异常的 API 设置。Level 1 并不关心 exception_class 的具体含义,而是把它原样传给 personality,再由后者判断当前异常是 native exception 还是 foreign exception (可以简单理解为 C++ 运行时抛出的异常为 native exception,其他语言抛出的异常为 foreign exception,这块文章最后会再补充一些)。
exception_class 用来表示这个异常对象属于哪种语言和运行时,前4个字节一般表示厂商,而后4个字节表示语言。例如,libc++abi 的 __cxa_throw 会把 exception_class 设成表示 "CLNGC++\0" 的 uint64_t,而 libsupc++ 使用的是表示 "GNUCC++\0" 的 uint64_t。exception_cleanup 保存对应异常对象的析构函数,会在出 catch 作用域时,由 Level 2 的 API 调用。
栈展开过程中需要的相关信息,比如给定 IP 或者 SP 寄存器如何获取上一个栈帧的 IP 和 SP,则是由具体实现定义。对于 ELF,栈展开的相关信息都保存在 .eh_frame 和 .eh_frame_hdr中。这部分原理不影响理解栈展开的主要流程,我们在下一篇再详细介绍。
API
_Unwind_RaiseException 负责执行异常的栈展开。这个函数没有通常意义上的 return 语句,控制权最终要么转移给匹配到的 catch 块,要么在无法 catch 时转移给相应清理代码,从而析构局部对象的代码。整个过程分成两个阶段:search phase(搜索阶段)和 cleanup phase(清理阶段)。
- 在搜索阶段,要找出能够处理该异常的
catch,并把对应栈帧的栈指针记录到private_2- 根据 IP、SP 以及其他已保存寄存器,沿着调用链逐帧回溯
- 对每个栈帧,如果没有对应
personality就跳过;如果有,就传入_UA_SEARCH_PHASE作为参数并调用它 - 如果
personality返回_URC_CONTINUE_UNWIND,表示继续向上搜索 - 如果
personality返回_URC_HANDLER_FOUND,表示找到了匹配的catch块,将对应栈帧保存到private_2。 - 过程中如果发现 ABI 层面不匹配,此时搜索停止
- 在清理阶段,要先跳转搜索阶段遍历过程中,没有捕获异常的栈帧的清理代码(通常是局部变量析构),最后再把控制权转交给搜索阶段找到的
catch块- 同样根据 IP、SP 和其他寄存器沿调用链逐帧回溯
- 对每个栈帧,如果没有对应
personality就跳过;如果有,就传入_UA_CLEANUP_PHASE作为参数并调用它;而搜索阶段标记过的那个栈帧还会额外带上_UA_HANDLER_FRAME - 如果
personality返回_URC_CONTINUE_UNWIND,表示没有landing pad,即该栈帧不需要额外处理 - 如果
personality返回_URC_INSTALL_CONTEXT,表示找到了landing pad,需要跳转到landing pad继续执行 - 对于那些没有在搜索阶段被标记的中间栈帧,
landing pad只负责清理工作(通常是析构已离开作用域的变量),然后调用_Unwind_Resume回到清理阶段 - 对于搜索阶段标记的那个栈帧,
landing pad会调用__cxa_begin_catch,随后执行catch块中的代码,最后调用__cxa_end_catch完成销毁异常对象
landing pad在下面 Level 2 部分会介绍,它是一段编译器为函数生成的处理异常的代码。这里补充一点,具体跳转到landing pad的操作由unwinder完成,而跳转到哪里则是由personality决定的。
关于 personality 我们在下一篇会详细介绍,此处只需要了解它连接了 Level 1 和 Level 2 API,其主要功能是:
- 在栈展开过程中检查每个栈帧是否有匹配的
catch - 搜索阶段返回
_URC_CONTINUE_UNWIND或_URC_HANDLER_FOUND,以表示该栈帧能否处理该异常 - 清理阶段返回
_URC_CONTINUE_UNWIND或_URC_INSTALL_CONTEXT,以表示是否跳转到landing pad
除此之外,还有几个常见的 API:
_Unwind_ForcedUnwind: 强制栈展开,也就是跳过搜索阶段,直接进入清理阶段,典型场景是pthread_cancel_Unwind_Resume: Level 1 中几乎唯一一个直接由编译器生成调用的 API。如果当前栈帧不能捕获异常、但需要先清理栈上对象,那么清理完成后就会调用_Unwind_Resume继续清理阶段_Unwind_DeleteException:调用_Unwind_Exception中的exception_cleanup销毁给定的异常对象。_Unwind_Backtrace: 忽略personality,而是执行一个回调。典型场景就是 gdb 里的 backtrace,大致原理是用当前指令寄存器%rip去查.eh_frame,算出“上一帧在哪”,然后不断重复
完整 _Unwind_RaiseException 栈展开的代码如下:
static _Unwind_Reason_Code unwind_phase1(unw_context_t *uc, _Unwind_Context *ctx,
_Unwind_Exception *obj) {
// Search phase: unwind and call personality with _UA_SEARCH_PHASE for each frame
// until a handler (catch block) is found.
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; // e.g. stack corruption
}
}
return _URC_NO_REASON;
}
static _Unwind_Reason_Code unwind_phase2(unw_context_t *uc, _Unwind_Context *ctx,
_Unwind_Exception *obj) {
// Cleanup phase: unwind and call personality with _UA_CLEANUP_PHASE for each frame
// until reaching the handler. Restore the register state and transfer control.
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 actions = _UA_CLEANUP_PHASE;
size_t sp;
unw_get_reg(ctx, UNW_REG_SP, &sp);
if (sp == obj->private_2) actions |= _UA_HANDLER_FRAME;
switch (ctx->personality(1, actions, obj->exception_class, obj, ctx)) {
case _URC_CONTINUE_UNWIND:
break;
case _URC_INSTALL_CONTEXT:
unw_resume(ctx); // Return if there is an error
return _URC_FATAL_PHASE2_ERROR;
default: return _URC_FATAL_PHASE2_ERROR; // Unknown result code
}
}
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;
return unwind_phase2(&uc, &ctx, obj);
}
显然这个过程是可以在一次遍历情况下完成的,之所以要遍历两次,主要是为了在没有任何 catch 能处理异常的情况下,避免过早做真正的栈展开。也就是说,在搜索阶段没有找到任何可以处理异常的栈帧时,运行时就能更早终止程序。
Level 2: C++ ABI
在 Level 1 的基础上,定义了 __cxa_* API(例如 __cxa_allocate_exception、__cxa_throw、__cxa_begin_catch、__cxa_end_catch 等),以及如何通过这些 API,实现 C++ 的 throw / catch 语法。
__cxa_exception
__cxa_exception 是在 _Unwind_Exception 的基础上,再补充一层 C++ 异常语义信息的结构。
struct __cxa_exception {
void *reserve; // here on 64-bit platforms
size_t referenceCount; // here on 64-bit platforms
std::type_info *exceptionType;
void (*exceptionDestructor)(void *);
unexpected_handler unexpectedHandler; // by default std::get_unexpected()
terminate_handler terminateHandler; // by default std::get_terminate()
__cxa_exception *nextException; // linked to the next exception on the thread stack
int handlerCount; // incremented in __cxa_begin_catch, decremented in __cxa_end_catch, negated in __cxa_rethrow; last non-dependent performs the clean
// The following fields cache information the catch handler found in phase 1.
int handlerSwitchValue; // ttypeIndex in libc++abi
const char *actionRecord;
const char *languageSpecificData;
void *catchTemp; // landingPad
void *adjustedPtr; // adjusted pointer of the exception object
_Unwind_Exception unwindHeader;
};
每个线程都会维护一个当前被捕获异常的栈,caughtExceptions 指向栈顶,也就是最近一次被捕获的异常,__cxa_exception::nextException 则指向栈里的下一个异常。
struct __cxa_eh_globals {
__cxa_exception *caughtExceptions;
unsigned uncaughtExceptions;
};
int main() {
try {
throw 1;
} catch (...) {
try {
throw 2;
} catch (...) {
// The global exception stack has two exceptions here.
}
}
}
而具体处理异常所需的信息,例如某个 IP 指令寄存器是否位于 try-catch 范围内、是否存在需要执行的离开作用域变量析构等,通常放在 language-specific data area(LSDA)里。这部分属于具体实现细节,不是 Level 2 ABI 直接规定的内容。
LSDA 也就是 ELF 中的
.gcc_except_table,我们在下一篇再详细展开。
Landing pad
landing pad 由编译器生成,是一段专门用于异常处理的代码。它通常会完成以下三种动作之一(注意每个栈帧只会执行其中一种):
- 无法捕获对应异常,调用已离开作用域变量的析构函数,或者调用通过
__attribute__((cleanup(...)))注册的回调,然后使用_Unwind_Resume回到清理阶段 - 能捕获对应异常,先析构已离开作用域的变量,再调用
__cxa_begin_catch,执行catch块里的代码,最后调用__cxa_end_catch - 如果在
catch中有rethrow,则会先析构catch子句里定义的局部变量,再调用__cxa_end_catch,然后通过_Unwind_Resume继续清理阶段
如果一个 try 块后面跟着多个 catch 子句,那么 LSDA 中会有多条 catch 条目。不过在代码生成层面,它们通常会汇总到同一个 landing pad 中。personality 在把控制权转交给 landing pad 之前,会调用 _Unwind_SetGP,把 handlerSwitchValue 放进 __builtin_eh_return_data_regno(1) 对应的寄存器里(x86_64 下是 %rdx),用来告诉 landing pad 这次匹配到的是哪个类型异常,从而跳转到对应的 catch 块。
rethrow 则是在 catch 代码执行过程中通过 __cxa_rethrow 触发的。它需要先析构 catch 子句里定义的局部变量,再调用 __cxa_end_catch,抵消 catch 开始时那次 __cxa_begin_catch。
API
-
__cxa_allocate_exception:当代码里出现throw A();时,编译器生成的代码会调用这个构造函数,分配一块内存来存放__cxa_exception和A对象。其中__cxa_exception就紧挨在A对象的左侧。下面这个函数展示了程序可见的异常对象地址和__cxa_exception之间的关系:static void *thrown_object_from_cxa_exception(__cxa_exception *exception_header) { return static_cast<void *>(exception_header + 1); // address of A }注意
__cxa_exception是在堆上创建的。运行时通常还会在启动时预留一小块内存,并预先构造一个std::bad_alloc,以便在内存分配失败时仍然能够抛出异常。 __cxa_throw:先根据上面的关系找到__cxa_exception,填好其中各个字段(referenceCount、exception_class、unexpectedHandler、terminateHandler、exceptionType、exceptionDestructor、unwindHeader.exception_cleanup),然后调用_Unwind_RaiseException开始栈展开__cxa_begin_catch:编译器会在catch块开头生成对它的调用。主要作用是更新__cxa_exception中的handlerCount,更新当前线程的全局异常栈,返回被抛出对象的地址__cxa_end_catch:编译器会在catch块结束处,或者在rethrow前生成对它的调用。主要作用是更新__cxa_exception中的handlerCount,如果为0,则从全局异常栈出栈。__cxa_rethrow:它会给异常对象打上“重新抛出”的标记。这样当__cxa_end_catch把handlerCount减到 0 时,这个异常对象不会被销毁,因为后续_Unwind_Resume恢复清理阶段时还要继续使用它
Level 2 的 API 主要都是为了提供 C++ 的各种语法底层支持,除了基础的 throw / catch 之外,还包括 std::current_exception、std::rethrow_exception、std::get_terminate 等。下面是一个简化版的 __cxa_throw 实现:
void __cxa_throw(void *thrown, std::type_info *tinfo, void (*destructor)(void *)) {
__cxa_exception *hdr = (__cxa_exception *)thrown - 1;
hdr->exceptionType = tinfo; hdr->destructor = destructor;
hdr->unexpectedHandler = std::get_unexpected();
hdr->terminateHandler = std::get_terminate();
hdr->unwindHeader.exception_class = ...;
__cxa_get_globals()->uncaughtExceptions++;
_Unwind_RaiseException(&hdr->unwindHeader);
// Failed to unwind, e.g. the .eh_frame FDE is absent.
__cxa_begin_catch(&hdr->unwindHeader);
std::terminate();
}
Example
下面结合一个例子,再梳理一遍整个异常处理流程。
struct A {
~A() {}
};
void baz() {
throw 1;
}
void bar() {
A a;
baz();
}
void foo() {
try {
bar();
} catch (int x) {
x++;
}
}
对应的汇编伪代码大致如下:
void baz() {
__cxa_exception *thrown = __cxa_allocate_exception(sizeof(int));
*thrown = 1;
__cxa_throw(thrown, &typeid(int), nullptr/*destructor*/);
}
void bar() {
A a;
baz();
return;
landing_pad:
a.~A();
_Unwind_Resume();
}
void foo() {
bar();
return;
landing_pad:
__cxa_begin_catch(obj);
x++;
__cxa_end_catch(obj);
}
控制流可以概括成下面几步:
foo调用bar,bar调用baz,baz抛出异常baz动态分配一块内存,这块内存里依次保存一个__cxa_exception对象和被抛出的int,然后执行__cxa_throw__cxa_throw会设置__cxa_exception中的字段,然后调用_Unwind_RaiseException
_Unwind_RaiseException 开始执行栈展开。第一阶段要先搜索能够捕获 int 异常的栈帧:
- 对
bar来说,传入_UA_SEARCH_PHASE调用personality;返回值是_URC_CONTINUE_UNWIND,表示这里不能捕获该异常 - 对
foo来说,传入_UA_SEARCH_PHASE调用personality;返回值是_URC_HANDLER_FOUND,表示这里能捕获该异常 foo这个栈帧的栈指针会被记录下来,存入private_2,然后搜索阶段结束
此时已经确定 foo 的栈帧可以接住这个异常,第二阶段开始做清理:
bar的栈帧没有被搜索阶段标记,传入_UA_CLEANUP_PHASE调用personality,返回_URC_INSTALL_CONTEXT,代表有landing pad- 跳转到
bar栈帧对应的landing pad,完成清理后,通过_Unwind_Resume回到清理阶段 foo的栈帧在搜索阶段已经被标记,传入_UA_CLEANUP_PHASE | _UA_HANDLER_FRAME调用personality时,返回_URC_INSTALL_CONTEXT,代表有landing pad- 跳转到
foo栈帧对应的landing pad,其中调用__cxa_begin_catch,执行catch代码,最后调用__cxa_end_catch
完整的汇编如下,可以对照加深理解(重点关注 bar 和 foo 的 landing pad):
A::~A() [base object destructor]:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
nop
popq %rbp
ret
.set A::~A() [complete object destructor],A::~A() [base object destructor]
baz():
pushq %rbp
movq %rsp, %rbp
movl $4, %edi
call __cxa_allocate_exception
movl $1, (%rax)
movl $0, %edx
movl $_ZTIi, %esi
movq %rax, %rdi
call __cxa_throw
bar():
pushq %rbp
movq %rsp, %rbp
pushq %rbx
subq $24, %rsp
call baz()
leaq -17(%rbp), %rax
movq %rax, %rdi
call A::~A() [complete object destructor]
jmp .L6
; landing pad of bar
movq %rax, %rbx
leaq -17(%rbp), %rax
movq %rax, %rdi
call A::~A() [complete object destructor]
movq %rbx, %rax
movq %rax, %rdi
call _Unwind_Resume
.L6:
movq -8(%rbp), %rbx
leave
ret
foo():
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
call bar()
jmp .L12
; landing pad of foo
cmpq $1, %rdx
je .L9
movq %rax, %rdi
call _Unwind_Resume
.L9:
; catch block in foo
movq %rax, %rdi
call __cxa_begin_catch
movl (%rax), %eax
movl %eax, -4(%rbp)
addl $1, -4(%rbp)
call __cxa_end_catch
.L12:
nop
leave
ret
这里详细分析下 bar 和 foo 的 landing pad:
对于 bar ,在跳转到对应的 landing pad 之前,_Unwind_RaiseException 已经通过 personality 确定了 bar 不能处理这个异常,因此它的 landing pad 就是清理栈上的对象,然后调用 _Unwind_Resume 继续栈展开。
; landing pad of bar
movq %rax, %rbx
leaq -17(%rbp), %rax
movq %rax, %rdi
call A::~A() [complete object destructor]
movq %rbx, %rax
movq %rax, %rdi
call _Unwind_Resume
对于 foo,在跳转到对应的 landing pad 之前,_Unwind_RaiseException 已经通过 personality 确定了 foo 能处理这个异常,并且知道是第几个 catch 块与之匹配。相关信息会通过下面两个寄存器传给 landing pad:
; %rax -> exception object,后续会传给 __cxa_begin_catch
; %rdx -> 类型匹配结果
通过比对 %rdx,跳转到对应的 catch 块。__cxa_begin_catch 会返回被抛出对象的地址,也就是 catch 块里 x 对应的地址。执行 x++ 之后,最后调用 __cxa_end_catch 完成这次异常捕获。
注意,
%rax里已经保存了抛出的异常对象。__cxa_begin_catch之所以还要再返回一次对象地址,是因为这里可能需要做一次地址调整。
; landing pad of foo
; 确定foo能处理当前异常 通过比较%rdx 跳转到对应的catch block进行处理
cmpq $1, %rdx
je .L9 ; go to catch(int)
; 不能catch当前异常 继续调用_Unwind_Resume
movq %rax, %rdi
call _Unwind_Resume
.L9:
; catch block in foo
movq %rax, %rdi
call __cxa_begin_catch
movl (%rax), %eax
movl %eax, -4(%rbp)
addl $1, -4(%rbp) ; x++
call __cxa_end_catch
Misc
最后再补充一些零碎的信息。
Native exception vs Foreign exception
前面提到 _Unwind_Exception 中有个 exception_class 字段,
Level 1 API 不会处理该字段,而是将其原样传给 personality,personality 利用这个值来区分 native exception 和 foreign exception:
native exception: 由相同C++ ABI运行时抛出的异常。即包含完整的C++类型信息(RTTI),可以被C++运行时正确地栈展开,从而进行捕获。foreign exceptions: 非C++代码产生,不遵循C++ ABI异常处理规范,只能被catch (...)捕获。
之所以要强调相同C++ ABI运行时的一个典型例子是:
libstdc++抛出的异常会被libc++abi视为foreign exception。
__cxa_begin_catch 和 __cxa_end_catch 对于 native exception 以及 foreign exception 有不同的处理方式:
void* __cxa_begin_catch(void *obj) 编译器会在 catch 块开头生成对它的调用。对于:
native exception- 增加
handlerCount - 将异常压入当前线程的全局异常栈,并减少
uncaught_exception计数 - 返回调整后的异常对象的地址指针
- 增加
foreign exception(不一定有__cxa_exception头部)- 若当前线程的全局异常栈为空则压栈,否则调用
std::terminate(在任意时刻,C++ ABI运行时只能处理一个foreign exception) - 返回
static_cast<_Unwind_Exception *>(obj) + 1(假设_Unwind_Exception紧邻被抛出对象)
- 若当前线程的全局异常栈为空则压栈,否则调用
void __cxa_end_catch() 在 catch 块结束或 rethrow 时被调用。对于:
native exception- 从当前线程的全局异常栈中取出异常,减少
handlerCount - 当
handlerCount减至 0 时(引用计数为 0),将其从全局异常栈中出栈 - 当
handlerCount减至 0 时调用__cxa_free_exception(若为 dependent exception,则减少referenceCount,待其降至 0 时再调用__cxa_free_exception)
- 从当前线程的全局异常栈中取出异常,减少
foreign exception- 调用
_Unwind_DeleteException - 执行
__cxa_eh_globals::uncaughtExceptions = nullptr;(和__cxa_begin_catch时对应,栈中只有一个异常)
- 调用
注意,除
__cxa_begin_catch和__cxa_end_catch之外,大多数__cxa_*函数都无法处理foreign exception(因为它们没有__cxa_exception头部)。
这一篇到这就差不多了,主要以了解异常处理和栈展开的流程为主。下一篇将从 personality 开始,详细描述栈展开的原理。
Reference
-
[C++ exception handling ABI MaskRay](https://maskray.me/blog/2020-12-12-c++-exception-handling-abi) - CppCon 2017: Dave Watson “C++ Exceptions and Stack Unwinding”