DWARF段解析
为什么ELF中需要这一段
从一次段错误说起
想像你正在写这样一段 C 代码:
1 | |
编译运行:
1 | |
用 gdb 调试:
1 | |
问题来了:
- gdb 为什么知道崩溃地址 0x000055555555513d 对应的是 hello.c 第 3 行?
- 更进一步,
bt怎么把栈回溯成人类可读的函数名、行号、参数?
“-g” 背后的故事
把源码变成可执行文件,通常经历:
1 | |
当你加 -g 时,编译器在生成机器码的同时,会额外产出一张“地图”:
- 地址 ↔ 行号(行表)
- 地址 ↔ 函数、变量、类型(DIE 树)
- 栈帧如何回退(CFI)
- 宏、内联、模板实例化……
这些调试信息被塞进一组以 .debug_ 开头的 ELF section 里,格式规范就是 DWARF(Debugging With Attributed Record Formats)。
DWARF 名称的来历与版本简史
- 最早在 1988 年由 Bell Labs 的 UNIX 系统引入,用来取代 stabs 格式。
- 名字借用了《白雪公主》里七个小矮人(Dwarf),暗示“小巧却功能齐全”。
- 版本演进:
DWARF1(1992) → DWARF2(1993,ELF 通用) → DWARF3(2006,支持更多语言) → DWARF4(2010,压缩/类型单元) → DWARF5(2017,索引、字符串池、Split-DWARF)。 - 今天 Linux、*BSD、macOS、甚至 WebAssembly 上,DWARF 都是事实标准。
ELF 视角:DWARF 在可执行文件中的存在形态
在 ELF 的语境下,DWARF 调试信息并非以单一连续区域出现,而是以一组具有严格命名约定的 section 为核心载体。
section 的枚举
使用 GNU binutils 提供的 readelf 工具,可直接枚举目标文件的所有 section:
1 | |
上述输出表明,DWARF 调试信息由以 .debug_ 为前缀的若干 PROGBITS 类型 section 承载。每个 section 在文件中有独立的偏移与长度,于链接阶段可被合并或拆分,但彼此逻辑上紧密关联。
section 与 segment 的区分
在 ELF 规范中,section 用于描述“链接视图”,而 segment(程序头部表项,Program Header)描述“执行视图”。调试信息 section 通常带有 SHF_ALLOC 以外的标志,因此默认不会被映射到进程地址空间。换言之:
- section 是静态分析(链接、调试)的粒度;
- segment 是运行时(加载、执行)的粒度。
除非显式使用 --build-id 与 .note.gnu.build-id 机制,将调试信息单独存放并通过 .gnu_debuglink 引用,否则生产环境的可执行文件往往通过 strip 移除 .debug_* section,以减小体积。
DWARF 语法与语义详解
debug_abbrev
该段的作用
.debug_abbrev 为 .debug_info 中所有调试信息条目(DIE)提供“模板”。它只存放结构:每个 DIE 属于什么类型(tag)、是否包含子节点、拥有哪些属性、这些属性以何种数据格式(form)存放。如此可避免在 .debug_info 中重复描述字段。
示例程序
1 | |
编译命令:
1 | |
解析命令和解析结果
1 | |
输出:
1 | |
分析
“Number 1” 对应编译单元(DW_TAG_compile_unit)。
- “has children” 表明它下面会挂子 DIE(例如函数、变量)。
- 属性列表给出了编译器、语言类型、源文件名、目录、代码起止地址、行号表偏移等。
“Number 2” 对应子程序(DW_TAG_subprogram),即函数 add。
- 属性提供了函数名、声明文件/行/列、函数签名、返回类型、函数体地址范围以及计算帧基址的表达式。
.debug_abbrev 不直接存放上述字符串或地址,而是规定“此处应有一个 DW_FORM_strp 的字符串,此处应有一个 DW_FORM_addr 的地址”,真正的数据全部放在 .debug_info 中,解析器只需按“模板”顺序读取即可。
debug_info
该段的作用
.debug_info 存放构成调试信息树的所有 DIE(Debugging Information Entry)。每个 DIE 由 abbrev code 指向 .debug_abbrev 中的模板,随后按模板给出的属性顺序和格式,依次存储属性值。通过这些 DIE,调试器可在运行时把机器地址映射到源文件位置、变量名、类型描述及作用域层级。
示例程序
同上
解析命令和解析结果
1 | |
输出:
1 | |
分析
偏移 0x0 的 DIE 对应编译单元(DW_TAG_compile_unit)。
- 通过 abbrev 1 可知其属性顺序:DW_AT_producer 给出编译器版本;DW_AT_language 标识语言为 C99;DW_AT_name 与 DW_AT_comp_dir 共同确定了源文件完整路径;DW_AT_low_pc / DW_AT_high_pc 描述本 CU 机器码地址范围(此处为 0x0–0x18);DW_AT_stmt_list 指向 .debug_line 的行号表。
偏移 0x2d 的 DIE 对应函数 add(DW_TAG_subprogram)。
- abbrev 2 指定其属性:
• DW_AT_name 指向字符串表中的 “add”;
• DW_AT_decl_file / DW_AT_decl_line 指出函数定义在 demo.c 第 1 行;
• DW_AT_prototyped = 1 表示函数有原型;
• DW_AT_type = <0x6a> 表示返回值类型 DIE 位于同一 CU 偏移 0x6a;
• DW_AT_low_pc / DW_AT_high_pc 给出函数体机器码范围 0x0–018;
• DW_AT_frame_base 在 DWARF 操作码中,0x9c 对应 DW_OP_call_frame_cfa,说明使用调用帧的CFA作为帧基址。
- abbrev 2 指定其属性:
偏移 0x4f 的 DIE 对应形参 a(DW_TAG_formal_parameter)。
- abbrev 3 指定属性:
• DW_AT_name = “a”;
• DW_AT_decl_file / DW_AT_decl_line 指出其在 demo.c 第 1 行;
• DW_AT_type = <0x6a> 指向返回值类型 DIE;
• DW_AT_location 使用 DW_OP_fbreg 操作码,表示该参数在帧基址下偏移 -20 字节。
- abbrev 3 指定属性:
debug_line
该段的作用
.debug_line 以状态机方式记录“机器地址 ↔ 源文件行列”的精确对应关系,并提供语句边界、基本块、函数序言/结尾等标记。调试器借此在断点、崩溃或单步时,将任意指令地址转换为人类可读的源位置,反之亦然。
示例程序
同上
解析命令和解析结果
1 | |
典型输出(节选,行号→地址):
1 | |
分析
- 第一条记录:demo.c 第 1 列 0 → 地址 0x0,is_stmt 标记说明这是“推荐断点”位置(函数入口)。
- 第二条记录:demo.c 第 2 列 0xe → 地址 0xe,is_stmt 标记说明这是“推荐断点”位置(函数体开始)。
- 第三条记录:demo.c 第 3 列 0x16 → 地址 0x16,is_stmt 标记说明这是“推荐断点”位置(函数体结束)。
- 第四条记录:demo.c 第 3 列 - → 地址 0x18,is_stmt 标记为 false,表示这是函数结尾(return 语句)位置,不建议设置断点。
debug_frame / eh_frame
该段的作用
.debug_frame 与 .eh_frame 以统一的 CIE/FDE 表格式描述“如何在运行时从任意指令地址回退到上一栈帧”。
• 调试器:崩溃时生成回溯 (backtrace)。
• 采样剖析器:如 perf,依赖 CFI 做无帧指针回溯。
• 异常机制:C++ throw 或 Rust panic 展开栈帧,同样复用 .eh_frame。
解析命令和解析结果
1 | |
典型输出(节选):
1 | |
分析
CIE(Common Information Entry)
- 起始地址 0x0,长度 0x14,提供公共规则:
- 帧基址寄存器为 rbp,偏移 16;
- 返回地址保存于偏移 -16(rbp)。
- 起始地址 0x0,长度 0x14,提供公共规则:
FDE(Frame Description Entry)
– 起始地址 0x18,长度 0x10,关联同一 CIE;
– pc 范围 0x0–018 恰好覆盖 add 函数全部指令;
– 在函数序言后 4 字节处,通过 DW_CFA_def_cfa rsp, 8 将帧基址切换为 rsp+8,实现无帧指针回溯。
调试器或异常运行时,若采样到地址 0x7(位于 add 函数中部),即可按上述规则恢复前一栈帧的 rbp、rip,从而生成可信的回溯链。
debug_loc —— 变量位置列表(Location Lists)
该段的作用
.debug_loc 为同一变量在不同代码区间提供寄存器位置信息。每条记录包含起始地址、结束地址及对应的位置表达式,调试器据此在单步或回溯时显示正确的变量值。
示例程序
1 | |
编译:
1 | |
解析命令和解析结果
1 | |
输出:
1 | |
分析
- 第一条记录:
- 起始地址 0x4,结束地址 0x8,表示 a 在寄存器 rdi(edi)。
- 这意味着在函数 foo 的前半段,a 的值直接存储在寄存器中。
- 第二条记录:
- 起始地址 0x7,结束地址 0x8,表示 a 在栈 [rbp-4]。
- 这意味着在函数 foo 的后半段,由于编译器优化,a 的值被溢出到栈上,而不再使用寄存器 edi。
- 第三条记录:
- 起始地址 0x7,结束地址 0x8,表示 b 在寄存器 rax(eax)。
- 这意味着在函数 foo 的后半段,b 的值被计算并存储在寄存器中。
调试器在地址 0x7 处暂停时,会按第二条记录到栈 [rbp-4] 读取 a,而不是继续去寄存器 edi;若用户尝试打印超出 0xf 后的 a,调试器会报告“变量已被优化掉”。
DIE 类型详解
第一类:作用域与容器类
1. DW_TAG_compile_unit
- 描述整个翻译单元(一个 .c/.cpp 文件及其包含的所有代码与数据)。包含语言类型、编译器版本、源码路径、低/高 PC 范围、行号表偏移等全局信息。
- 例子:
1 | |
- 对应 DIE 片段:
1 | |
2. DW_TAG_subprogram
- 描述一个函数或成员函数,记录函数名、返回值类型、入口地址范围、帧基址计算方式、形参列表等。
- 例子:
1 | |
- 对应 DIE 片段:
1 | |
3. DW_TAG_inlined_subroutine
- 描述被内联展开的函数实例,保留其原函数名、调用点文件/行号、内联后的地址范围,用于调试器正确回溯内联代码。
- 例子:
1 | |
- 对应 DIE 片段:
1 | |
4. DW_TAG_structure_type
- 描述 C 结构体或 C++ POD 结构,记录名称、字节大小、成员列表与每个成员的偏移。
- 例子:
1 | |
- 对应 DIE 片段:
1 | |
5. DW_TAG_class_type
- 描述 C++ 类(含成员函数、继承信息等)。与 DW_TAG_structure_type 类似,但可附加 DW_TAG_inheritance、DW_TAG_subprogram 子 DIE。
- 例子:
1 | |
- 对应 DIE 片段:
1 | |
6. DW_TAG_union_type
- 描述联合体,列出各成员及其在联合体中的起始偏移(均为 0)。
- 例子:
1 | |
- 对应 DIE 片段:
1 | |
第二类:数据对象类
1. DW_TAG_variable
- 描述全局或静态变量、常量、线程局部变量等。记录变量名、类型、作用域、存储位置(寄存器、栈偏移、绝对地址等)。
- 例子:
1 | |
DW_TAG_variable
DW_AT_name ("counter")
DW_AT_type (reference to DW_TAG_base_type int)
DW_AT_location (DW_OP_addr 0x2000)
DW_AT_linkage_name ("counter")
1
2
3
4
5
6
7
8
#### 2. DW_TAG_formal_parameter
- 描述函数形参。记录参数名、类型、所在寄存器或栈偏移。
- 例子:
```c
int add(int a, int b) { return a + b; }
```
- 对应 DIE 片段:
DW_TAG_subprogram "add"
...
DW_TAG_formal_parameter
DW_AT_name ("a")
DW_AT_type (reference to DW_TAG_base_type int)
DW_AT_location (DW_OP_reg5) ; edi
DW_TAG_formal_parameter
DW_AT_name ("b")
DW_AT_type (reference to DW_TAG_base_type int)
DW_AT_location (DW_OP_reg4) ; esi
1
2
3
4
5
6
7
8
#### 3. DW_TAG_enumerator
- 描述枚举中的单个常量成员。记录常量名及其数值。
- 例子:
```c
enum Color { RED = 0, GREEN = 1, BLUE = 2 };
```
- 对应 DIE 片段:
DW_TAG_enumeration_type "Color"
...
DW_TAG_enumerator
DW_AT_name ("RED")
DW_AT_const_value (0)
DW_TAG_enumerator
DW_AT_name ("GREEN")
DW_AT_const_value (1)
DW_TAG_enumerator
DW_AT_name ("BLUE")
DW_AT_const_value (2)
1
2
3
4
5
6
7
8
9
10
### 第三类:类型描述类
#### 1. DW_TAG_typedef
- 为已有类型定义新的别名,记录别名名、底层类型。
- 例子:
```c
typedef unsigned int uint32_t;
```
- 对应 DIE 片段:
DW_TAG_typedef
DW_AT_name ("uint32_t")
DW_AT_type (reference to DW_TAG_base_type "unsigned int")
1
2
3
4
5
6
7
8
#### 2. DW_TAG_pointer_type
- 描述指针类型本身,记录指针大小及所指向的类型。
- 例子:
```c
int *p;
```
- 对应 DIE 片段:
DW_TAG_pointer_type
DW_AT_byte_size (8)
DW_AT_type (reference to DW_TAG_base_type int)
1
2
3
4
5
6
7
8
#### 3. DW_TAG_array_type
- 描述数组类型,记录元素类型及维度信息(通过 DW_TAG_subrange_type 子 DIE)。
- 例子:
```c
int arr[4];
```
- 对应 DIE 片段:
DW_TAG_array_type
DW_AT_type (reference to DW_TAG_base_type int)
DW_TAG_subrange_type
DW_AT_upper_bound (3)
1
2
3
4
5
6
7
8
#### 4. DW_TAG_subrange_type
- 描述数组维度或切片范围,给出上下界。
- 例子:
```c
int matrix[2][3];
```
- 对应 DIE 片段(第二维):
DW_TAG_subrange_type
DW_AT_upper_bound (2)
1
2
3
4
5
6
7
8
#### 5. DW_TAG_enumeration_type
- 描述枚举类型本身,记录名称、底层整数类型及所有枚举常量列表。
- 例子:
```c
enum Status { OK = 0, ERROR = 1 };
```
- 对应 DIE 片段:
DW_TAG_enumeration_type
DW_AT_name ("Status")
DW_AT_type (reference to DW_TAG_base_type "int")
DW_TAG_enumerator ("OK") DW_AT_const_value (0)
DW_TAG_enumerator ("ERROR") DW_AT_const_value (1)
```