Appearance
.eh_frame / .eh_frame_hdr / .gcc_except_table —— 异常处理帧
C++ 的
throw/catch为什么能在栈上找到正确的catch块?gdb bt为什么能在无 frame pointer(-fomit-frame-pointer)的二进制里照样回溯调用栈?perf record --call-graph dwarf靠什么拿到调用链?答案在.eh_frame(Exception Handling Frame)和.gcc_except_table里——它们是"零成本异常处理"(Zero-Cost Exception Handling)的基础设施,但它们的用途远不止异常处理:栈回溯(unwind)、backtrace、perf DWARF 调用链、.so里的dl_iterate_phdr展开,全部共享同一套 unwind 表。
更新时间:2026-08-06
一、术语前置
| 术语 | 含义 |
|---|---|
| EH | Exception Handling,异常处理 |
| CFI | Call Frame Information,调用帧信息——描述每个指令位置下寄存器和栈的状态 |
| CIE | Common Information Entry,公共信息条目——描述一段代码区的通用 unwind 信息 |
| FDE | Frame Description Entry,帧描述条目——描述一个具体函数的 unwind 信息 |
| LSDA | Language-Specific Data Area,语言相关数据区——指向 .gcc_except_table |
| DWARF CFI | DWARF 标准中的调用帧信息格式,.eh_frame 基于它 |
| Zero-Cost EH | 零成本异常处理——不抛异常时无额外指令开销,只在抛出时查表 |
二、为什么需要 unwind 表

-fomit-frame-pointer(-O1 及以上默认开启)释放了 rbp 寄存器,但代价是栈上没有 rbp 链条了。此时要回溯调用栈,必须有 .eh_frame 表——它记录了每个指令位置下"怎么从当前的栈指针和寄存器值反算出调用者的栈帧位置(CFA,Canonical Frame Address)"。
三、三节的关系与分布

| 节 | 内存加载? | 内容 | 谁在用 |
|---|---|---|---|
.eh_frame | 是(A 标志) | CIE + FDE 表,记录每个函数的 CFA 计算规则 | libgcc unwinder、gdb、perf |
.eh_frame_hdr | 是 | 二进制搜索索引,加速"给定 PC → 找到对应 FDE" | _Unwind_Find_FDE 二分查找 |
.gcc_except_table | 是 | LSDA:try/catch 区域编码、catch 类型列表、action 表 | __cxa_throw → 选 catch 块 |
.eh_frame在大型二进制里可能非常可观——几百 KB 级别,比.symtab还大。因为它为每个函数都生成了一个 FDE。
四、.eh_frame —— CIE 与 FDE
4.1 CIE(Common Information Entry)
CIE 描述一个"代码区"的通用信息——包括:
- 指令指针(IP/RIP)的初始寄存器(通常是当前 PC)
- CFA 的计算规则(通过哪个寄存器+偏移来表达)
- Code Alignment Factor / Data Alignment Factor
- Return Address Register
可以把 CIE 理解为"这个编译单元的模板",多个 FDE 共用一个 CIE。
4.2 FDE(Frame Description Entry)
每个 FDE 对应一个函数,包含:
- PC Begin / PC Range:函数起始地址和范围
- 指向其 CIE 的指针
- DWARF CFI 指令序列:描述在这个函数的每个指令位置下,如何根据当前寄存器恢复调用者的寄存器和 CFA
- LSDA 指针:如果有
try/catch,指向.gcc_except_table的对应区域
bash
.eh_frame 逻辑结构
┌──────────────────────────────┐
│ CIE #1 │
├──────────────────────────────┤
│ FDE for func_1 │ → PC=[0x4019a0, 0x401b00), LSDA=...
├──────────────────────────────┤
│ FDE for func_2 │ → PC=[0x401b00, 0x401c80), LSDA=0 (无异常)
├──────────────────────────────┤
│ FDE for func_3 │ → ...
└──────────────────────────────┘每个 FDE 的大小通常几十~几百字节。对于有大量 try/catch 的函数,FDE 内的 CFI 指令会更复杂。对简单函数(无分支、无异常),FDE 可能只有几条指令。
五、.eh_frame_hdr —— 加速查找索引
问题:catch 发生时,只知当前 RIP,要在 .eh_frame 的成百上千个 FDE 中找到覆盖这个 RIP 的 FDE。线性扫描太慢。
.eh_frame_hdr 是排序后的二分查找索引,结构如下:
bash
.eh_frame_hdr 布局
┌────────────────────────┐
│ version (1 byte) │
├────────────────────────┤
│ eh_frame_ptr_enc │ 编码方式
├────────────────────────┤
│ fde_count_enc │
├────────────────────────┤
│ table_enc │
├────────────────────────┤
│ eh_frame 节指针 │
├────────────────────────┤
│ FDE 数量 (N) │
├────────────────────────┤
│ 二分查找表 (N+1 项) │ → 每项是 {PC, FDE 偏移} 对
├────────────────────────┤
│ (padding) │
└────────────────────────┘_Unwind_Find_FDE(target_pc) 在这个表上做二分查找,O(log N) 找到对应的 FDE。
六、.gcc_except_table —— 语言相关数据区(LSDA)
.gcc_except_table 是 GCC 专用的"哪个地址范围属于哪个 catch"明细表。它被 .eh_frame 的 FDE 通过 LSDA 指针引用。
bash
$ readelf -S ./cpu_demo | grep except
[18] .gcc_except_table PROGBITS 0000000000406a08 00006a08
00000484 A 0 0 4里面有三种表:
| 子表 | 内容 |
|---|---|
| LPStart | Landing Pad(catch 入口)表:每个 catch 块的第一条指令地址 |
| TTBase | Type Table(类型表):RTTI 指针数组,描述 catch 的类型(const std::exception& 等) |
| Call Site Table | 调用点表:{try 起始 PC, try 结束 PC, landing pad 索引, action 记录索引} |
七、异常抛出全流程

关键点:
- 零成本:不抛异常时,
try块里没有任何额外的运行时开销。开销全在throw时——查表 + 逐帧展开。 - 展开过程:从 throw 位置开始,逐帧检查 FDE → 如果 LSDA 非空,用
.gcc_except_table匹配 catch 类型 → 匹配上了就跳到 landing pad;没匹配上就继续往上一层展开。 - 无 catch:展开到栈顶都没匹配 → 调
std::terminate()。
八、.eh_frame 的延申用途
.eh_frame 设计初衷是异常处理,但栈回溯(stack unwinding)是通用需求:
| 场景 | 如何使用 .eh_frame |
|---|---|
gdb bt(无 rbp) | 遍历 RIP 链,每个用 .eh_frame 反算 CFA 和调用者 |
perf record --call-graph dwarf | 采样 RIP 后,用 .eh_frame 生成完整调用链 |
backtrace()(glibc) | _Unwind_Backtrace 内部遍历 .eh_frame |
dl_iterate_phdr | 查找 .so 的 .eh_frame 地址 |
addr2line | 不需要 .eh_frame,它用 .debug_line(DWARF) |
注意:
.eh_frame能被 C 和汇编使用——即使非 C++ 代码,只要用了asm(".cfi_startproc")之类的 CFI 指令,汇编器就会生成.eh_frame。所以.eh_frame不是 C++ 专享。
九、实操速查
bash
# 看 .eh_frame 大小
readelf -S ./app | grep eh_frame
# 输出示例:[16] .eh_frame A 0x1000 → Size 看具体
# 用 eu-unstrip(elfutils)看 .eh_frame 更友好
eu-readelf --debug-dump=frames ./app | less
# 用 readelf 的 unwind 选项
readelf -u ./app | head -50
# 看 .gcc_except_table 大小
readelf -S ./app | grep gcc_except
# 编译时不生成异常处理表(减小体积,但 throw 不了)
g++ -fno-exceptions -fno-unwind-tables main.cpp十、与相关文档的关系
| 文档 | 覆盖内容 | 本文的关系 |
|---|---|---|
| elf-format.md §四/§五 | .eh_frame 的简要描述 | 本文是 .eh_frame 的专题深入 |
| symbol-table.md §五 | perf/gdb 如何使用符号表 | 本文解释 perf/gdb 如何用 .eh_frame 做栈回溯 |
| c++-abi.md | C++ 零成本异常处理模型 | 本文从 ELF 节角度解释 unwind 表的内部结构 |
| compile-link-load.md | 动态链接加载 .so | .so 的 .eh_frame 注册到全局 unwind 表 |
一句话总结:
.eh_frame是每个函数的"反推调用栈"说明书(通过 CIE/FDE 的 CFI 指令从寄存器状态恢复调用者的 CFA 和返回地址),.eh_frame_hdr是二分查找索引,.gcc_except_table是"哪个 try 块对应的 catch 在哪"。三者组合实现了零成本异常处理——不抛异常时零开销,抛出时通过查表逐帧展开。这套 unwind 基础设施同时被 gdb 栈回溯、perf DWARF 调用链、glibcbacktrace()共享。