资讯动态

GDB 断点停住了,为什么还是找不到源码?用一个小程序分清符号和路径

发布时间:2026/10/30 4:08:11 来源:尧图企业网站定制
GDB 断点停住了为什么还是找不到源码调试 Redis 这样的项目配置里经常要填几处路径可执行文件、源码目录有时还要选远端程序。这几处都像是在告诉调试器“我的程序在哪”很容易混在一起。尤其遇到这种情况连接没报错断点也停了参数都能看见编辑器却说找不到源码。那到底是没连上还是路径写错了先不启动整个 Redis。我们用一个十几行的 C 程序把这个现象单独做出来。这里在同一个 Linux 环境中运行 GDB 和 gdbserver通过管道使用远程调试协议不需要开端口也不需要虚拟机、SSH 或 IDE。它用来解释配置里的两类路径不是完整的跨机器配置教程。先停在这个函数里程序保存为probe.c后面附有完整代码。核心只有这几行int double_value(int value) { return value * 2; }main调用double_value(21)最后应该打印result42。正常用gcc -g编译时除了机器指令文件里还会有调试信息。比如这个函数的参数叫value某段指令对应probe.c的哪一行。先不用记住它的格式我们马上看这些信息能做什么。为了稳定复现“源码不在原位置”编译时加一个参数-fdebug-prefix-map实际源码目录/__blog_build_not_present__/src它把调试信息中的目录前缀换成一个故意不存在的目录。源文件没有搬走程序计算也没有改变但 GDB 读到的路径变了。完整的手动编译步骤如下。在只有probe.c的实验目录里执行路径不要带空格自动化脚本不受这个手动操作约定限制。root$PWD work$(mktemp -d $root/gdb-source-XXXXXX) gcc -g -O0 -fno-inline -fno-omit-frame-pointer -fno-pie -no-pie \ -fdebug-prefix-map$root/__blog_build_not_present__/src \ $root/probe.c -o $work/probe cd $work gdb -q ./probe这里特意进入新建的工作目录里面只有二进制没有probe.c。否则 GDB 可能从当前目录找到同名源码故障就被绕过去了。root留在外层 shell 中后面还要用它找到真正的源码目录。在 GDB 里输入set debuginfod enabled off target remote | gdbserver --once - ./probe 2server.log break double_value continue print value list double_valuetarget remote | ...让 GDB 用管道与 gdbserver 通信这里的-表示标准输入输出并不是一个 TCP 地址。本次实验在断点处出现的关键输出是Breakpoint 1, double_value (value21) at /__blog_build_not_present__/src/probe.c:5 ... No such file or directory注意这两件事同时发生了参数是21源码文件却找不到。如果调试器什么都没读到它怎么知道参数名又怎么知道第 5 行所以先不用把整套远程配置推倒重来。至少在这个例子里GDB 已经得到了一部分有效信息。它知道第几行不代表它有这一行的文字我们顺着刚才的输出往下想。GDB 知道“当前指令对应某个文件的第 5 行”这是一个位置关系。要把那一行显示在窗口里还要打开对应的源文件。文件打不开位置关系本身却不一定丢失。把这一次的查找画出来停在 double_valuevalue 21 | v 调试信息probe.c第 5 行 记录目录不存在的 /__blog.../src | v 去旧目录打开 probe.c | v 失败那里没有这个文件图里缩短了目录名真实命令仍使用前面的完整前缀。那么我们缺的不是另一个端口而是一条“旧目录现在对应哪里”的说明。在 GDB 中输入第二个目录替换成前面root的实际值set substitute-path /__blog_build_not_present__/src /实际源码目录 list double_value再看这一步发生了什么原来旧目录 probe.c - 打不开 加映射旧目录换成实际源码目录 | v 实际源码目录/probe.c | v 显示 return value * 2;这次输出里就能看到3 int double_value(int value) 4 { 5 return value * 2; 6 }这一轮不需要重新编译也没有重新连接更没有改程序。我们只是让 GDB 去正确的目录找源码。set substitute-path做的是目录前缀替换匹配还要满足目录边界不能理解成任意字符串替换。更详细的规则见 GDB 的源码路径说明。那只要配好源码目录就都能调了吗再换一个条件。保持 gdbserver 运行的仍是刚才那个程序但给 GDB 读取的文件去掉调试信息objcopy --strip-debug ./probe ./probe-no-debug先在前一轮 GDB 中continue让程序正常结束再quit。然后执行上面的命令用gdb -q ./probe-no-debug开始新一轮。连接仍然是target remote | gdbserver --once - ./probe 2server.log注意两个文件名不同运行的是probeGDB 这边读取的是probe-no-debug。这是特意安排的对照不是推荐实际工程混用构建产物。这一次我们仍设置正确的源码映射仍然break double_value、continue。在本次构建中断点居然还可以停住但print value和list double_value失败了。为什么还认得函数--strip-debug去掉的是调试信息并不等于删掉所有符号。这一份文件还保留了可供定位函数入口的普通符号参数信息、源代码行的关系却不在了。因此“按函数名能打断点”也不能单独证明调试信息完整。现在即使把probe.c放到 GDB 面前它也不会自动从 C 代码反推出这些缺失信息。源码路径映射没有替代调试信息的能力。把三轮结果放一起区别就比较清楚了GDB 读取的文件源码映射本次结果带调试信息的probe没有断点停住、参数为 21源码打不开同一个probe正确参数为 21源码第 5 行可见probe-no-debug正确函数断点仍可停参数和源码行不可用三轮都继续执行到result42并正常退出。第三轮的变化在 GDB 读取的信息不在程序的计算结果。回到 Redis、IDE 和另一台机器现在再看配置里的路径可以先问两个具体问题GDB 读到的是不是与实际运行程序相匹配、含有所需调试信息的构建文件调试信息记录的源码目录在 GDB 所在环境里是否存在如果不存在现在对应哪个目录比如程序在构建机器的/build/redis/src下编译源码在你这里放到了/home/me/redis/src。路径映射解决的是这两处目录的对应关系。它不负责把另一版本的 Redis 变成这次编译使用的版本。即使窗口能显示源码拿错版本仍可能让你看着错误的行解释程序。如果连目标都连接不上先检查连接与进程如果连接和停止都正常再按已有输出检查符号与源码。这两种现象不该只因为“都叫调试失败”就使用同一套修复操作。共享库和跨架构调试还有各自的配置本例没有覆盖它们。另一个配置不能照着字面猜不要以为gdbserver 127.0.0.1:端口必然只监听回环。GNU 文档明确说明当前 TCP 地址参数的主机部分会被忽略且 gdbserver 没有内置安全保护。本例因此使用标准输入输出管道不开放调试端口。跨机器可以进一步研究文档中的 SSH 管道方式不能直接把无保护的调试端口暴露出去。见 gdbserver 官方说明。附程序和验证脚本怎么安排实验环境为 WSL 中的 Arch Linux、x86_64GCC 16.1.1、GDB/gdbserver 17.2。下面两个文件放在同一目录执行python3 verify_remote.py需要系统已有 GCC、binutils、带 Python 支持的 GDB 和 gdbserver。无需 root 权限不修改系统配置。先是完整的probe.c#include stdio.h int double_value(int value) { return value * 2; } int main(void) { int result double_value(21); printf(result%d\n, result); return result 42 ? 0 : 1; }验证脚本不会只判断进程有没有退出。它通过 GDB 内置 Python 读取value取得list的输出再检查三组结果是否符合刚才的区别。第二轮仍使用第一轮的二进制只改变映射避免用重新编译偶然修好问题来解释结果。第三轮从同一文件剥离调试信息作为“只有源码路径正确仍然不够”的对照。各轮独立启动调试会话日志和临时产物留在脚本所在目录下的新子目录不覆盖你的项目文件。还有两个小细节本实验为便于对照保留函数入口位置使用-O0、禁用内联并生成非 PIE 的小程序这不是要求 Redis 或日常项目关闭 PIE。另一个是缺源码时GDB 不一定让list抛出异常也可能返回一行文件位置同时打印警告。因此测试既检查源码正文没出现也检查实际的缺文件警告而不是只认某一种报错形式。verify_remote.pyimport hashlib import json import os from pathlib import Path import re import signal import shlex import subprocess import tempfile base Path(__file__).resolve().parent run Path(tempfile.mkdtemp(prefixgdb-source-, dirbase)) virtual /__blog_build_not_present__/src source base / probe.c binary run / probe stripped run / probe-no-debug def command(args, cwdrun): result subprocess.run(args, cwdcwd, capture_outputTrue, textTrue, timeout30) if result.returncode: raise RuntimeError(result.stdout result.stderr) return result.stdout.strip() command([gcc, -g, -O0, -fno-inline, -fno-omit-frame-pointer, -fno-pie, -no-pie, f-fdebug-prefix-map{base}{virtual}, str(source), -o, str(binary)], base) command([objcopy, --strip-debug, str(binary), str(stripped)]) def experiment(name, symbols, map_source): folder run / name folder.mkdir() log folder / server.log observed folder / observed.json script folder / commands.gdb mapping fset substitute-path {virtual} {base}\n if map_source else script.write_text( set pagination off\nset confirm off\nset debuginfod enabled off\n ffile {symbols}\ntarget remote | gdbserver --once - {shlex.quote(str(binary))} 2{shlex.quote(str(log))}\n mapping break double_value\ncontinue\npython\n import gdb, json\n data {symbolFound: gdb.lookup_global_symbol(double_value) is not None}\n try:\n data[argument] int(gdb.parse_and_eval(value))\n except gdb.error as error:\n data[argumentError] str(error)\n try:\n data[listing] gdb.execute(list double_value, to_stringTrue)\n except gdb.error as error:\n data[listingError] str(error)\n fwith open({str(observed)!r}, w) as out: json.dump(data, out, indent2)\n end\ncontinue\n, encodingutf-8) with (folder / client.log).open(w) as output: server subprocess.Popen([gdb, -q, --batch, -x, str(script)], cwdfolder, stdoutoutput, stderrsubprocess.STDOUT, start_new_sessionTrue) try: server.wait(timeout30) output.seek(0) client_log output.read() server_log log.read_text() if server.returncode: raise RuntimeError(client_log server_log) if result42 not in server_log client_log or exited normally not in client_log: raise AssertionError(Inferior did not finish with expected result) data json.loads(observed.read_text()) data.update({clientExit: server.returncode, serverLog: server_log, clientLog: client_log, commands: str(script), transport: stdio pipe; no TCP listener}) return data finally: if server.poll() is None: os.killpg(server.pid, signal.SIGTERM) try: server.wait(timeout5) except subprocess.TimeoutExpired: os.killpg(server.pid, signal.SIGKILL) server.wait(timeout5) match re.search(rcreated; pid (\d), log.read_text()) if match: pid int(match.group(1)) try: executable Path(f/proc/{pid}/exe).resolve(strictTrue) except FileNotFoundError: executable None if executable binary.resolve(): os.kill(pid, signal.SIGKILL) missing experiment(missing-source, binary, False) fixed experiment(mapped-source, binary, True) no_debug experiment(no-debug-symbols, stripped, True) if not (missing[symbolFound] and missing.get(argument) 21 and return value * 2; not in missing.get(listing, ) and No such file or directory in missing[clientLog]): raise AssertionError(missing) if not (fixed[symbolFound] and fixed.get(argument) 21 and return value * 2; in fixed.get(listing, )): raise AssertionError(fixed) if no_debug[symbolFound] or argumentError not in no_debug or listingError not in no_debug: raise AssertionError(no_debug) result {passed: True, architecture: os.uname().machine, gcc: command([gcc, --version]).splitlines()[0], gdb: command([gdb, --version]).splitlines()[0], gdbserver: command([gdbserver, --version]).splitlines()[0], binarySha256: hashlib.sha256(binary.read_bytes()).hexdigest(), sourceSha256: hashlib.sha256(source.read_bytes()).hexdigest(), virtualSourcePrefix: virtual, run: str(run), missing: missing, fixed: fixed, noDebug: no_debug} (run / results.json).write_text(json.dumps(result, indent2), encodingutf-8) print(json.dumps({passed: True, results: str(run / results.json)}, indent2))运行成功会打印results.json的位置里面保留命令、日志、工具版本和文件摘要。真正需要核对的不是一句passed第一轮有没有读到参数第二轮是否只改路径便显示了源码第三轮是否在映射正确时仍缺少参数信息。这三个现象才是这份实验要验证的内容。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑