其实在此之前我就大概知道,stack unwinding 是反向回溯栈的一种机制,在 throw 了什么东西后一层层回溯栈,一层层析构掉这些栈上的 RAII 对象,直到找到 try catch 块后跳转到 catch 块。
但是问题是这种事情是怎样实现的?多说无益,我们直接从 throw 的源码入手分析。
首先我们编写一段示例代码:
1 |
|
编译后使用 objdump -d 看 func2 的内容,发现 throw 被编译成了 __cxa_throw 调用:
__cxa_throw
__cxa_throw 位于 llvm 的 libcxxabi 子项目中:
1 | void |
能看出来其核心实现在 _Unwind_RaiseException
_Unwind_RaiseException
在 libunwind 子项目中找到其实现:
1 | /// Called by __cxa_throw. Only returns if there is a fatal error. |
这里我们可以发现有两个 phase:
search phase
1 | static _Unwind_Reason_Code |
向下找一个栈帧
通过查表(
.eh_frame_hdr)获取这个栈帧的 FDE 头
search phase 比较好理解,一次只往下找一个栈,如果找到了 catch clause 或者需要析构的 RAII 对象,则会返回 _URC_NO_REASON,只有 search phase 返回 _URC_NO_REASON 才会继续向下走到 clean up phase。
__unw_step
1 | /// Move cursor to next frame. |
_unw_step 这个函数的实际逻辑最后定位到了 libunwind 的 stepWithDwarfFDE(这是 linux 上的实现)。
首先会从栈帧上拿到保存的 ip (instruction pointer,即函数的返回地址)
然后调用 DwarfInstructions::stepWithDwarf:
1 | template <typename A, typename R> |
看着虽然长,但有很多是在抹平指令集的平台差异,实际的逻辑还算比较清晰:
decodeFDE / parseFDEInstructions读 FDE (Frame Description Entry),解释执行 FDE 中的 DWARF 字节码,得到指定 PC 位置对应栈上保存的值的位置偏移量表

拿到栈底基准地址
根据位置偏移量表获取到栈上保存的寄存器的所有值,恢复到拷贝的寄存器状态中
设置 IP 为 returnAddress(跳到上一层函数)
切换寄存器状态 register 为 newRegisters
__unw_get_proc_info
1 | /// Get unwind info at cursor position in stack frame. |
这段代码主要是在查找 FDE 头:
如果有
fdeSectionOffsetHint,直接根据fdeSectionOffsetHint查找如果没有,利用
.eh_frame_hdr查找如果没有,则从缓存中查找,如果这个函数之前 unwind 过,则会被 libunwind 在内存中记录下来
如果缓存中仍然找不到,则全量扫描一遍
.eh_frame拿到后解析信息并将结果设置到缓存
get_handler_function
1 | struct unw_proc_info_t { |
从 FDE 头中拿到 handler 代码段,将其拷贝并返回,看起来 handler 代码段应该是固定长度,可以 alloc 在栈上。
clean up phase
1 | static _Unwind_Reason_Code |
向下遍历栈帧
获取当前栈帧信息 (
__unw_get_proc_info)如果当前栈帧有 handler(即有 catch clause 或需要析构的 RAII 对象),则执行这个 handler
如果是 catch 代码块,会直接通过修改 IP 跳转到 catch 块,所以函数不会返回。
如果是析构 RAII object,也会通过
__unw_phase2_resume跳转到 landing pad,执行析构函数后调用_Unwind_Resume跳回 Unwinder。
__unw_step_stage2
跟前面 _unw_step 其实是一样的,只是 arm64 架构会有特殊处理。
__unw_phase2_resume
1 | #define __unw_phase2_resume(cursor, payload) \ |
__unw_phase2_resume 是一个宏,会跳到一个 trampoline,trampoline 会把 x0 中保存的寄存器状态(即 shstkRegContext)恢复到寄存器(包括 ip),此时就算成功跳转到 Landing Pad 了。
Landing pad
Landing pad 中执行完析构函数后,会调用 _Unwind_Resume 跳转回 unwind 逻辑。
1 | _LIBUNWIND_EXPORT void |
其实就是重新调用了一下 unwind_phase2 ,因为 unwind_phase2 里面是一个死循环的结构,所以恢复只需要再次调用就可以了,想到了 Kotlin 协程,思路挺像的。
至于参数就获取当前的寄存器快照就可以了,当前的sp其实就在当前 Landing pad 对应栈帧的下面一个栈帧,是 Landing pad 这个函数自己开辟的。
将这个寄存器快照传给 unwind_phase2,它所做的第一件事就是往上找一层,就找到了当前 Landing pad 对应的栈帧。
unwind_phase2 是通过 IP 来查找对应的 handler 的,当从 Landing pad 调用 unwind_phase2 时,IP 已经位于函数的末尾(Landing pad 就位于函数的末尾),所以会直接放行,不会查到第一次进入时 handler。
AI 生成的流程图
