导引:点亮什么、为什么、设计图
这一章我们要点亮什么
我们要让 big kernel 第一次拥有"扛事"的能力。落到一个看得见的效果上:在 main 里执行一句 int $3 触发断点异常,串口打印出完整的寄存器 dump,然后——这是重点——内核没死,它从异常里返回,接着往下跑,吐出一句 Breakpoint returned, continuing。
听起来不起眼,但对一个裸奔的内核来说,这是从"一碰就碎"到"能自我诊断"的分水岭。没有这层兜底,后面随便一个 #PF、#GP 都会换来一次莫名其妙的重启,连死因都看不着。
为什么现在需要它
009 时候的 big kernel,本质上是个只会 kprintf 的裸奔程序。它脚下的临时 GDT 够它蹭两步,可 CPU 一旦抛出任何异常——除零、缺页、非法指令——没有 IDT 就意味着没有 handler,CPU 找不到落点,连续触发下去就是三重故障,QEMU 直接重启。调试这种状态是噩梦,因为你根本不知道自己错在哪。
我们要做的,是在内核和"彻底崩溃"之间塞进一张安全网。这张网就是 IDT,中断描述符表:它告诉 CPU,"当异常 X 发生时,去调用这个函数"。而 IDT 里每个 gate 最终都要跳进一段代码段去执行——这就用上了上一篇铺好的 GDT 选择子。所以两篇的顺序是锁死的,GDT 先就绪,IDT 才接得上。
设计图
先看 IDT gate 长什么样。它和 GDT 的段描述符不一样,是 16 字节一个,干的事就一件:把"异常发生时该跳去哪"编码进去。
字节: 0 2 4 5 6 8 12 16
┌─────────┬────────┬─────┬──────────┬────────┬─────────┬─────────┐
│ offset │selector│ IST │ type_attr│ offset │ offset │ reserved│
│ low │ (CS) │ │ │ mid │ high │ │
│ (16b) │ (16b) │(8b) │ (8b) │ (16b) │ (32b) │ (32b) │
└─────────┴────────┴─────┴──────────┴────────┴─────────┴─────────┘handler 的地址被拆成三段(低 16、中 16、高 32)塞进 gate,因为一个 64 位地址没法直接放进连续字段;selector 是这个 handler 要跑在哪个代码段,就是我们上一篇的 GDT_KERNEL_CODE;IST 决定它用哪个中断栈;type_attr 编码"这是个什么 gate"。
异常一来,CPU 干的事大致是这样一条链:先把 SS/RSP/RFLAGS/CS/RIP(再外加可能的 error code)压进栈,然后按 IDT[向量号] 找到 gate,把 gate.selector 装进 CS,跳去 handler。handler 那头是我们写的汇编 stub,它保存通用寄存器,把栈指针当参数传给 C 函数,C 函数读完现场再返回,stub 恢复寄存器、跳过 error code、iretq,CPU 就把刚才压的那些东西弹回去,回到被打断的地方接着跑。整条链我们下面逐段拆。