Itanium C++ ABI 的栈展开是编译器生成元数据与运行时库协同的被动回溯过程,依赖.eh_frame、.gcc_except_table和type_info三类元数据,由personality routine决策、libunwind执行回溯与跳转,析构函数抛异常将触发std::terminate。

Itanium C++ ABI 的栈展开不是靠“跳转”实现的,而是靠编译器生成的元数据 + 运行时库协同查表驱动的被动回溯过程。
栈展开依赖的三类关键元数据(.eh_frame / .gcc_except_table)
编译器在生成目标文件时,会把每个函数的异常处理信息写入特定段:
-
.eh_frame:标准 DWARF 风格帧信息,描述函数栈帧布局、寄存器保存位置、CFA(Canonical Frame Address)计算规则 -
.gcc_except_table(或.text中嵌入的 LEB128 编码表):记录该函数内所有try块范围、catch类型信息、对应personality routine入口地址 - 异常对象类型信息(
type_info指针)与std::exception对象内存布局,供运行时做类型匹配
这些数据不参与正常执行,只在 throw 触发后被 libunwind 或 libgcc 读取并解析。
personality routine 是栈展开的决策中枢
每个函数的异常表中都指定了一个 personality routine(如 __gxx_personality_v0),它在栈展开每一步都被调用,决定:
立即学习“C++免费学习笔记(深入)”;
- 当前栈帧是否包含能捕获该异常的
catch块(类型匹配 + 作用域覆盖) - 是否需要执行该帧内的局部对象析构(即是否触发栈展开清理)
- 是否继续向上回溯,还是停止并跳转到
catch块入口
它不直接“销毁对象”,只是告诉运行时:“这个帧里有 Guard 对象,构造完成了,可以安全析构”,然后运行时调用其析构函数。
libunwind 负责实际的栈帧回溯与控制流切换
当 personality routine 返回“继续展开”时,libunwind 开始干活:
- 根据
.eh_frame计算上一帧的 CFA 和返回地址 - 恢复上一帧的寄存器状态(包括 SP、FP、可能被保存的通用寄存器)
- 在每一帧退出前,调用该帧中所有已构造、未析构对象的析构函数(按逆序)
- 最终,当
personality routine返回“找到 handler”时,libunwind直接将 PC 设置为catch块起始地址,跳过中间所有函数的 return 指令
注意:libunwind 不解析 C++ 语义,它只做机械的栈操作;析构逻辑仍由编译器生成的代码完成。
noexcept 函数和析构函数抛异常会破坏整个流程
如果在栈展开过程中,某个析构函数(或 noexcept 函数内)意外抛出异常,__cxa_call_unexpected 会被触发,最终调用 std::terminate() —— 因为此时运行时已处于“正在展开”的敏感状态,无法安全启动第二轮展开。
- 所有析构函数默认是
noexcept(true),显式声明noexcept(false)并抛异常 = 终止程序 - 编译器对
noexcept函数的异常表会省略catch描述,personality routine会直接拒绝处理其内部异常 - 这意味着:RAII 安全的前提,是析构函数绝对不能抛异常
真正容易被忽略的是:栈展开不是“自动的魔法”,它是建立在精确的元数据、严格的析构契约、以及运行时库不报错协作之上的脆弱链条。少一个 .eh_frame 段、错一个 personality 地址、或一个 throw 在析构里——整个展开就塌了。



















