外观
traps:沿栈找回调用者,再实现用户定时处理函数
版本:traps,提交 4270ccc3d1a60f3e0645933a11e275c6e212808d。本实验将“保存了哪些状态”变成可以运行检验的问题。先读异常与中断,尤其注意 2025 的 prepare_return 路径。
汇编热身:不要从源码猜机器行为
构建 user/call.asm,定位 g、f、main。记录参数寄存器、jal/jalr 的目标、ra 的含义。若找不到某个函数调用,先考虑编译器内联,而不是断言反汇编缺失。若 printf 格式需要两个数而调用只给一个,额外结果没有确定值;C 调用协议不会凭空补出一个合理数字。
字节序例子可以手算:整数 0x00646c72 在小端内存依次为 72 6c 64 00,按字符串读就是 rld。按整数格式打印与按 char 指针打印,解释的是同一存储的不同视图。数值本身与其内存字节排列必须分开。
backtrace:有界地走 frame pointer 链
在 kernel/printf.c 实现 backtrace,读取 s0,然后按照本实验栈布局读取保存的 ra 和上一个 fp。把当前栈页的上下界先保存下来;每轮验证当前 fp 足以容纳要读的字段、处于同一栈范围、下一个 fp 沿预期方向前进。不要仅仅检查 fp 非零就盲目追下去。
插入 sys_pause(),用 bttest 验证;再把地址送给本次构建对应的 addr2line -e kernel/kernel。得到的是调用链中的返回位置,不保证是调用指令自身那一行。编译器优化还可能使源代码行显示在相邻语句。
这个任务的关键不变量是“不为打印诊断信息再制造一次非法访问”。接入 panic 后尤其如此:原始故障必须保留,不能被无界 backtrace 的二次崩溃掩盖。
Alarm 状态机
建议把进程字段分成配置和运行状态:配置包含 interval 与 handler;运行状态包含累计 ticks、是否正在 handler、备份的 trapframe。所有字段在 allocproc 初始化,销毁时清理;备份若单独分配,所有失败分支也要回收。
stateDiagram-v2 [*] --> Disabled Disabled --> Counting: sigalarm(n>0) Counting --> Counting: timer tick 未到期 Counting --> Handling: 到期且不在 handler Handling --> Counting: sigreturn 恢复状态 Counting --> Disabled: sigalarm(0,0)
查看流程图文本
stateDiagram-v2 [*] --> Disabled Disabled --> Counting: sigalarm(n>0) Counting --> Counting: timer tick 未到期 Counting --> Handling: 到期且不在 handler Handling --> Counting: sigreturn 恢复状态 Counting --> Disabled: sigalarm(0,0)
只在正确的时钟事件路径计数。到期时先备份完整用户寄存器与原 epc,再标记正在处理,最后将返回 epc 指向 handler。先改 epc 再备份会保存到错误的返回位置。处理期间仍可能发生 trap,但不能再次覆盖原备份。
sigreturn 恢复备份、清除 handling、重新开始计数,然后以不会破坏原 a0 的方式离开系统调用分派。通用 syscall 会把实现返回值写进 trapframe.a0,因此这一接口的返回值应与被恢复的 a0 协调,而非普通地 return 0。
分阶段实现与验证
第一阶段只让 alarmtest 编译:声明接口、生成桩、编号、分派。第二阶段让 test0 出现 alarm,证明 handler 调度成功。第三阶段恢复完整状态,让被打断计算得到正确结果。第四阶段禁止重入,通过长 handler 场景;第五阶段验证关闭 alarm、handler 位于地址 0、恢复 a0 等边界。
调试控制流可用 make CPUS=1 qemu-gdb 降低干扰,但最终仍要按正常配置运行测试。观察每次到期前的 epc、handler、保存区地址和 a0;日志要限量,否则大量串口输出会改变时序。
常见症状对应关系:只打印一次就崩溃,先看返回 PC;计算结果偶尔错误,先看漏存寄存器;handler 不停递归,先看 handling;test3 失败,关注 a0;所有程序都异常,检查是否误改了公共 trap 返回字段。
自测:既然 C 调用只要求保存 callee-saved 寄存器,alarm 为什么保存更多?
普通调用发生在编译器知道的调用点,调用者会按 ABI 安排活跃值。中断可以发生在任意指令边界,被打断代码没有机会提前保护 caller-saved 寄存器,因此必须恢复整个用户执行现场。
验收:bttest、alarmtest、usertests -q、make grade。来源:2025 traps 官方任务,对应 trap.c/printf.c/proc.h/proc.c。