gdb 显示具体代码

若想 gdb 调试时可以显示代码,需满足以下两个条件:

  1. 二进制文件中带有调试信息
  2. 机器上有源代码

1. 二进制文件带有调试信息

编译代码时,使用 -g-gz 标志,用于生成调试信息。

注:-gz 表示压缩调试信息。

查看一个二进制文件是否带有调试信息:

1
objdump -S /path/to/bin

若输出中含有 .debug_xxx 段或 .zdebug_xxx 段,则表示该二进制文件含调试信息。

2. 机器上有源代码

二进制文件中有调试信息后,gdb 就知道了 xxx 地址的代码对应于 /path/of/code/xxx 文件中的 yy 行,然后 gdb 就会根据路径去机器上查看这里的代码。这也就意味着,使用 gdb 调试的机器上必须要有源代码!

一般情况下,将代码上传线上服务肯定是不现实的,因此我们一般是将 core 文件下载到开发机上进行调试。请注意,要保证本地使用的二进制程序的源代码和 core 文件使用的二进制的源代码是相同的版本!

我们可以使用在 gdb 中使用 dir 设置代码查找路径(当调试信息中的文件路径为相对地址时)。比如 gdb 显示代码路径为 cika/cika_server/logic/xxx.cc ,我们源代码的根路径为 /data/code/main,使用 dir /data/code/main 就可以为 gdb 搜索代码时指明去该路径下查找代码。

调试的原理

使用 -g 标志进行编译后,编译器 / 链接器会在生成的二进制文件上加入 .debug_xxx 段,这里面便存在着相关的调试信息。

调试信息一般以 DWARF(Debugging With Attributed Record Formats) 格式存储。

DWARF 基本结构

DWARF 调试信息通常包含以下几部分:

  1. 编译单元(Compilation Unit, CU):每个编译单元对应一个源文件及其包含的所有调试信息。
  2. 调试信息条目(Debugging Information Entry, DIE):每个 DIE 描述一个调试信息实体,如变量、函数、类型等。
  3. 属性(Attributes):每个 DIE 包含若干属性,描述该实体的详细信息,如名称、类型、地址等。
  4. 行号信息(Line Number Information):记录源代码行号与机器代码地址的对应关系。

简单的 DWARF 例子

假设我们有一个简单的 C++ 程序 example.cpp:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

void foo() {
std::cout << "Inside foo" << std::endl;
}

int main() {
foo();
return 0;
}

编译这个程序并生成调试信息:

1
g++ -g -o example example.cpp

我们可以使用 readelf 工具查看生成的 DWARF 调试信息:

1
readelf --debug-dump=info example

输出可能会非常详细,但我们可以简化并解释其中的一部分。假设输出的一部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<1><0x0000000b>    DW_TAG_compile_unit
DW_AT_producer ("GNU C++14 9.3.0 -mtune=generic -march=x86-64 -g")
DW_AT_language (DW_LANG_C_plus_plus)
DW_AT_name ("example.cpp")
DW_AT_stmt_list (0x00000000)
DW_AT_comp_dir ("/path/to/directory")
DW_AT_low_pc (0x00000000004004a0)
DW_AT_high_pc (0x00000000004004d0)

<2><0x0000001b> DW_TAG_subprogram
DW_AT_name ("foo")
DW_AT_decl_file (0x00000001)
DW_AT_decl_line (0x00000003)
DW_AT_prototyped (true)
DW_AT_low_pc (0x00000000004004b0)
DW_AT_high_pc (0x00000000004004c0)

<2><0x0000002b> DW_TAG_subprogram
DW_AT_name ("main")
DW_AT_decl_file (0x00000001)
DW_AT_decl_line (0x00000007)
DW_AT_prototyped (true)
DW_AT_low_pc (0x00000000004004c0)
DW_AT_high_pc (0x00000000004004d0)

解释

  1. 编译单元(CU)
    • <1><0x0000000b> DW_TAG_compile_unit:表示一个编译单元。
    • DW_AT_producer:编译器信息。
    • DW_AT_language:编程语言。
    • DW_AT_name:源文件名。
    • DW_AT_stmt_list:行号信息的偏移。
    • DW_AT_comp_dir:编译目录。
    • DW_AT_low_pc 和 DW_AT_high_pc:该编译单元的代码地址范围。
  2. 子程序(Subprogram)
    • <2><0x0000001b> DW_TAG_subprogram:表示一个子程序(函数)。
    • DW_AT_name:函数名。
    • DW_AT_decl_file:声明该函数的源文件索引。
    • DW_AT_decl_line:声明该函数的源代码行号。
    • DW_AT_prototyped:是否有原型。
    • DW_AT_low_pc 和 DW_AT_high_pc:该函数的代码地址范围。

行号信息

行号信息记录了源代码行号与机器代码地址的对应关系。可以使用 readelf –debug-dump=decodedline example 查看行号信息:

1
readelf --debug-dump=decodedline example

输出可能如下:

1
2
3
4
5
6
7
8
CU: example.cpp:
File name Line number Starting address
example.cpp 3 0x00000000004004b0
example.cpp 4 0x00000000004004b5
example.cpp 5 0x00000000004004ba
example.cpp 7 0x00000000004004c0
example.cpp 8 0x00000000004004c5
example.cpp 9 0x00000000004004ca

扩展

GNU binutils (GNU 二进制工具集)

GNU binutilds:https://www.gnu.org/software/binutils/

常用的:

  • ld
  • as
  • gold
  • c++filt
  • nm
  • addr2line
  • objdump
  • objcopy
  • size
  • strings
  • strip
  • gprof

线上的 C++ 服务 coredump 了,其使用的二进制没有带 debug 调试信息,我可以用本地相同代码编译出来的带 debug 信息的二进制文件来调试吗?

可以。

注意事项:

  1. 确保代码一致性:确保本地编译的代码与线上运行的代码完全一致,包括源代码、编译选项、依赖库等。

  2. 地址空间布局随机化(ASLR):ASLR 可能会影响调试过程。如果遇到问题,可以在调试时临时禁用 ASLR:

    1
    echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
  3. 确保 core dump 文件和本地二进制文件的内存地址匹配。 如果不匹配,可能需要使用 set solib-absolute-prefixset solib-search-path 等 gdb 命令来调整。