gdb调试信息分离
八月 12, 2025 更新: 八月 26, 2026 [C/C++] #gdb调试信息分离
GDB的调试信息分离机制
GDB 允许程序的调试信息和可执行文件分离。当可执行程序出现问题时,GDB 会自动查找和加载调试信息。
- 这种机制的实现原理是,GDB 通过两种方式来指定独立调试信息文件:
- 一种是可执行文件包含一个调试链接,它会指定独立调试信息文件的名称;
- 另一种是可执行文件包含一个构建标识符,它是一个唯一的位串,在相应的调试信息文件中也包含这个位串?
调试链接详解
- 调试链接包含两个关键信息:
- 调试信息文件的名称和 CRC 校验和。 调试信息文件的名称通常为executable.debug,其中executable是相应的可执行文件的名称,不带有前导的目录名,方便查找。 CRC 校验和的目的是为了验证可执行程序和调试文件是否匹配。
- 在生成调试链接时,会将调试信息文件的名称和 CRC 校验和添加到可执行文件中
- 当 GDB 调试时,会根据这些信息来查找和加载调试信息。
- 例如,在编译生成release with debug info时,它包含调试信息,然后把它进行调试信息分离,生成一个可执行程序(用a代替)和调试信息文件(用b代替)。
- 接下来把调试链接做出来,就是在可执行程序a当中,加上一个链接,这个链接信息包含调试信息文件b的名字和 CRC 校验和 。
以 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 的粒度:
objcopy --strip-debug/strip --strip-debug:只去掉调试段,保留.symtab符号表 → gdb 的bt还能解析出函数名(如#0 0x... <abort>),info line则报No line number information available;strip -s/strip --strip-all:连符号表也删 →bt只有裸地址。
实测(同一二进制,无/有 .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 至少能粗定位调用链(函数名),定位到行必须带符号文件。
2. .gnu_debuglink 里只有文件名,没有路径
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 按以下顺序,找不到就放弃:
- 二进制同目录下的同名
.debug—— 这正是部署机的调试姿势:把.debug拷到 bin 同目录,gdb bin/xxx <core>自动加载,无需任何参数; 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/@/...
- rustc(crate 源码):Cargo 传相对路径 → DWARF 记录相对路径(
crates/.../src/main.rs,相对容器编译目录)→gdb -d <源码仓库根>直接对上; - musl libc 等 C/汇编:交叉编译器按绝对路径编译 →
comp_dir是/build/musl-cross-make/...(构建机路径)→ 部署机上没有这份源码,可用set substitute-path /build/musl-cross-make/... /your/srcdir映射。
实际上只调试 Rust 栈帧的话,C 帧显示地址没有源码也能接受。
4. 一个坑:.debug 必须与同次构建的 binary 配套
--add-gnu-debuglink 写入的是文件名 + CRC 校验和,objcopy/gdb 加载时会校验匹配。所以:
- 换了二进制必须重新构建/重新提取同次产出的
.debug,不能用旧包的.debug配新 binary; - 变量级调试(
CARGO_PROFILE_RELEASE_DEBUG=2生成全量 DWARF)同样要重建配套 pair——debug 等级只是信息量不同,关联机制一样。
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 之后执行)。