Skip to content

内核调试:从第一个异常状态开始,而不是从最后一次 panic 猜起 ​

先固定实验条件 ​

内核调试最容易浪费时间的原因,是代码、镜像、测试和运行参数来自不同版本。每次重大改动前记录上游 SHA,切换实验分支后 make clean,确认当前 LAB 宏与预期一致。文件系统布局变化后重建 fs.img;需要保留旧磁盘状态时使用 qemu-fs,并清楚标注它不是干净镜像。

将问题分成四类:构建失败、启动失败、功能失败、时序失败。编译器找不到声明通常与系统调用接线有关;启动前 panic 常涉及页表和分配器;只在某个用户测试失败可沿该测试最小化;仅多 CPU 或重复运行失败,优先检查生命周期与并发,而不是改输出格式。

scause、sepc、stval 三件套 ​

sepc 指向相关指令位置,stval 对页故障通常给出访问地址,scause 区分原因。用户 ecall 为 8,load page fault 为 13,store/AMO page fault 为 15;中断原因还有高位标记,不能把所有 scause 当普通整数异常号处理。

例子:scause=15, stval=0x4000,说明某条存储对 0x4000 的翻译或权限失败。下一步应找出 sepc 对应的存储指令、相关寄存器值和 0x4000 的 PTE。若它是合法 COW 页,处理后应重试;若是普通只读文本,杀进程才是正确行为。只看到 page fault 就给页面加 W,会把故障修成隔离漏洞。

反过来,内核 scause=13, stval=0 常见于内核空地址读取。即使用户页表映射了地址 0,当前内核页表也未必映射;这能帮助你定位误解引用用户指针的错误。

用同一次构建的符号解释地址 ​

sh
riscv64-unknown-elf-addr2line -e kernel/kernel 0x你的地址
riscv64-unknown-elf-objdump -S kernel/kernel

地址来自哪次运行,就必须用哪次构建的 ELF。不要重新编译后再用新 kernel 解释旧 panic 地址。对用户程序则选择 user/_程序名,不是 kernel/kernel。反汇编能说明指令,源代码行只提供附近上下文;优化后几条 C 语句可能合并成一段机器码。

GDB 基础流程:宿主先 make qemu-gdb,再启动匹配 RISC-V 的 GDB 并按仓库配置连接;设置 b syscall 或 b usertrap 后 continue,使用 p/x 查看寄存器与结构体,x/i 查看指令,bt 查看栈。若工具前缀不同,按本机 toolchain 调整名称。

调试 COW:观察映射和所有权 ​

每条有限量日志包含 pid、页对齐 VA、PA、flags、refcount、操作类型。只打印 fork happened 无法判断是哪一页损坏。对同一页画时间线:分配=1;fork=2;child fault 后 old=1/new=1;两个进程退出后均为零。

若出现 refcount 为负,先查启动 freerange 与重复 kfree;若非零页出现在 freelist,查计数锁与回收顺序;若 forkfork 后共享页变成普通只读,查 flags 是否丢失 COW;若文件读取到缓冲区失败而普通用户写成功,查 copyout。

调试锁与驱动:不要让打印成为新行为 ​

串口输出持有自己的锁,并且很慢。在线程争用或中断路径加入大量 printf,可能掩盖竞争,也可能制造新的锁顺序。优先计数和固定大小事件记录,触发失败后再查看;必要时限制只记录某个页或某个进程。

单 CPU 很适合观察 trap 控制流,却会隐藏真正的跨核竞争。锁测试的 CPU 数还可能是协议的一部分:本年 rwlktest 使用四核屏障。把它改为一核后卡住,不能立刻判定读写锁算法错误。网络只在环绕后失败,则记录描述符 index、DD、buffer 地址、TDT/RDT,而不是重复检查 IP 字符串。

一个可复用的排错循环 ​

第一步保存失败输入与原始输出。第二步写一句预期不变量,例如“已解除的页不应仍有有效 PTE”。第三步在最接近状态改变的位置观察,而不是只在 panic 处观察。第四步只修改一个假设对应的代码。第五步重跑最小失败样例,再跑受影响的回归测试。

失败后加一个特判让样例通过通常很诱人。先问这个特判是否解释了机制:为什么这个地址特殊,为什么这个计数应该被忽略,为什么这个测试允许丢数据?如果答案只是“否则测试失败”,它更可能掩盖最早的错误。

自测:为什么重新运行后 panic 地址变化,不代表根因变化?

代码布局、调度和受损对象可以不同,最后触发非法访问的位置也会变化。根因可能仍是早先一次 use-after-free 或引用计数错误。应追踪第一个不变量被破坏的位置,而不是只比较最后的 PC。

自测:测试预期杀死子进程时,usertrap 打印一定意味着失败吗?

不一定。mmap 解除后访问、只读页写入等测试故意触发异常。要结合测试名称、预期退出状态和最终断言判断,不能把所有异常文本都当成缺陷。

来源:官方 debugging guidance、syscall GDB 练习及固定分支代码。总检查表见实验验收。