调试现场
这一章有两份真实笔记,都值得当案例,因为它们都是「看起来能跑、实际埋雷」的典型。
案例一:线程退出炸成 0xDEADC0DE
生产里两个线程交替打 5 轮后崩溃,串口吐出 RIP=00000000deadc0de,然后三重错误重启。根因是两个 bug 叠加。
其一是 TaskBuilder 当初没在栈顶压返回地址——线程函数 return 时 ret 弹空栈,一路弹到栈底那个 0xDEADC0DE 哨兵,把它当地址跳过去,CPU 跳到 0xDEADC0DE 当然炸。这正好解释了为什么现在 build() 里非要有那句「栈顶压 exit_current」:它就是给线程函数的 return 准备的落脚点。修复一:栈顶压 exit_current。
其二是就算修了第一个 bug,exit_current 本身还有毛病:它先把 current_ 改成下一个任务,再调 context_switch(¤t_->ctx, &next->ctx)——可这时 current_ 已经是 next 了,from 和 to 指向同一个任务,切换成了空操作,执行继续停在已经死亡的线程栈上,该崩还是崩。修复二:进 exit_current 第一件事先 Task* prev = current_;,用 prev 当 from,保证 from != to。两个 bug 都修,两个线程才能干净利落地各打 5 轮、各打一句 done、各自 [SCHED] ... exited,最后队列空了打 No more tasks, halting.。
案例二:higher-half 没扶正,进程隔离失效
上面代码路线讲过:旧加载器剥掉 higher-half 偏移,让内核跑在恒等映射的 PML4[0](用户半区),破坏了「内核待在共享高半区」的设计,导致地址空间之间页表子树泄漏。这条 bug 在 019「只有内核线程、还没真造多个用户地址空间」时不会立刻发作——因为演示的两个线程共享内核地址空间。但它是颗定时炸弹:一旦 020 之后真给进程造独立地址空间,这颗雷就会以「A 进程的页表项莫名出现在 B 进程里」的形态爆出来。019 把加载器扶正,等于在雷爆之前拆了引信。这种「当下不发作、但迟早要命」的 bug,是做内核时最值得记进笔记的一类。