图文详解gcc -g调试信息

图文详解gcc -g调试信息

目录

大家好,这里是物联网心球。作为一个Linux开发者,我们经常会通过gcc -g命令来编译可执行程序,-g选项能够生成调试信息,开发者根据调试信息能够快速定位并排查程序问题。那么,调试信息到底是什么呢?本文我们一探究竟。

1.调试信息的重要性

gdb调试是一种非常重要的调试手段,如果gdb调试的程序没有调试信息,那么我们将无法从gdb调试器中获取到有价值的信息,调试过程也会异常困难。下面我们通过几个场景来验证一下。

1.1 场景1:查看变量

通过print命令打印变量的值时,带调试信息输出示例如下:

(gdb) print a
$1 = 100

未带调试信息输出示例如下:

(gdb) print a
'a' has unknown type; cast it to its declared type

gdb调试未带调试信息的可执行程序时无法识别变量。

1.2 场景2:打印数据类型

通过ptype命令打印数据类型,带调试信息输出示例如下:

(gdb) ptype it
type = struct item {
    char num;
    int data;
}

未带调试信息输出示例如下:

(gdb) ptype it
No symbol table is loaded. Use the "file" command.

gdb调试未带调试信息的可执行程序时无法识别数据类型。

1.3 场景3:指定源文件行号设置断点

通过break命令在源文件指定行号设置断点,带调试信息输出示例如下:

(gdb) break main.c:12
Breakpoint 2 at 0x55555555515c: file main.c, line 14.

未带调试信息输出示例如下:

(gdb) break main.c:12
No symbol table is loaded. Use the "file" command.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (main.c:12) pending.

gdb调试未带调试信息的可执行程序时,无法在指定源文件行号设置断点。

1.4 场景4:打印堆栈

通过bt命令打印堆栈信息,带调试信息输出示例如下:

(gdb) bt
#0 main (argc=1, argv=0x7fffffffe468) at main.c:14

未带调试信息输出示例如下:

(gdb) bt
#0 0x0000555555555151 in main ()

gdb调试未带调试信息的可执行程序时,打印堆栈信息无行号和其他调试信息。

通过以上几个场景的对比,我们能够直观的感受到,带调试信息的可执行文件能够提供更加丰富和关键的调试信息,协助开发者高效地解决问题。

2.认识调试信息

调试信息其实并不神秘,它是嵌入在ELF文件中的节,这些节的形式为.debug_*(*代表不同的节)。对ELF文件还不了解的读者可以先看一下我的这篇文章:从ELF文件到Linux进程 。 我们通过图1来深入学习调试信息。

图文详解gcc -g调试信息 图1

图1 两种文件对比

图1左边为无调试信息可执行文件,右边为带调试信息可执行文件。无调试信息可执行文件中的内容比较少,文件占用内存空间也比较小。通过对比,我们发现带调试信息可执行文件多了一些节:.debug_aranges节、.debug_info节、.debug_abbrev节、.debug_line节、.debug_str节、.debug_line_str节。这些多出来的节就是我们今天的主角:调试信息。这些节并不是随意生成的,需要遵循一定的规则,这个规则就是DWARF。

3.DWARF介绍

DWARF(全称:Debugging With Attributed Record Formats)是一种广泛使用的调试数据格式标准,旨在为编译器、调试器和其他工具提供一种标准化的机制,用于描述程序的源代码结构、变量、类型、函数调用栈等信息。

DWARF共经历了5个版本的迭代:

  • DWARF1:1992年发布,为Unix System V的sdb调试器设计,支持C 语言,现已很少使用。

  • DWARF2:1993年发布,修正v1问题,增加C++支持,引入数据压缩。

  • DWARF3:2005年发布,增加对多种语言支持,优化数据压缩。

  • DWARF4:2010年发布,进一步改善数据压缩,支持编译器优化后代码描述。

  • DWARF5:2017年发布,支持调试信息分离,改进宏和源文件描述。

DWARF的规则很复杂,笔者不建议大家直接研究DWARF标准。我们可以把DWARF标准当做一个手册,当我们学习的过程中遇到问题再去查阅它。相较于直接学习DWARF标准,更为有效的学习方法是选搞懂各种调试节(.debug_*)的原理和作用。

以DWARF5为例,常见的调试节见表1。

图文详解gcc -g调试信息 图2

表1 DWARF5常见调试节

通过命令:readelf -w 可执行文件,可显示所有的调试节。由于该命令展示的内容比较多,限于篇幅,这里不展示输出示例。

3.1 .debug_aranges节

.debug_aranges节用于提供内存地址与编译单元之间的映射关系,编译单元后续会详细介绍。通过.debug_aranges节,调试器可以快速将程序计数器(PC)的值映射到.debug_info节中的编译单元,从而获取相关的调试信息。

通过命令:readelf -wr 可执行文件,可查看.debug_aranges信息,输出示例如下:

图文详解gcc -g调试信息 图3

.debug_aranges节分为两个部分:头部信息和地址范围表。

头部信息相关字段如下:

  • Length:表示该节的总长度(不包括这个长度字段本身)。

  • Version:表示 DWARF 格式的版本号,通常为2或4。

  • Offset into .debug_info:表示.debug_info节中的偏移量,用于定位编译单元的起始位置。

  • Pointer Size:表示目标系统中指针的大小,以字节为单位。

  • Segment Size:表示目标系统中段选择符的大小,以字节为单位,通常为 0。

地址范围表由一系列的地址范围对组成,每个地址范围对包含两个字段:

  • Address:表示地址范围的起始地址。

  • Length:表示地址范围的长度。

gcc编译可执行程序时,会将源文件(如.c/.cpp文件)编译成编译单元并将编译单元覆盖的地址范围记录在.debug_aranges节。

gdb需要确定某个指令地址(如:0x7fff1234)对应的源代码位置时:首先,通过0x7fff1234地址查询.debug_aranges节地址范围表,找到对应的编译单元在.debug_info中的偏移;然后,通过偏移定位到.debug_info中的编译单元;最后,从编译单元开始解析DIE树,获取到源码相关的信息。

3.2 .debug_info节

.debug_info节由调试信息条目(Debugging Information Entry,DIE)构成。DIE是.debug_info节中的一个基本单元,它通过标签(Tag)和属性(Attributes)来描述程序中的一个语义实体(如:函数名、参数、局部变量、代码行号范围)。每个DIE可以有多个子条目,形成一个树状结构,用于描述更复杂的语义关系。

通过命令:readelf -wi 可执行文件,可查看.debug_info信息,输出示例如下:

图文详解gcc -g调试信息 图4

每个 DIE 包含以下几个部分:

(1)Tag(标签)

Tag 是一个单字节的值,用于标识调试信息条目(DIE)的类型。常见的Tag如下:

  • DW_TAG_compile_unit:表示一个编译单元,通常是源文件。

  • DW_TAG_subprogram:表示一个函数或方法。

  • DW_TAG_variable:表示一个变量,可以是全局变量或局部变量。

  • DW_TAG_formal_parameter:表示函数的参数。

  • DW_TAG_typedef:表示一个类型定义。

  • DW_TAG_structure_type:表示一个结构体类型。

  • DW_TAG_union_type:表示一个联合体类型。

  • DW_TAG_enumeration_type:表示一个枚举类型。

  • DW_TAG_array_type:表示一个数组类型。

  • DW_TAG_base_type:表示一个基本类型,如int 、float等。

(2)Attributes(属性)

Attributes 是一个属性列表,每个属性由一个属性名称和一个属性值组成。常见的Attributes如下:

  • DW_AT_name:表示实体的名称,如变量名、函数名、文件名等。

  • DW_AT_type:表示实体的类型,通常是一个偏移量,指向.debug_info节中的另一个 DIE。

  • DW_AT_location:表示变量的存储位置,可以是寄存器或内存地址。

  • DW_AT_low_pc和DW_AT_high_pc:表示函数的代码范围,分别表示函数的起始地址和结束地址。

  • DW_AT_decl_file、DW_AT_decl_line 和DW_AT_decl_column:表示实体在源文件中的声明位置。

(3)Children(子条目)

Children 是一个 DIE 的子条目列表,每个子条目也是一个 DIE。

3.3 .debug_abbrev节

.debug_abbrev节用于定义.debug_info节中调试信息条目(DIE)的结构和属性的缩写表。通过使用缩写表,.debug_info节可以更高效地存储和解析调试信息,减少重复数据的存储。

通过命令:readelf -wa 可执行文件,可查看.debug_abbrev节信息,输出示例如下:

图文详解gcc -g调试信息 图5

.debug_abbrev节中的DIE和.debug_info节中的DIE互为抽象和实现的关系。一个比较形象的比喻,.debug_abbrev节中的DIE如同C++中的类,.debug_info节中的DIE如同C++中的对象。类是数据类型,对象为类的具体实现。

3.4 .debug_line节

.debug_line节建立了机器指令地址与源代码行号之间的映射关系,是实现源码级调试的核心基础。

.debug_line节的核心功能是:

  • 提供源代码行号与机器指令地址的精确映射。
  • 使调试器能够将程序计数器(PC)值转换为源代码位置。
  • 支持设置断点、单步执行和堆栈跟踪等基本调试功能。

通过命令:readelf -wl 可执行文件,可查看.debug_line节信息,输出示例如下:

图文详解gcc -g调试信息 图6

注意,.debug_line节的规则比较复杂,我们可以跳过规则学习,先了解.debug_line的作用。.debug_line节最终将会呈现一个指令地址和源文件行号的映射表,见表2。

图文详解gcc -g调试信息 图7

表2 .debug_line指令地址和行号映射表

3.5 .debug_str节

.debug_str节是一个字符串池,它存储了调试过程中所需的所有字符串数据,包括:函数名、变量名、类型名、编译器信息、其他文本描述。

通过命令:readelf -ws 可执行文件,可查看.debug_str节信息,输出示例如下:

图文详解gcc -g调试信息 图8

从输出示例可以看到.debug_str包含一些我们比较常见的字符串,如果我们想使用这些字符串,只需要从调试文件指定的位置读取这些字符串即可。

3.6 .debug_line_str节

.debug_line_str节同样是一个字符串池。这些字符串通常包括文件名、目录路径等,它们被.debug_line节中的行号程序引用。

输出示例如下:

图文详解gcc -g调试信息 图9

.debug_line_str和.debug_str的使用方法类似。

4.调试信息分离

调试信息分离是指将调试信息从可执行文件中分离出来,存储在单独的文件中。将调试信息存储在单独的文件有几个优点:

  • 减小可执行文件大小:通过将调试信息分离到单独的文件中,可执行文件的大小可以显著减小。

  • 提高安全性:分离的调试信息文件可以存储在安全的环境中,从而防止调试信息泄露。

  • 提高性能:较小的可执行文件可以更快地加载和运行,从而提高应用程序的性能。

4.1 实现原理

为了让大家更好的理解调试信息分离的过程,我们通过一张图描述该过程,如图2所示。

图文详解gcc -g调试信息 图10

图2 调试信息分离

调试信息分离具体步骤如下:

  • 步骤1:编译带调试信息的可执行程序。 gcc -g编译带调试信息的可执行文件,以test程序为例,命令如下:
gcc -g -o test test.c
  • 步骤2:生成单独的调试信息文件。

使用objcopy命令的–only-keep-debug选项,将调试信息提取到一个单独的文件中,命令如下:

objcopy --only-keep-debug test test.debug

test.debug文件为单独调试文件。

  • 步骤3:去除可执行文件中的调试信息。

使用objcopy命令的–strip-debug选项,移除可执行文件调试信息,

objcopy --strip-debug test

执行完该命令,可执行文件中的.debug_*节将全部被删除。

  • 步骤4:添加调试信息文件链接。

使用objcopy命令的–add-gnu-debuglink选项,将调试信息文件的路径添加到可执行文件中。gdb调试可执行文件时,就能够自动查找到调试信息文件。

objcopy --add-gnu-debuglink=test.debug test

执行完该命令,可执行文件中将添加一个.gnu_debuglink节,其中包含调试信息文件的路径。通过以下命令可以查看.gnu_debuglink节中的内容:

readelf -p .gnu_debuglink test

输出示例如下:

图文详解gcc -g调试信息 图11

4.2 gdb调试单独调试文件

当我们按照上述步骤生成了单独的调试文件后,我们既能够获得一个轻量级的可执行程序,又能够有一个完整的调试文件。当程序在实际环境运行出现问题时,我们能够通过单独的调试文件排查问题。

gdb调试单独调试文件分为:手动添加调试文件和自动添加调试文件。

手动添加通过gdb add-symbol-file命令完成,输出示例如下:

(gdb) add-symbol-file test.debug
add symbol table from file "test.debug"
(y or n) y
Reading symbols from test.debug...

自动添加的方式需要在可执行程序中添加调试信息文件链接(参考调试信息分离步骤4)。注意,可执行文件和调试文件需要在同一个目录。输出示例如下:

图文详解gcc -g调试信息 图12

← 返回文章列表