基于DWARF 2规范及GDB 8.2源码实现编写
目录
DWARF概述与二进制布局 调试信息条目与属性 位置描述 行号与调用帧信息 GDB源码实现映射 x86实例:DWARF2调试全过程
1. DWARF概述与二进制布局
1.1 概述
DWARF(Debugging With Arbitrary Record Formats)是一种标准的调试信息格式,由编译器生成,供调试器使用。它将源代码级别的程序信息(变量、类型、函数、行号等)编码到目标文件的特定节(section)中,使得调试器能够在机器码和源代码之间建立映射。
版本演进:
.debug 节 | ||
| DWARF 2 | 1993 | 更紧凑的表示,信息移至 .debug_info;引入LEB128编码、栈式位置表达式 |
设计目标:语言无关性(核心调试信息与具体编程语言无关)、紧凑性(LEB128变长编码+缩写表)、可扩展性(允许添加新的标签/属性/操作码)。
1.2 二进制布局
DWARF调试信息分布在ELF文件的多个节中:
ELF文件├── .debug_info ← 核心调试信息(DIE树)├── .debug_abbrev ← 缩写表(DIE的"模板"定义)├── .debug_line ← 行号程序├── .debug_loc ← 位置列表├── .debug_str ← 间接字符串表├── .debug_ranges ← 地址范围列表(DWARF3+)├── .debug_frame ← 调用帧信息(CFI)├── .debug_aranges ← 地址范围查找加速表└── .eh_frame ← GCC扩展的CFI(加载时可用)每个 .debug_info 节包含一个或多个编译单元,每个编译单元以如下头部开始:
+------------------------+| unit_length | 4字节(DWARF32)或12字节(DWARF64)+------------------------+| version | 2字节(= 2 for DWARF2)+------------------------+| debug_abbrev_offset | 4字节,指向.debug_abbrev中的缩写表+------------------------+| address_size | 1字节,目标机地址宽度(如4=32bit, 8=64bit)+------------------------+| DIE data ... | 紧跟第一个DIEGDB中对应 struct comp_unit_head(dwarf2read.c),关键字段:length、version、addr_size、abbrev_sect_off、offset_size、initial_length_size。
DWARF2大量使用LEB128变长整数编码以节省空间:
ULEB128(无符号):每个字节最高位为延续标志(1=还有后续字节),低7位为有效数据,按小端序拼接 SLEB128(有符号):编码方式与ULEB128类似,最后一个字节的最高有效位为符号位
示例:编码 128(0x80)ULEB128: 0x80 0x01 (2字节) 第1字节: 1_0000000 → 延续位=1, 数据=0 第2字节: 0_0000001 → 延续位=0, 数据=1 解码:字节0低7位 × 2^0 + 字节1低7位 × 2^7 = 0 + 128 = 1282. 调试信息条目与属性
2.1 DIE与缩写表
DWARF调试信息以调试信息条目(Debugging Information Entry, DIE)为基本单位。每个DIE由三部分组成:
缩写码(Abbreviation Code):ULEB128编码,引用 .debug_abbrev中的缩写条目属性值:根据缩写条目中定义的form依次编码 子DIE(可选):如果缩写条目标记 has_children = DW_CHILDREN_yes
DIE通过父子关系和兄弟关系组成树形结构:
DW_TAG_compile_unit ← 根DIE├── DW_TAG_subprogram ← 子DIE (兄弟1)│ ├── DW_TAG_variable ← 子DIE│ ├── DW_TAG_variable ← 子DIE│ └── (null DIE) ← 子DIE终止├── DW_TAG_variable ← 子DIE (兄弟2)└── (null DIE) ← 子DIE终止核心DIE标签:
DW_TAG_compile_unit | ||
DW_TAG_subprogram | ||
DW_TAG_inlined_subroutine | ||
DW_TAG_variable | ||
DW_TAG_formal_parameter | ||
DW_TAG_lexical_block | ||
DW_TAG_base_type | ||
DW_TAG_pointer_type | ||
DW_TAG_structure_type | ||
DW_TAG_union_type | ||
DW_TAG_enumeration_type | ||
DW_TAG_typedef |
缩写表是DWARF2紧凑性的核心机制。它为每种DIE定义一个"模板",指明该DIE有哪些属性以及每个属性的编码形式(form),这样实际的DIE只需存储缩写码和属性值。.debug_abbrev 节布局:
+-----------------------------+| Abbreviation Code (ULEB128) | ← 缩写码 (1, 2, 3, ...)+-----------------------------+| Tag (ULEB128) | ← DW_TAG_*+-----------------------------+| Has Children (1 byte) | ← DW_CHILDREN_yes(1) / no(0)+-----------------------------+| Attr Name (ULEB128) | ← 属性对| Attr Form (ULEB128) |+-----------------------------+| 0, 0 | ← 属性对终止+-----------------------------+GDB使用 struct abbrev_info 和 struct abbrev_table 表示,通过 abbrev_table_read_table() 读取。
2.2 属性
常用属性一览:
DW_AT_name | |||
DW_AT_location | |||
DW_AT_type | |||
DW_AT_byte_size | |||
DW_AT_low_pcDW_AT_high_pc | |||
DW_AT_stmt_list | |||
DW_AT_comp_dir | |||
DW_AT_language | |||
DW_AT_decl_fileDW_AT_decl_line | |||
DW_AT_external | |||
DW_AT_frame_base | |||
DW_AT_sibling | |||
DW_AT_const_value | |||
DW_AT_data_member_location | |||
DW_AT_producer | |||
DW_AT_inline | |||
DW_AT_abstract_origin | |||
DW_AT_specification | |||
DW_AT_ranges |
属性值的Form(编码形式):
DW_FORM_addr | |
DW_FORM_block/block1/block2/block4 | |
DW_FORM_data1/data2/data4/data8 | |
DW_FORM_string | |
DW_FORM_strp | |
DW_FORM_flag | |
DW_FORM_sdataDW_FORM_udata | |
DW_FORM_ref1/ref2/ref4/ref8/ref_udata | |
DW_FORM_ref_addr | |
DW_FORM_indirect | |
DW_FORM_sec_offset | |
DW_FORM_exprloc | |
DW_FORM_implicit_const |
属性值语义类别:address(程序地址)、block(位置表达式)、constant(常量)、flag(存在性)、reference(DIE引用)、string(字符串)。
2.3 GDB中的表示
DIE由 struct die_info(dwarf2read.c)表示:
structdie_info { ENUM_BITFIELD(dwarf_tag) tag : 16; // DW_TAG_*unsignedchar num_attrs; // 属性个数unsignedint abbrev; // 缩写码 sect_offset sect_off; // 在节中的偏移structdie_info *child;// 第一个子DIEstructdie_info *sibling;// 下一个兄弟DIEstructdie_info *parent;// 父DIEstructattributeattrs[1];// 属性数组(柔性数组)};属性由 struct attribute 表示,访问宏:DW_STRING(attr)、DW_UNSND(attr)、DW_SND(attr)、DW_ADDR(attr)、DW_BLOCK(attr)
构建部分符号表时使用精简版 struct partial_die_info,仅保留名称、PC范围、外部性等关键信息。
3. 位置描述
位置描述是DWARF最核心的机制之一,用于回答"变量在哪里"这个问题。
3.1 位置表达式
位置描述有两种形式:位置表达式(变量位置在生命周期内不变,内联在DIE属性中)和位置列表(变量位置随执行变化,存储在 .debug_loc 节中)。
位置表达式基于一个简单的栈机器模型:栈元素大小等于目标机地址大小,每个操作码对栈进行操作,执行完毕后栈顶值即为结果。
寄存器命名与寻址:
DW_OP_reg0DW_OP_reg31 | |
DW_OP_regx | |
DW_OP_fbreg | |
DW_OP_breg0DW_OP_breg31 | |
DW_OP_bregx |
字面量与地址:
DW_OP_lit0DW_OP_lit31 | |
DW_OP_addr | |
DW_OP_const1u/1sDW_OP_const8u/8s | |
DW_OP_constuDW_OP_consts |
栈操作:
DW_OP_dupDW_OP_drop | |
DW_OP_pickDW_OP_over | |
DW_OP_swapDW_OP_rot | |
DW_OP_derefDW_OP_deref_size |
算术/逻辑/控制流:
DW_OP_plusDW_OP_minus / DW_OP_mul / DW_OP_div / DW_OP_mod | |
DW_OP_andDW_OP_or / DW_OP_xor / DW_OP_not | |
DW_OP_shlDW_OP_shr / DW_OP_shra | |
DW_OP_plus_uconst | |
DW_OP_le/ge/eq/lt/gt/ne | |
DW_OP_skipDW_OP_bra | |
DW_OP_pieceDW_OP_nop |
位置表达式示例:
DW_OP_reg3 | |
DW_OP_fbreg -50 | |
DW_OP_bregx 54 32; DW_OP_deref | |
DW_OP_reg3; DW_OP_piece 4; DW_OP_reg10; DW_OP_piece 2 |
3.2 位置列表
位置列表存储在 .debug_loc 节中,用于描述位置随生命周期变化的对象。每个条目包含:起始地址、结束地址(均相对于CU基址)、位置表达式。
特殊情况:起始和结束地址均为0表示列表终止;均为0xFFFFFFFF表示基址选择条目;地址范围未覆盖的部分表示对象不可用。
3.3 GDB中的位置表达式求值
GDB通过 dwarf_expr_context 类(dwarf2expr.c)实现位置表达式求值,核心为 execute_stack_op() 方法。结果类型:
DWARF_VALUE_MEMORY | |
DWARF_VALUE_REGISTER | |
DWARF_VALUE_STACK | |
DWARF_VALUE_LITERAL | |
DWARF_VALUE_OPTIMIZED_OUT | |
DWARF_VALUE_IMPLICIT_POINTER |
位置列表通过 dwarf2_find_location_expression()(dwarf2loc.c)查找,支持标准 .debug_loc、.debug_loc.dwo、.debug_loclists 三种格式。
4. 行号与调用帧信息
4.1 行号信息
行号信息建立机器指令地址与源代码行号之间的映射,存储在 .debug_line 节中,使用状态机模型编码。
行号程序头关键字段:
unit_lengthversion | |
minimum_instruction_length | |
default_is_stmt | |
line_baseline_range / opcode_base | |
standard_opcode_lengths[] | |
include_dirs[]file_names[] |
行号状态机寄存器:
address | ||
fileline / column | ||
is_stmt | ||
basic_blockend_sequence | ||
prologue_endepilogue_begin |
标准操作码(DW_LNS_*):
DW_LNS_copy | |
DW_LNS_advance_pc | |
DW_LNS_advance_line | |
DW_LNS_set_fileDW_LNS_set_column | |
DW_LNS_negate_stmtDW_LNS_set_basic_block | |
DW_LNS_const_add_pcDW_LNS_fixed_advance_pc | |
DW_LNS_set_prologue_endDW_LNS_set_epilogue_begin | |
DW_LNS_set_isa |
扩展操作码(DW_LNE_*):DW_LNE_end_sequence(序列结束)、DW_LNE_set_address(设置地址)、DW_LNE_define_file(定义文件)、DW_LNE_set_discriminator(设置判别器)。
特殊操作码将地址增量和行增量编码在一个字节中:
special_opcode = opcode - opcode_baseaddress_advance = special_opcode / line_rangeline_advance = line_base + (special_opcode % line_range)4.2 调用帧信息(CFI)
CFI描述如何从当前栈帧回溯到调用者栈帧(unwind),存储在 .debug_frame 或 .eh_frame 节中。
CFI由两种条目组成:
CIE(公共信息条目):包含版本号、增强字符串(如"zR")、代码/数据对齐因子、返回地址寄存器、初始CFI指令序列。
FDE(帧描述条目):包含指向所属CIE的指针、PC范围起始和长度、CFI指令序列。
CFI指令分类:
DW_CFA_set_locDW_CFA_advance_loc* | ||
DW_CFA_def_cfadef_cfa_sf / def_cfa_register / def_cfa_offset / def_cfa_expression | ||
DW_CFA_undefinedsame_value / offset / offset_extended_sf / val_offset / register / expression / val_expression | ||
DW_CFA_remember_staterestore_state / nop |
GDB中的CFI数据结构:struct dwarf2_cie、struct dwarf2_fde、struct dwarf2_frame_state、struct dwarf2_frame_state_reg、enum dwarf2_frame_reg_rule。关键函数:dwarf2_frame_cfa()、dwarf2_append_unwinders()。
5. GDB源码实现映射
5.1 文件职责与数据流
dwarf2read.hdwarf2read.c | |
dwarf2expr.hdwarf2expr.c | |
dwarf2loc.hdwarf2loc.c | |
dwarf2-frame.hdwarf2-frame.c | |
dwarf2-frame-tailcall.c.h |
核心数据流:
ELF文件加载 │ ▼dwarf2_build_psymtabs_hard() ← 扫描所有CU,构建部分符号表 │ ├─→ abbrev_table_read_table() / read_partial_die() / 创建partial_symtab │ ▼psymtab_to_symtab_1() ← 按需展开完整符号表 │ ├─→ load_full_comp_unit() │ ├─→ read_die_and_children() → read_full_die_1() / read_attribute() │ └─→ process_die() → read_file_scope() / read_func_scope() / read_type_die() ... │ ├─→ dwarf_decode_lines() │ ▼变量求值 ├─→ dwarf2_evaluate_loc_desc() → execute_stack_op() ├─→ dwarf2_find_location_expression() └─→ dwarf2_frame_cfa()5.2 关键函数索引
read_full_die_1 | ||
read_die_and_children | ||
process_die | ||
dwarf2_attr | ||
read_type_die | ||
abbrev_table_read_table | ||
dwarf_decode_lines | ||
execute_stack_op | ||
dwarf2_find_location_expression | ||
dwarf2_evaluate_loc_desc | ||
dwarf2_frame_cfa |
6. x86实例:DWARF2调试全过程
本节以一个简单的x86-64 C程序为例,完整展示从编译到GDB调试的DWARF2信息流转过程。
6.1 示例程序与编译
// example.cintadd(int a, int b){int sum = a + b;return sum;}intmain(){int x = 10;int y = 20;int result = add(x, y);return result;}编译命令:
gcc -gdwarf-2 -O0 -fstack-protector-strong -c example.c -o example.o-gdwarf-2 指定生成DWARF2格式调试信息,-O0 关闭优化以保证变量在栈上而非寄存器中。
6.2 ELF节布局
readelf -S example.o [Nr] Name Type [ 1] .text PROGBITS [ 5] .debug_info PROGBITS [ 7] .debug_abbrev PROGBITS [ 8] .debug_loc PROGBITS [ 9] .debug_aranges PROGBITS [11] .debug_line PROGBITS [13] .debug_str PROGBITS各节作用:.debug_info 存DIE树,.debug_abbrev 存缩写表,.debug_line 存行号程序,.debug_loc 存位置列表,.debug_str 存间接字符串。
6.3 缩写表解读
readelf --debug-dump=abbrev example.o Number TAG 1 DW_TAG_compile_unit [has children] DW_AT_producer DW_FORM_strp DW_AT_language DW_FORM_data1 DW_AT_name DW_FORM_strp DW_AT_comp_dir DW_FORM_strp DW_AT_low_pc DW_FORM_addr DW_AT_high_pc DW_FORM_addr DW_AT_stmt_list DW_FORM_data4 2 DW_TAG_subprogram [has children] DW_AT_external DW_FORM_flag DW_AT_name DW_FORM_strp DW_AT_decl_file DW_FORM_data1 DW_AT_decl_line DW_FORM_data1 DW_AT_type DW_FORM_ref4 DW_AT_low_pc DW_FORM_addr DW_AT_high_pc DW_FORM_addr DW_AT_frame_base DW_FORM_data4 DW_AT_GNU_all_tail_call_sites DW_FORM_flag DW_AT_sibling DW_FORM_ref4 3 DW_TAG_variable [no children] DW_AT_name DW_FORM_string DW_AT_decl_file DW_FORM_data1 DW_AT_decl_line DW_FORM_data1 DW_AT_type DW_FORM_ref4 DW_AT_location DW_FORM_block1 4 DW_TAG_variable [no children] DW_AT_name DW_FORM_strp DW_AT_decl_file DW_FORM_data1 DW_AT_decl_line DW_FORM_data1 DW_AT_type DW_FORM_ref4 DW_AT_location DW_FORM_block1 5 DW_TAG_base_type [no children] DW_AT_byte_size DW_FORM_data1 DW_AT_encoding DW_FORM_data1 DW_AT_name DW_FORM_string 6 DW_TAG_subprogram [has children] DW_AT_external DW_FORM_flag DW_AT_name DW_FORM_string DW_AT_decl_file DW_FORM_data1 DW_AT_decl_line DW_FORM_data1 DW_AT_prototyped DW_FORM_flag DW_AT_type DW_FORM_ref4 DW_AT_low_pc DW_FORM_addr DW_AT_high_pc DW_FORM_addr DW_AT_frame_base DW_FORM_data4 DW_AT_GNU_all_call_sites DW_FORM_flag 7 DW_TAG_formal_parameter [no children] DW_AT_name DW_FORM_string DW_AT_decl_file DW_FORM_data1 DW_AT_decl_line DW_FORM_data1 DW_AT_type DW_FORM_ref4 DW_AT_location DW_FORM_block1解读要点:
缩写1定义编译单元,包含producer/name/low_pc/high_pc/stmt_list等属性 缩写2和6都定义函数(subprogram),区别在于2用strp引用name且有sibling属性,6用内联string且无sibling 缩写2的 DW_AT_frame_base类型为DW_FORM_data4,指向.debug_loc中的位置列表(而非内联exprloc),因为编译器开启了-fstack-protector-strong,帧基址随PC位置变化缩写7定义函数参数,使用 DW_FORM_block1编码位置表达式
6.4 DIE树解读
readelf --debug-dump=info example.o <0><b>: Abbrev Number: 1 (DW_TAG_compile_unit) <c> DW_AT_producer : GNU C11 7.5.0 -mtune=generic -march=x86-64 -gdwarf-2 -O0 -fstack-protector-strong <10> DW_AT_language : 12 (ANSI C99) <11> DW_AT_name : example.c <15> DW_AT_comp_dir : /home/yefei/embedded/temp/case <19> DW_AT_low_pc : 0x0 <21> DW_AT_high_pc : 0x47 <29> DW_AT_stmt_list : 0x0 <1><2d>: Abbrev Number: 2 (DW_TAG_subprogram) <2e> DW_AT_external : 1 <2f> DW_AT_name : main <33> DW_AT_decl_file : 1 <34> DW_AT_decl_line : 6 <35> DW_AT_type : <0x79> <39> DW_AT_low_pc : 0x1a <41> DW_AT_high_pc : 0x47 <49> DW_AT_frame_base : 0x0 (location list) <4d> DW_AT_GNU_all_tail_call_sites: 1 <4e> DW_AT_sibling : <0x79> <2><52>: Abbrev Number: 3 (DW_TAG_variable) <53> DW_AT_name : x <55> DW_AT_decl_file : 1 <56> DW_AT_decl_line : 7 <57> DW_AT_type : <0x79> <5b> DW_AT_location : 2 byte block: 91 64 (DW_OP_fbreg: -28) <2><5e>: Abbrev Number: 3 (DW_TAG_variable) <5f> DW_AT_name : y <61> DW_AT_decl_file : 1 <62> DW_AT_decl_line : 8 <63> DW_AT_type : <0x79> <67> DW_AT_location : 2 byte block: 91 68 (DW_OP_fbreg: -24) <2><6a>: Abbrev Number: 4 (DW_TAG_variable) <6b> DW_AT_name : result <6f> DW_AT_decl_file : 1 <70> DW_AT_decl_line : 9 <71> DW_AT_type : <0x79> <75> DW_AT_location : 2 byte block: 91 6c (DW_OP_fbreg: -20) <2><78>: Abbrev Number: 0 <1><79>: Abbrev Number: 5 (DW_TAG_base_type) <7a> DW_AT_byte_size : 4 <7b> DW_AT_encoding : 5 (signed) <7c> DW_AT_name : int <1><80>: Abbrev Number: 6 (DW_TAG_subprogram) <81> DW_AT_external : 1 <82> DW_AT_name : add <86> DW_AT_decl_file : 1 <87> DW_AT_decl_line : 1 <88> DW_AT_prototyped : 1 <89> DW_AT_type : <0x79> <8d> DW_AT_low_pc : 0x0 <95> DW_AT_high_pc : 0x1a <9d> DW_AT_frame_base : 0x60 (location list) <a1> DW_AT_GNU_all_call_sites: 1 <2><a2>: Abbrev Number: 7 (DW_TAG_formal_parameter) <a3> DW_AT_name : a <a5> DW_AT_decl_file : 1 <a6> DW_AT_decl_line : 1 <a7> DW_AT_type : <0x79> <ab> DW_AT_location : 2 byte block: 91 5c (DW_OP_fbreg: -36) <2><ae>: Abbrev Number: 7 (DW_TAG_formal_parameter) <af> DW_AT_name : b <b1> DW_AT_decl_file : 1 <b2> DW_AT_decl_line : 1 <b3> DW_AT_type : <0x79> <b7> DW_AT_location : 2 byte block: 91 58 (DW_OP_fbreg: -40) <2><ba>: Abbrev Number: 3 (DW_TAG_variable) <bb> DW_AT_name : sum <bf> DW_AT_decl_file : 1 <c0> DW_AT_decl_line : 2 <c1> DW_AT_type : <0x79> <c5> DW_AT_location : 2 byte block: 91 6c (DW_OP_fbreg: -20) <2><c8>: Abbrev Number: 0 <1><c9>: Abbrev Number: 0DIE树结构总览:
DW_TAG_compile_unit (example.c, PC: 0x0~0x47)├── DW_TAG_subprogram (main, PC: 0x1a~0x47, frame_base→.debug_loc+0x0)│ ├── DW_TAG_variable (x, loc: DW_OP_fbreg: -28)│ ├── DW_TAG_variable (y, loc: DW_OP_fbreg: -24)│ └── DW_TAG_variable (result, loc: DW_OP_fbreg: -20)├── DW_TAG_base_type (int, 4 bytes, signed)└── DW_TAG_subprogram (add, PC: 0x0~0x1a, frame_base→.debug_loc+0x60) ├── DW_TAG_formal_parameter (a, loc: DW_OP_fbreg: -36) ├── DW_TAG_formal_parameter (b, loc: DW_OP_fbreg: -40) └── DW_TAG_variable (sum, loc: DW_OP_fbreg: -20)6.5 位置表达式详解
本例中所有变量位置均使用 DW_OP_fbreg 编码,而非 DW_OP_breg6(直接引用rbp)。这是因为编译器开启了 -fstack-protector-strong,帧基址(frame base)在函数序言期间会发生变化,需要通过位置列表描述。
fbreg 与 breg6 的区别:
DW_OP_fbreg: offset— 取当前函数的帧基址 + SLEB128偏移。帧基址由DW_AT_frame_base属性定义(一个位置列表),其值随PC位置变化DW_OP_breg6: offset— 直接取r6(rbp)寄存器值 + SLEB128偏移,帧基址固定
结合汇编验证(以add函数为例):
add: 0: push %rbp ; 保存旧rbp 1: mov %rsp,%rbp ; 建立新帧基址 4: mov %edi,-0x14(%rbp) ; a入栈 → rbp-0x14 = rbp-20 7: mov %esi,-0x18(%rbp) ; b入栈 → rbp-0x18 = rbp-24 a: mov -0x14(%rbp),%edx d: mov -0x18(%rbp),%eax 10: add %edx,%eax 12: mov %eax,-0x4(%rbp) ; sum → rbp-0x4 = rbp-4 15: mov -0x4(%rbp),%eax 18: pop %rbp 19: retDIE中记录:a = DW_OP_fbreg: -36,b = DW_OP_fbreg: -40,sum = DW_OP_fbreg: -20。
为什么偏移与汇编不一致?因为 DW_OP_fbreg 使用帧基址而非rbp。根据6.7节的CFI分析,序言后CFA = rbp + 16,而 DW_AT_frame_base 位置列表在序言后的值等于CFA,因此:
DW_OP_fbreg: -36 → frame_base + (-36) = (rbp + 16) - 36 = rbp - 20 ✓ 与汇编中-0x14一致DW_OP_fbreg: -40 → frame_base + (-40) = (rbp + 16) - 40 = rbp - 24 ✓ 与汇编中-0x18一致DW_OP_fbreg: -20 → frame_base + (-20) = (rbp + 16) - 20 = rbp - 4 ✓ 与汇编中-0x4一致GDB求值 DW_OP_fbreg: -28 的内部过程(对应 execute_stack_op()):
读取操作码 DW_OP_fbreg(0x91),得知需要取帧基址加SLEB128偏移读取SLEB128操作数:-28 从当前DIE所属的 DW_TAG_subprogram获取DW_AT_frame_base(位置列表偏移0x0)通过 dwarf2_find_location_expression()查找当前PC对应的位置表达式,结果为 CFA (= rbp + 16)计算 (rbp + 16) + (-28) = rbp - 12 结果类型为 DWARF_VALUE_MEMORY,GDB从该内存地址读取4字节得到变量值
6.6 行号信息解读
readelf --debug-dump=line example.o Offset: 0x0 Length: 65 DWARF Version: 3 Prologue Length: 32 Minimum Instruction Length: 1 Initial value of 'is_stmt': 1 Line Base: -5 Line Range: 14 Opcode Base: 13 The Directory Table is empty. The File Name Table (offset 0x1c): Entry Dir Time Size Name 1 0 0 0 example.c Line Number Statements: [0x2a] Extended opcode 2: set Address to 0x0 [0x35] Copy [0x36] Special opcode 146: advance Address by 10 to 0xa and Line by 1 to 2 [0x37] Special opcode 160: advance Address by 11 to 0x15 and Line by 1 to 3 [0x38] Special opcode 48: advance Address by 3 to 0x18 and Line by 1 to 4 [0x39] Special opcode 35: advance Address by 2 to 0x1a and Line by 2 to 6 [0x3a] Special opcode 118: advance Address by 8 to 0x22 and Line by 1 to 7 [0x3b] Special opcode 104: advance Address by 7 to 0x29 and Line by 1 to 8 [0x3c] Special opcode 104: advance Address by 7 to 0x30 and Line by 1 to 9 [0x3d] Advance PC by constant 17 to 0x41 [0x3e] Special opcode 20: advance Address by 1 to 0x42 and Line by 1 to 10 [0x3f] Special opcode 48: advance Address by 3 to 0x45 and Line by 1 to 11 [0x40] Advance PC by 2 to 0x47 [0x42] Extended opcode 1: End of SequenceGDB执行行号状态机后建立的地址-行号映射:
int add(int a, int b) { | ||
int sum = a + b; | ||
return sum; | ||
} | ||
int main() { | ||
int x = 10; | ||
int y = 20; | ||
int result = add(x, y); | ||
return result; | ||
} |
当GDB收到当前PC值(如0x25),通过行号表查找:0x25落在[0x22, 0x29)区间,对应源码第7行 int x = 10;,这就是 list 命令能显示对应源码的原理。
6.7 CFI解读
readelf --debug-dump=frames example.o00000000 0000000000000014 00000000 CIE Version: 1 Augmentation: "zR" Code alignment factor: 1 Data alignment factor: -8 Return address column: 16 Augmentation data: 1b DW_CFA_def_cfa: r7 (rsp) ofs 8 DW_CFA_offset: r16 (rip) at cfa-8 DW_CFA_nop DW_CFA_nop00000018 000000000000001c 0000001c FDE cie=00000000 pc=0000000000000000..000000000000001a DW_CFA_advance_loc: 1 to 0000000000000001 DW_CFA_def_cfa_offset: 16 DW_CFA_offset: r6 (rbp) at cfa-16 DW_CFA_advance_loc: 3 to 0000000000000004 DW_CFA_def_cfa_register: r6 (rbp) DW_CFA_advance_loc: 21 to 0000000000000019 DW_CFA_def_cfa: r7 (rsp) ofs 8 DW_CFA_nop DW_CFA_nop DW_CFA_nop00000038 000000000000001c 0000003c FDE cie=00000000 pc=000000000000001a..0000000000000047 DW_CFA_advance_loc: 1 to 000000000000001b DW_CFA_def_cfa_offset: 16 DW_CFA_offset: r6 (rbp) at cfa-16 DW_CFA_advance_loc: 3 to 000000000000001e DW_CFA_def_cfa_register: r6 (rbp) DW_CFA_advance_loc: 40 to 0000000000000046 DW_CFA_def_cfa: r7 (rsp) ofs 8 DW_CFA_nop DW_CFA_nop DW_CFA_nopCIE解读: 增强字符串"zR"表示使用 .eh_frame 格式(DW_CFA_offset使用DW_ARC_offset64)。初始CFA = rsp + 8,返回地址(rip)保存在CFA-8处。
add函数FDE解读(pc=0x0~0x1a):
关键点:序言完成后 CFA = rbp + 16,而非 rbp + 0。这是因为 def_cfa_register 只更换寄存器,不改变偏移量。push rbp使rsp减8,CFA从rsp+8变为rsp+16;mov rsp,rbp后,CFA = rbp + 16。
GDB回溯调用栈的过程(假设当前在add函数中,PC=0x10):
根据PC=0x10找到add的FDE,执行CFI指令,得出CFA = rbp + 16 返回地址:CFA-8处的值(即rbp+8处,存放call add时压入的返回地址) 旧rbp:CFA-16处的值(即rbp+0处,存放push rbp保存的旧rbp) 返回地址指向main中0x3f(call add之后),结合行号表定位到源码第9行 以旧rbp为新的CFA基址,继续回溯main的栈帧
6.8 完整调试流程串联
以 break main; run; print x; backtrace 为例,展示GDB内部如何利用DWARF2各节:
用户操作 GDB内部动作 使用的DWARF节─────────────── ────────────────────────────────────── ──────────────────break main 1. 查找符号"main" .debug_info (DW_TAG_subprogram) 2. 获取 DW_AT_low_pc = 0x1a .debug_info 3. 在地址0x1a处设置断点run (程序运行到断点,PC=0x1a)print x 1. 查找变量"x" .debug_info (DW_TAG_variable) 2. 获取 DW_AT_location .debug_info = DW_OP_fbreg: -28 3. 查找当前PC对应的frame_base .debug_loc (offset 0x0) → CFA = rbp + 16 4. 求值: (rbp+16) + (-28) = rbp-12 dwarf2expr.c (execute_stack_op) 5. 读取内存rbp-12处的4字节 → 10 6. 输出: $1 = 10backtrace 1. 获取当前PC=0x1a,查找FDE .eh_frame (FDE) 2. 执行CFI指令,计算CFA = rbp+16 3. 读取CFA-8得到返回地址(main中0x3f) 4. 读取CFA-16得到旧rbp(main的rbp) 5. 返回地址0x3f结合行号表 .debug_line → example.c:9 6. 继续回溯main的栈帧(无更多调用者) 7. 输出: #0 add (a=10, b=20) at example.c:2 #1 main () at example.c:9整个流程中,DWARF2各节各司其职:
.debug_info | |
.debug_abbrev | |
.debug_line | |
.debug_loc | |
.eh_frame | |
.debug_str |
附录A:DWARF2与DWARF3/4/5主要差异速查
.debug_loc | .debug_loclists | |||
.debug_ranges | .debug_rnglists | |||
.debug_types | .debug_info | |||
附录B:常用readelf/objdump命令
# 查看DWARF调试信息节readelf -S <elf_file> | grep debug# 查看DIE树readelf --debug-dump=info <elf_file># 查看缩写表readelf --debug-dump=abbrev <elf_file># 查看行号信息readelf --debug-dump=line <elf_file># 查看位置列表readelf --debug-dump=loc <elf_file># 查看CFI帧信息readelf --debug-dump=frames <elf_file># 查看地址范围readelf --debug-dump=aranges <elf_file># 查看字符串表readelf --debug-dump=str <elf_file># 使用objdump查看DWARF信息objdump --dwarf=info <elf_file>objdump --dwarf=frames <elf_file>参考文献
DWARF 2.0.0 Specification: https://dwarfstd.org/doc/dwarf-2.0.0.pdf

夜雨聆风