C++调试(1)—— 认识Dwarf格式

DWARF全称为“Debugging With Attributed Record Formats”,其设计初衷是为了配合ELF格式进行UNIX可执行文件的调试信息生成。DWARF调试信息主要面向开发者用以指导如何生成调试信息以及如何使用调试信息。比如编译器、链接器开发者需要参考DWARF来生成调试信息,而调试器开发者需要参考DWARF来使用调试信息。DWARF开始时主要是为 UNIX 下的调试器提供必要的调试信息,例如内存地址对应的文件名以及代码行号等信息。GCC和Clang以及Go和Rust都使用该格式生成调试信息,与该格式相对的,Windows平台使用PDB(Program Database)作为调试信息的主要格式。
2017年,DWARF v5发布,提供了更好的数据压缩能力,调试信息与可执行程序的分离,对macro宏和源代码文件的更好的描述以及更快速的符号搜索、还有对编译器优化后代码的更好描述等等。

DWARF格式

DWARF调试信息根据描述对象的不同,在最终存储到不同的section,section名称均以前缀.debug_开头。为了提升效率,对DWARF数据的大多数引用都是通过相对于当前编译单元的偏移量引用。

常见的ELF sections及其存储的内容如下:

  • .debug_abbrev, 存储.debug_info中使用的缩写信息;
  • .debug_arranges, 存储一个加速访问的查询表,通过内存地址查询对应编译单元信息;
  • .debug_frame, 存储调用栈帧信息;
  • .debug_info, 存储核心DWARF数据,包含了描述变量、代码等的DIEs;
  • .debug_line, 存储行号表程序 (程序指令由行号表状态机执行,执行后构建出完整的行号表)
  • .debug_loc, 存储location描述信息;
  • .debug_macinfo, 存储宏相关描述信息;
  • .debug_pubnames, 存储一个加速访问的查询表,通过名称查询全局对象和函数;
  • .debug_pubtypes, 存储一个加速访问的查询表,通过名称查询全局类型;
  • .debug_ranges, 存储DIEs中引用的address ranges;
  • .debug_str, 存储.debug_info中引用的字符串表,也是通过偏移量来引用;
  • .debug_types, 存储描述数据类型相关的DIEs;

符号级调试器需要两张大表,一个是行号表(Line Number Table),一个是调用栈信息表(Call Frame Information)。

  • 行号表, 将程序代码段的指令地址映射为源文件中的地址,如“源文件名:行号”。当然如果指定了源文件中的位置,也可以将其映射为程序代码段中的指令地址。

  • 调用栈信息表, 它允许调试器根据指令地址来定位其在调用栈上的栈帧。

DWARF使用调试信息条目DIE(Debugging Information Entry)来表示每一个编译单元,也即变量、函数、指针等。每个DIE都包含一个tag(如DW_TAG_variable表示变量,DW_TAG_pointer_type表示指针类型,DW_TAG_subprogram表示一个函数等)以及一系列的attributes。每个DIE还可以包含子DIE,结合构成树结构共同描述一个变量、数据类型、函数、编译单元等不同的程序构造,DIE中的每个attribute可以引用另一个DIE(类似指针),例如一个描述变量的DIE,它会包含一个属性DW_AT_type来指向一个描述变量数据类型的DIE。

生成时机

DWARF生成

dSYM 文件和 DWARF 文件在编译时生成是根据链接动作中链接脚本下的符号解析与重定位构建。

DIE

具体的,每个DIE都包含一个标签(tag)以及一系列的属性(attributes),存储在.debug_info和.debug_types中:

  • tag指明了当前调试信息条目描述的程序构造属于哪种类型,如类型、变量、函数、编译单元等;
  • attribute定义了调试信息条目的一些特征,如函数的返回值类型是int类型。

基本类型描述

DW_TAG_base_type,用来描述多种基本类型,包括:整数,地址,字符和浮点数。早期 DWARF 和其他调试信息格式,都假设编译器和调试器对基本类型的大小达成共识,例如int是8位,16位还是32位。但是对于不同的硬件平台和编程语言位宽不一致, DWARF v2以后提供了基础类型和具体硬件上实现的映射解决该问题。

复合类型描述

DWARF支持通过组合或者链接其他基本数据类型来定义新的数据类型。TAG为DW_TAG_base_type,表示它是一个基本数据类型,具体为4字节有符号整数。

数组

DW_TAG_array_type,结合一些相关attributes共同来描述数组,数组对应的DIE,该DIE包含了这样的一些属性来描述数组元素:

  • DW_AT_ordering:描述数组是按照“行主序”还是按照“列主序”存储,如Fortran是按照列主序存储,C和C++是按照行主序存储。如果未指定该属性值,则使用DW_AT_language指定编程语言的默认数组排列规则;
  • DW_AT_type:描述数组中各个元素的类型信息;
  • DW_AT_byte_stride/DW_AT_bit_stride:如果数组中每个元素的实际大小和分配的空间大小不同的话,可以通过这两个属性来说明;

数组的索引值范围,DIE中也需要通过指定最小、最大索引值来给出一个有效的索引值区间。这样DWARF就可以既能够描述C风格的数组(用0作为数组起始索引),也能够描述Pascal和Ada的数组(其数组最小索引值、最大索引值是可以变化的)。数组维度一般是通过换一个TAG为DW_TAG_subrange_type或者DW_TAG_enumeration_type的DIE来描述。

组合多种不同的数据类型来定义一个新的数据类型是编程语言的基本功能,DWARF中分别使用下述tag来描述不同类型:

  • DW_TAG_structure_type,描述结构体struct;
  • DW_TAG_class_type,描述类class;
  • DW_TAG_union_type,描述联合union;
  • DW_TAG_interface_type,描述interface。

如果class实例的大小在编译时可以确定,比如都是基础类型或者没有堆上指针,描述class的DIE就会多一个属性DW_AT_byte_size以描述类的大小。

变量

DW_TAG_variable,用来描述变量,变量名代指存储变量值的内存位置,变量的类型描述了包含的值及其是否可以修改的修饰(例如const)。对变量进行区分的关键是变量的存储位置和作用域,变量可以被存储在全局数据区(.data section)、栈、堆或者寄存器中,变量的作用域,描述了它在程序中什么时候是可见的,某种程度上,变量作用域是由其声明时的位置确定的,在DWARF中通过三元组(文件名,行号,列号)对变量声明位置进行描述。

位置信息

DWARF提供了一种非常通用的机制描述如何确定变量的数据位置,就是通过属性DW_AT_location,该属性允许指定一个操作序列,来告知调试器如何确定数据的位置。调试信息必须为调试器提供一种方法,使其能够查找程序变量的位置、确定动态数组和字符串的范围,以及能找到函数栈帧的基地址或函数返回地址的方法。

而位置信息描述可以分为两类:

  • 位置表达式,是与语言无关的寻址规则表示形式,它是由一些基本构建块、操作序列组合而成的任意复杂度的寻址规则。 只要对象的生命周期是静态或与拥有它的词法块相同,并且在整个生命周期内都不会移动,它们就足以描述任何对象的位置。
  • 位置列表,用于描述生命周期有限的对象或在整个生命周期内可能会更改位置的对象。

位置表达式由零个或多个位置操作组成。 如果没有位置运算表达式,则表示该对象在源代码中存在,但是在目标代码中不存在,可能是由于编译器优化导致的。

可执行描述

DW_TAG_subprogram用来描述函数:

  • 函数DIE具有属性 DW_AT_low_pc、DW_AT_high_pc,以给出函数占用的内存地址空间的上下界。 函数的内存地址可能是连续的,也可能不是连续的。如果不连续,则会有一个内存范围列表。一般DW_AT_low_pc的值为函数入口点地址,除非明确指定了另一个地址;
  • 函数的返回值类型由属性 DW_AT_type 描述。 如果没有返回值,则此属性不存在。如果在此函数的相同范围内定义了返回类型,则返回类型DIE将作为此函数DIE的兄弟DIE;
  • 函数可能具有零个或多个形式参数,这些参数由DIE DW_TAG_formal_parameter 描述,这些形参DIE的位置被安排在函数DIE之后,并且各形参DIE的顺序按照形参列表中出现的顺序;
  • 函数主体可能包含局部变量,这些变量由DIE DW_TAG_variables 在形参DIE之后列出。

编译单元

在生成程序时,多个源文件都被视为一个独立的编译单元,并被编译为独立的*.o文件(例如C),然后链接器会将这些目标文件、系统特定的启动代码、系统库链接在一起以生成完整的可执行程序。DWARF中采用了C语言中的术语编译单元(compilation unit)作为DIE的名称 DW_TAG_compilation_unit。DIE包含有关编译的常规信息,包括源文件对应的目录和文件名、使用的编程语言、DWARF信息的生产者,以及有助于定位行号和宏信息的偏移量等等。

如果编译单元占用了连续的内存(即,它会被装入一个连续的内存区域),那么该单元的低内存地址和高内存地址将有值,即低pc和高pc属性。 这有助于调试器更轻松地确定特定地址处的指令是由哪个编译单元生成的。

如果编译单元占用的内存不连续,则编译器和链接器将提供代码占用的内存地址列表,每个编译单元都由一个“公共信息条目CIE(Common Information Entry)”表示,编译单元中除了CIE以外,还包含了一系列的帧描述条目FDE(Frame Description Entrie)。

其他信息

除了这些内容以外,DWARF调试信息中还有几种非常重要的信息需要描述,符号级调试器非常依赖这些数据。这几种重要的调试信息主要包括:

  1. 加速访问

调试器经常需要根据符号名、类型名、指令地址,快速定位到对应的源代码行。DWARF为了加速查询,在DWARF信息生成的时候允许编译器额外创建3张表用来加速查询,加速符号名查询的.debug_pubnames,加速类型名查询的.debug_pubtypes(查询类型),加速指令地址查询的.debug_aranges。

  1. 行号表

DWARF行号表,包含了可执行程序机器指令的内存地址和对应的源代码行之间的映射关系。

  1. 宏信息

DWARF调试信息中包含了对程序中定义的宏的描述。这是非常基本的信息,但是调试器可以使用它来显示宏的值或将宏翻译成相应的源语言。

  1. 调用栈信息

DWARF中的调用栈信息(Call Frame Information,CFI)为调试器提供了如下信息,函数是如何被调用的,如何找到函数参数,如何找到调用函数(caller)的栈帧信息。调试器借助CFI可以展开调用栈、查找上一个函数、确定当前函数的被调用位置以及传递的参数值。

  1. 变长数据

DWARF定义了一种可变长度的整数,称为Little Endian Base 128(带符号整数为LEB128或无符号整数为ULEB128),LEB128可以压缩占用的字节来表示整数值以节省存储空间。

  1. 压缩DWARF数据

简单玩法

为了不编译和链接C++标准库依赖,简化示例,给出下列代码:

1
2
3
4
5
6
7
8
int foo(int x, int y) {
return x + y;
}

int main() {
int ans = foo(1, 2);
return 0;
}

执行命令:

1
2
clang -O0 -gdwarf-5 test.cpp -o test # 生成dwarf和调试信息
objdump --dwarf=info test

打出来的结果应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
test:     file format elf64-x86-64

Contents of the .debug_info section:

Compilation Unit @ offset 0:
Length: 0x66 (32-bit)
Version: 5
Unit Type: DW_UT_compile (1)
Abbrev Offset: 0
Pointer Size: 8
<0><c>: Abbrev Number: 1 (DW_TAG_compile_unit)
<d> DW_AT_producer : (indexed string: 0): Debian clang version 16.0.6 (27)
<e> DW_AT_language : 33 (C++14)
<10> DW_AT_name : (indexed string: 0x1): test.cpp
<11> DW_AT_str_offsets_base: 0x8
<15> DW_AT_stmt_list : 0
<19> DW_AT_comp_dir : (indexed string: 0x2): /home/luhuanbing
<1a> DW_AT_low_pc : (index: 0): 0x1130
<1b> DW_AT_high_pc : 0x49
<1f> DW_AT_addr_base : 0x8
<1><23>: Abbrev Number: 2 (DW_TAG_subprogram)
<24> DW_AT_low_pc : (index: 0): 0x1130
<25> DW_AT_high_pc : 0x12
<29> DW_AT_frame_base : 1 byte block: 56 (DW_OP_reg6 (rbp))
<2b> DW_AT_linkage_name: (indexed string: 0x3): _Z3fooii
<2c> DW_AT_name : (indexed string: 0x4): foo
<2d> DW_AT_decl_file : 0
<2e> DW_AT_decl_line : 1
<2f> DW_AT_type : <0x65>
<33> DW_AT_external : 1
<2><33>: Abbrev Number: 3 (DW_TAG_formal_parameter)
<34> DW_AT_location : 2 byte block: 91 7c (DW_OP_fbreg: -4)
<37> DW_AT_name : (indexed string: 0x7): x
<38> DW_AT_decl_file : 0
<39> DW_AT_decl_line : 1
<3a> DW_AT_type : <0x65>
<2><3e>: Abbrev Number: 3 (DW_TAG_formal_parameter)
<3f> DW_AT_location : 2 byte block: 91 78 (DW_OP_fbreg: -8)
<42> DW_AT_name : (indexed string: 0x8): y
<43> DW_AT_decl_file : 0
<44> DW_AT_decl_line : 1
<45> DW_AT_type : <0x65>
<2><49>: Abbrev Number: 0
<1><4a>: Abbrev Number: 4 (DW_TAG_subprogram)
<4b> DW_AT_low_pc : (index: 0x1): 0x1150
<4c> DW_AT_high_pc : 0x29
<50> DW_AT_frame_base : 1 byte block: 56 (DW_OP_reg6 (rbp))
<52> DW_AT_name : (indexed string: 0x6): main
<53> DW_AT_decl_file : 0
<54> DW_AT_decl_line : 5
<55> DW_AT_type : <0x65>
<59> DW_AT_external : 1
<2><59>: Abbrev Number: 5 (DW_TAG_variable)
<5a> DW_AT_location : 2 byte block: 91 78 (DW_OP_fbreg: -8)
<5d> DW_AT_name : (indexed string: 0x9): ans
<5e> DW_AT_decl_file : 0
<5f> DW_AT_decl_line : 6
<60> DW_AT_type : <0x65>
<2><64>: Abbrev Number: 0
<1><65>: Abbrev Number: 6 (DW_TAG_base_type)
<66> DW_AT_name : (indexed string: 0x5): int
<67> DW_AT_encoding : 5 (signed)
<68> DW_AT_byte_size : 4
<1><69>: Abbrev Number: 0

总结

DWARF的基本概念为在程序编译的链接的符号重定向阶段对符号进行解析并生成对应的信息:

  • 程序被描述为“DIE节点构成的树”,抽象表示源码中的各种函数、数据和类型;
  • 行号表提供了可执行指令地址和生成它们的源码之间的映射关系;
  • CFI描述了如何虚拟地展开堆栈(unwind的用处)。

C++调试(1)—— 认识Dwarf格式

https://devillove084.github.io/2024/06/15/Debug-1/

作者

devillove084

发布于

2024-06-15

更新于

2024-06-16

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×