Linux内核文件系统调试:调试器与I O栈异常定位 - 第4节
4.1 使用 GDB 加载 vmlinux 符号
调试内核的第一步是让 GDB 能够解析内核的符号表与调试信息。vmlinux 是编译后未压缩的内核映像,包含完整的 ELF 调试段(.debug_info、.debug_line 等)。通过 gdb vmlinux 启动,GDB 即可加载所有内核函数、全局变量和行号信息。
前提条件:内核编译时必须开启
CONFIG_DEBUG_INFO,否则 vmlinux 不含 DWARF 调试信息,GDB 只能看到符号地址而无法显示源码行。
4.1.1 启动与符号加载验证
# 启动 GDB 并加载 vmlinux
gdb /usr/src/linux/vmlinux
# 在 GDB 内部验证符号是否加载成功
(gdb) info functions vfs_read
All functions matching regular expression "vfs_read":
File fs/read_write.c:
ssize_t vfs_read(struct file *, char __user *, size_t, loff_t *);
(gdb) info line vfs_read
Line 438 of "fs/read_write.c" starts at address 0xffffffff812a3b00 <vfs_read>
and ends at 0xffffffff812a3b06 <vfs_read+6>
关键点:如果
info functions 返回 "No symbol table is loaded",请检查 CONFIG_DEBUG_INFO 是否开启,以及 vmlinux 路径是否正确。
4.2 设置断点:函数断点与行号断点
断点是调试的基石。在内核文件系统调试中,最常用的是函数断点和行号断点。函数断点直接命中入口,行号断点可以定位到特定代码行。
| 断点类型 | GDB 命令 | 适用场景 |
|---|---|---|
| 函数断点 | break vfs_read |
拦截文件读取入口,观察参数 |
| 行号断点 | break fs/read_write.c:438 |
定位到具体源码行 |
| 条件断点 | break vfs_write if count > 4096 |
只在大块写入时暂停 |
| 临时断点 | tbreak do_sys_open |
只命中一次,适合快速检查 |
4.2.1 断点操作示例
(gdb) break vfs_read
Breakpoint 1 at 0xffffffff812a3b00: file fs/read_write.c, line 438.
(gdb) break fs/read_write.c:460 if file->f_flags & O_DIRECT
Breakpoint 2 at 0xffffffff812a3b80: file fs/read_write.c, line 460.
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0xffffffff812a3b00 in vfs_read at fs/read_write.c:438
2 breakpoint keep y 0xffffffff812a3b80 in vfs_read at fs/read_write.c:460
stop only if file->f_flags & O_DIRECT
注意:条件断点在内核调试中开销较大,因为每次函数被调用时 GDB 都会评估条件。如果条件表达式复杂,建议先用普通断点,再手动检查。
4.3 观察点:监控内存与变量变化
观察点(Watchpoint)用于监控指定内存地址或变量的读写操作。在文件系统调试中,观察点特别适合追踪 struct file 的 f_pos 变化、inode 的 i_size 修改,或者 page 结构的 flags 变更。
4.3.1 硬件观察点与软件观察点
| 类型 | 命令 | 限制 | 推荐用途 |
|---|---|---|---|
| 硬件写观察点 | watch *addr |
数量有限(通常 4 个) | 监控关键变量写操作 |
| 硬件读写观察点 | awatch *addr |
数量有限 | 监控变量读写 |
| 软件观察点 | rwatch *addr |
无数量限制,但极慢 | 仅用于小范围调试 |
4.3.2 观察点实战:追踪 f_pos 变化
(gdb) break vfs_read
(gdb) run
Breakpoint 1, vfs_read (file=0xffff88003a1b5c00, buf=..., count=0x1000, pos=0xffff88003a1b5c08)
at fs/read_write.c:438
(gdb) watch *file->f_pos
Hardware watchpoint 2: *file->f_pos
(gdb) continue
Continuing.
Hardware watchpoint 2: *file->f_pos
Old value = 0
New value = 4096
vfs_read (file=0xffff88003a1b5c00, buf=..., count=0x1000, pos=0xffff88003a1b5c08)
at fs/read_write.c:460
460 ret = __vfs_read(file, buf, count, pos);
技巧:观察点触发后,使用
bt 查看调用栈,可以快速定位是哪个函数修改了目标变量。
4.4 单步跟踪:逐行执行文件系统函数
单步跟踪是理解内核执行流最直接的手段。GDB 提供 step(步入)、next(步过)、finish(执行到返回)三种基本操作。在内核文件系统调试中,建议优先使用 next 跳过不关心的内部调用,只在关键路径上使用 step。
4.4.1 单步跟踪流程
- 设置断点:在目标函数入口打断点,如
break do_sys_open。 - 启动执行:
continue让内核运行到断点。 - 步过:
next执行当前行,不进入子函数。 - 步入:
step进入子函数,如do_filp_open。 - 查看变量:
print file->f_path.dentry检查当前状态。 - 执行到返回:
finish快速跳出当前函数。
4.4.2 单步跟踪示例:do_sys_open 路径
(gdb) break do_sys_open
(gdb) continue
Breakpoint 1, do_sys_open (dfd=-100, filename=..., flags=0, mode=0) at fs/open.c:1080
1080 {
(gdb) next
1084 struct filename *tmp = getname(filename);
(gdb) step
getname (filename=0x7fff...) at fs/namei.c:120
120 {
(gdb) print filename
$1 = (const char __user *) 0x7fff...
(gdb) finish
Run till exit from getname
0xffffffff812a4c00 in do_sys_open (dfd=-100, filename=..., flags=0, mode=0) at fs/open.c:1086
1086 if (IS_ERR(tmp))
(gdb) next
1092 fd = get_unused_fd_flags(flags);
建议:在单步跟踪时,配合
display 命令自动打印关键变量,避免反复手动输入 print。例如:display file->f_pos。
4.5 调试会话架构图
下图展示了 GDB 调试内核文件系统时的完整会话流程,从加载符号到单步跟踪的各个阶段。
4.6 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 断点无法命中 | 内核 KASLR 导致地址偏移 | 使用 set disable-randomization on 关闭 KASLR |
| 观察点触发频繁 | 变量被频繁修改(如 jiffies) |
使用条件观察点 watch *addr if condition |
| 单步时跳入汇编 | 当前函数没有调试信息 | 确认该文件编译时包含 -g 选项 |
| GDB 响应极慢 | 软件观察点或远程调试延迟 | 优先使用硬件观察点,减少 step 次数 |
重要:在调试文件系统代码时,如果遇到
page fault 或 kernel panic,GDB 可能无法正常响应。此时应使用 kdb 或 crash 工具分析转储文件,而非继续单步。