导引:点亮什么、为什么、设计图
这一章我们要点亮什么
跑到这里,我们的 big kernel 已经能被 mini kernel 从磁盘加载、跳进来,还能用 kprintf 往串口吐字。但它有个尴尬的处境:它脚下的"段"是别人铺的。
mini kernel 为了把 big kernel 跑起来,顺手塞了一张临时 GDT 进去。那张 GDT 只够"别崩",谈不上结构。这一章,我们要让 big kernel 自己建一张正式的 GDT:7 个 entry,空段、内核代码、内核数据、用户代码、用户数据各占一个,再加上占两个槽的 TSS;然后用 lgdt 把它交给 CPU,再借一次远跳把 CS 切到我们自己写的内核代码段;等一切就位,CS 是 0x08、DS/SS/ES 是 0x10,段寄存器全部指向我们定义的段。
验证也很直接:make run-big-kernel-test 里有一组断言,直接读 CS/DS/SS/ES 的值,确认它们等于我们设的选择子。段铺没铺对,机器替你检查。
为什么现在需要它
很多人写到 long mode 会卡在一个问题上,我们先把它的皮扒掉——
"段基址在 64 位下都被忽略了,GDT 不是该退休了吗?"
没退休。long mode 忽略的是段描述符里的 base 和 limit(也就是段的起始地址和长度),但选择子(selector)这东西本身仍然必需,而且后面的几乎每一章都在踩它。
最直接的,CS 必须指向一个有效的、L 位为 1 的长模式代码段——CPU 虽然不再看 base,但它照样要检查这个描述符的属性位:是不是 present、是不是 code、是不是 64 位。这里给错了,收获的就是一个干脆利落的 #GP。再往下,特权级的切换全靠选择子最低两位的 RPL 和 CPL,后面我们要进 ring3 跑用户程序,本质就是把 CS 从 0x08 换成 0x1B,而能换的前提是 GDT 里得有对应的用户段。中断返回(IRETQ)也一样,它要恢复的 CS、SS 都是选择子,背后必须有有效描述符撑着。就连 TSS 也得挂在 GDT 里——任务切换、IST 中断栈切换,全靠 TR 指向 GDT 中的 TSS 描述符。
所以结论有点反直觉:long mode 不是不要 GDT,而是 GDT 的角色从"管内存分段"退化成了"管段属性、特权、TSS 的查表入口"。地基换了种铺法,但还是地基,这一章铺的就是它。
那 009 的时候为什么没崩?因为 mini kernel 留下的临时 GDT 恰好够 big kernel 用 kprintf 蹭两步。可一旦要碰特权级、碰中断、碰 TSS,那张临时表就不够看了——必须自己来。
外部依据:Intel SDM Vol.3A 在 Segment Descriptors 一节说明,long mode 下代码段描述符的 L 位为 1 时进入 64 位模式,此时 base/limit 被视为 0/无限。OSDev 的 Global Descriptor Table 页对"64 位下 GDT 的精简角色"有社区视角的总结。(精确章节号我们在写完后用本地 SDM 核实,见篇末参考。)
设计图
先把要建的 GDT 画出来。它是一段连续内存,每 8 字节一个 entry:
偏移 entry 选择子 access 用途
0x00 [ null ] — 0x00 第 0 项必须全 0(CPU 规定)
0x08 [ kernel code ] 0x08 0x9A 内核代码段,L=1 长模式
0x10 [ kernel data ] 0x10 0x92 内核数据段
0x18 [ user code ] 0x1B 0xFA 用户代码段(RPL=3)
0x20 [ user data ] 0x23 0xF2 用户数据段(RPL=3)
0x28 [ TSS low ] ┐ 0x28 0x89 64 位 TSS,104 字节,
0x30 [ TSS high ] ┘ 跨两个 8 字节槽两个关键点先记牢:
- 选择子 = (entry 偏移) | RPL。
0x08= "第 1 项,RPL=0";0x1B= "第 3 项(偏移 0x18)再| 0x03" =0x1B,RPL=3。选择子低 3 位里,bit 2 是 TI(0=查 GDT),bit 0-1 是 RPL。 - TSS 占两个槽。64 位 TSS 是 104 字节,一个 8 字节描述符装不下它的 64 位 base,所以用两个连续槽:一个装 limit + 低 32 位 base,一个装高 32 位 base。这就是为什么 entries 数组是 7 而不是 6。
再看一个 8 字节段描述符的内部结构:
字节: 0 1 2 3 4 5 6 7
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ limit │ base │ base │ access │ flags │ base │ (续) │ (续) │
│ low │ low │ mid │ byte │+lim hi │ high │ │ │
│ (16b) │ (16b) │ (8b) │ (8b) │ (8b) │ (8b) │ │ │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
access byte 各位(8 位): | P | DPL(2) | S | Type(4) |
flags nibble(字节高 4 位): | G | D/B | L | AVL |long mode 代码段的标志组合是 access=0x9A、flags=0xA(G=1, L=1)。下面我们把这几个数一行行算给你看,不是背下来的。