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 单步跟踪流程

  1. 设置断点:在目标函数入口打断点,如 break do_sys_open。
  2. 启动执行:continue 让内核运行到断点。
  3. 步过:next 执行当前行,不进入子函数。
  4. 步入:step 进入子函数,如 do_filp_open。
  5. 查看变量:print file->f_path.dentry 检查当前状态。
  6. 执行到返回: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 调试内核文件系统时的完整会话流程,从加载符号到单步跟踪的各个阶段。

GDB 内核调试会话流程 GDB 内核调试会话流程 加载 vmlinux 设置断点/观察点 continue 运行 命中断点暂停 next/step 单步 print 检查变量 循环单步直到定位问题 quit 退出调试 说明: 1. 加载 vmlinux 后,GDB 可识别所有内核符号与源码行号。 2. 断点/观察点设置后,通过 continue 让内核运行到命中点。 3. 单步跟踪时,next 跳过子函数,step 进入子函数,finish 快速返回。

4.6 常见问题与避坑指南

问题 原因 解决方案
断点无法命中 内核 KASLR 导致地址偏移 使用 set disable-randomization on 关闭 KASLR
观察点触发频繁 变量被频繁修改(如 jiffies) 使用条件观察点 watch *addr if condition
单步时跳入汇编 当前函数没有调试信息 确认该文件编译时包含 -g 选项
GDB 响应极慢 软件观察点或远程调试延迟 优先使用硬件观察点,减少 step 次数
重要:在调试文件系统代码时,如果遇到 page fault 或 kernel panic,GDB 可能无法正常响应。此时应使用 kdb 或 crash 工具分析转储文件,而非继续单步。

4.7 本章小结