gdb调试信息分离

八月 12, 2025 更新: 八月 26, 2026 [C/C++] #gdb

调试信息分离

GDB的调试信息分离机制

GDB 允许程序的调试信息和可执行文件分离。当可执行程序出现问题时,GDB 会自动查找和加载调试信息。

调试链接详解

以 Redis 为例的调试流程

编译带调试信息的可执行文件

在编译 Redis 时,我们可以使用-O2或-O3优化选项来优化代码,提高运行效率,同时使用-g选项添加调试信息,-DENDEBUG用于禁用断言(在 Release 版本中通常会禁用断言以提高性能)。

make CFLAGS="-O2 -g -DENDEBUG"

这样就会生成一个包含调试信息的 Redis 可执行程序,虽然文件大小可能会有所增加,但为后续的调试提供了便利。

转存调试信息

我们使用objcopy工具将调试信息转存到一个新的文件中。命令如下:

objcopy --only-keep-debug ./src/redis-server redis-server.debug

这条命令会把./src/redis-server中的调试信息提取出来,保存到redis-server.debug文件中。

此时,redis-server.debug装满调试信息,里面包含了变量名、函数名、行号等重要信息。

去除可执行程序调试信息

为了得到一个 “干净” 的、不包含调试信息的可执行程序,我们使用strip命令:

strip ./src/redis-server -o redis-server

经过这一步,redis-server就变成了一个体积更小、运行效率更高的裸可执行程序, 但缺少了调试所需的信息。

添加调试链接

最后,我们要为这个裸可执行程序添加调试链接,让它能够找到对应的调试信息文件。使用objcopy命令:

objcopy --add-gnu-debuglink=redis-server.debug redis-server

这条命令会在redis-server中添加一个调试链接,链接到redis-server.debug文件。

此时,redis-server虽然不包含调试信息,但它知道从哪里获取调试信息,为后续的调试做好了准备。

用 GDB 调试

现在,我们就可以使用 GDB 来调试这个带有调试链接的可执行程序了。

启动 GDB 并加载redis-server:

gdb redis-server

GDB 会自动根据调试链接找到redis-server.debug文件,并加载其中的调试信息。

在 GDB 中,我们可以使用各种调试命令,如设置断点(break)、查看变量值(print)、单步执行(step)等,从而定位和解决程序中的问题。例如,我们可以在某个函数处设置断点,查看程序执行到该点时变量的值,分析程序的执行逻辑是否正确。

生产环境实测补充

上面这套流程算是"原理篇"。2026-08 在 wfuzz-utils 里做 release 构建 + 符号分离 + 部署机 core 调试时,实测补充了下面几个容易踩的结论(构建流程:CARGO_PROFILE_RELEASE_DEBUG=1 生成行号级 DWARF → --only-keep-debug 剥离到包外 .debug → --add-gnu-debuglink 关联 → 发布 tar 包内不放符号文件)。

1. 没有 .debug 文件时,bt 还能用吗?

能,但只有函数名,没有行号。 关键区别在 strip 的粒度:

实测(同一二进制,无/有 .debug 对照):

$ gdb -batch -ex 'file message-queue-enhance' -ex 'info line *0x348b31'
# 无 .debug:No line number information available for address 0x348b31 <abort>
# 有 .debug:Line 10 of "../src_musl/src/exit/abort.c" starts at address 0x348b31 <abort>

也就是说:没有 .debug 至少能粗定位调用链(函数名),定位到行必须带符号文件。

objdump -s --section .gnu_debuglink <bin> 看到的只有文件名 + 4 字节 CRC32,没有路径:

Contents of section .gnu_debuglink:
 0000 6d657373 6167652d 71756575 652d656e  message-queue-en
 0010 68616e63 652e6465 62756700 c9ef7c32  hance.debug...|2

所以 GDB 查找 .debug 按以下顺序,找不到就放弃:

  1. 二进制同目录下的同名 .debug —— 这正是部署机的调试姿势:把 .debug 拷到 bin 同目录,gdb bin/xxx <core> 自动加载,无需任何参数;
  2. debug-file-directory(gdb -d <dir> / set debug-file-directory <dir>)指定的目录。

3. 单步调试还需要源码

.debug 里只有行号表和变量描述,GDB 显示代码行、next/step 定位都依赖读取真实的源码文件。且 DWARF 里的路径是编译时的——rustc 和 C 编译器记录方式不同:

$ readelf --debug-dump=info <bin>.debug | grep -m1 DW_AT_comp_dir
DW_AT_comp_dir : /build/musl-cross-make/build/local/x86_64-linux-musl/obj_musl
DW_AT_name     : crates/message-queue-enhance/src/main.rs/@/...

实际上只调试 Rust 栈帧的话,C 帧显示地址没有源码也能接受。

4. 一个坑:.debug 必须与同次构建的 binary 配套

--add-gnu-debuglink 写入的是文件名 + CRC 校验和,objcopy/gdb 加载时会校验匹配。所以:

5. CRC 校验有多严?被拒载后的两条出路

沿 .gnu_debuglink 的自动加载是字节级校验:.debug 改掉一个字节就拒载(实测):

warning: the debug information found in "…/full.debug" does not match "…/bin" (CRC mismatch).

且这条路径在 file <bin>、gdb -p <pid>(attach,gdb 自动从 /proc/<pid>/exe 识别二进制并沿 debuglink 查找)都生效——结论都一样:同目录 + 未修改 + 配套才自动加载。

绕不过只能"换路"——显式加载符号文件不做任何 CRC 校验(gdb 信任用户的显式指定):

# 等价两种写法,注意顺序:symbol-file 必须在 file 之后,否则符号会被 file 重置
gdb <bin> -ex 'symbol-file <任意路径>/xxx.debug' <core>
gdb -p <pid> -ex 'symbol-file <任意路径>/xxx.debug'
# attach 场景同样可以先 attach 再 symbol-file,绕过同目录/同名/CRC 限制

实测:改过一字节的 .debug,用 symbol-file 挂载照样 Line 2 of "foo.c" 正常出源码行。代价是——不配套的风险从"机制强制"变成"自己负责":符号文件与二进制不是同一构建时会行号错位,这是 CRC 原本保护的东西。

命令行 --symbols=FILE 版注意一个坑:gdb --symbols=full bin 若随后执行 file <bin>,符号会被 file 重置丢失;安全写法是 / 运行时 symbol-file -ex 命令(在 file 之后执行)。

参考

gdb调试