前言
写完 xv6 的 find 用户程序后,我想用 GDB 打个断点单步跟一下。结果在打断点的时候,GDB 直接报了下面这两行提示:
No source file named /Users/penry/Documents/code/my-xv6-learn/user/find.c.
Breakpoint 1 (-source /Users/penry/Documents/code/my-xv6-learn/user/find.c -line 14) pending.看到 No source file named ... 的时候我挺纳闷的:这个文件明明就在硬盘上,Neovim 里也正打开着,为什么 GDB 死活说找不到?
一开始我以为是 GDB 的源码搜索路径配错了,后来理清楚才发现,根本原因其实很简单:我只加载了 xv6 内核的符号表,压根没把 _find 用户程序的符号表加进去。
重要
问题原因 GDB 打源码断点并不是直接去硬盘上找同名
.c文件,而是要先查已加载二进制(ELF)里的调试符号表,把“文件名 + 行号”算成内存地址。如果只加载了内核kernel/kernel,GDB 的符号表里自然没有user/find.c。
区分源码与 ELF 符号文件
在 xv6 里,内核和用户程序是分开编译、各自链接成独立 ELF 文件的:
user/find.c:我们平时编辑的 C 语言源码。user/_find:编译出来的用户程序二进制,里面带着find.c的调试符号。kernel/kernel:xv6 内核二进制,只包含内核自己的符号。
平时配调试环境(参考之前的 xv6 的 macOS 环境准备)时,默认的 program 指向的是 kernel/kernel。这样从系统启动开始跟内核很方便,但 GDB 不会因此自动把 user/ 底下十几个用户程序的符号全都读进来。
检查已加载的符号表
遇到找不到源文件时,可以先在 GDB 控制台里敲这几条命令看看底细:
info files
info sources
info functions ^find$info files:看当前加载的是哪个可执行文件。info sources:看当前符号表里认识哪些.c源文件。
注意
注意 千万不要在 GDB 里直接敲不带参数的
file命令!它不是用来查看当前文件的,而是会直接把当前已加载的可执行文件和符号表清空(详见 GDB 文件命令说明)。
如果 info sources 列表里压根就没有 find.c,那就不用去折腾什么源码路径映射了,先把用户程序的符号补上才是正事。
加载用户程序符号表
在 xv6 仓库根目录下启动 GDB 后,可以把 user/_find 的符号追加进来:
add-symbol-file user/_find
info functions ^find$
info line user/find.c:15这里用的是 add-symbol-file 而不是 symbol-file。因为 symbol-file 会把原有的内核符号替换掉,而 add-symbol-file 是在保留内核符号的基础上把 _find 的符号补进去,方便同时看内核和用户程序。
我在本地不连 QEMU 的情况下直接用命令行跑了一次验证:
riscv64-elf-gdb -q -nx -batch \
-ex 'file kernel/kernel' \
-ex 'info functions ^find$' \
-ex 'add-symbol-file user/_find' \
-ex 'info functions ^find$' \
-ex 'info line user/find.c:15' \
-ex 'break user/find.c:15' \
-ex 'info breakpoints'对比非常明显:只加载 kernel/kernel 时,查不到 find 函数;执行 add-symbol-file user/_find 之后,GDB 马上就能识别出 user/find.c,把第 15 行解析到 0x0 地址,断点也顺利打上了。
确认断点对应的指令行号
回头看最开始的报错,当时断点打在了第 14 行。我用本地的 user/_find 查了一下发现,第 14 行正好停在函数定义开头,本身没有生成对应的机器指令,第 15 行才有实际的指令地址(参考 GDB 断点文档)。
平时打断点前,如果不确定哪一行对应真实代码,可以先用 list 和 info line 看一眼:
list user/find.c:15
info line user/find.c:15
break user/find.c:15
info breakpoints另外,改完 .c 代码后一定要记得重新编译生成 user/_find 和 fs.img,光在编辑器里保存代码是不会更新二进制里的符号信息的。
配置源码搜索路径映射
只有当 info sources 里明明已经有 find.c 了,但 GDB 在显示代码时提示打不开文件(比如二进制是在别的机器或者容器里编译的,记录的绝对路径和本机不一样),才需要用 directory 或 set substitute-path 去改路径映射(参考 GDB 源码路径文档):
show directories
show substitute-path假设二进制里记录的旧编译目录是 /old/build/my-xv6-learn,而本机源码在 /Users/penry/Documents/code/my-xv6-learn,可以这样替换前缀:
set substitute-path /old/build/my-xv6-learn /Users/penry/Documents/code/my-xv6-learn但在本机自己编译、自己调试 xv6 的情况下,路径本来就是一致的,一般不需要动这项设置。
QEMU 远程调试注意事项
本地符号能解析出来之后,连上 QEMU 实际跑的时候还有几件事要留心:
- 先用
make print-gdbport看一眼当前项目的调试端口(我的环境算出来是25501),再用make CPUS=1 qemu-gdb启动虚拟机。 - QEMU 的 GDB 挂接的是整台虚拟机。刚启动时系统还在跑内核和
sh,直到你在 xv6 的 shell 里敲下find命令,CPU 才会真正进入find进程的用户地址空间。 - xv6 的用户程序默认都从虚拟地址
0x0开始链接,如果之前给别的用户程序打过0x0附近的断点,记得先把旧断点清掉,免得串台。