一张三项的扁平 GDT
进了保护模式,段寄存器的身份就换了。实模式里它装的是段基址,自己左移四位就能用了,而保护模式里它装的是一个叫段选择子的索引,CPU 拿着它去查一张共享的表,查回来的条目里才有段的基址、限长和权限。它的名字就是全局描述符表(GDT)。咱们本站要建的,是它的一个相当袖珍的版本,三个条目也就够用了:
偏移 条目 选择子 access base/limit 用途
0x00 null — 0x00 base=0,lim=0 必须全零
0x08 code 0x08 0x9A base=0,lim=4GB 32 位代码段
0x10 data 0x10 0x92 base=0,lim=4GB 32 位数据段选择子的算法简单得出奇:它就是条目在表里的偏移,再或上一个 RPL——咱们用的是内核特权级 0,所以 RPL 项填了零,或上去的效果等于没有。咱们看表里住在偏移 0x08 的那一项,它的选择子就是 0x08。住在 0x10 的那一项,选择子也就成了 0x10。至于开头的 0x00 那一项,CPU 规定它必须是全零的,它谁也不描述、只是把表头的位置占住了——历史上往零选择子上撞,是保护模式里一类经典翻车的来源,咱们干脆不给它任何用途。
真正值得咱们端详的,是表整身的取向:base 全是零,limit 全开到了 4GB。这样的排法就叫扁平模型了。您可能会问,保护模式不是靠段来做保护的吗,怎么咱们一上来就把段铺得平平的,好像什么都没保护?
您问得对,不过咱们要看清楚的,是保护换在了哪儿。实模式的不设防,在于任何人拿任何地址都能直接落进内存,CPU 是无从过问的,而保护模式下,哪怕段是平的,每一次访问也都要过一遍表里写明的权限。您拿数据段选择子 0x10 去当代码执行,CPU 当场就给您一个异常,而限长也在表里写着,越界的访问同样过不去。咱们现在做的,是把权限立起来的这一层,同时也主动放弃了“用段把内存切块”的能力。为什么不切块?因为 x86 上还有一层更强的机制等着接班,也就是分页了。段的切块功能又老又难用,内核真正的内存管理走的是分页的路,而分页要的恰是一个平坦的段:地址铺平了,分页才有干净的起点。
扁平的直接红利,咱们马上就能吃到:base=0,意味着线性地址就等于偏移自己,而 limit=4GB,意味着整片 32 位的地址空间都在段内。从此咱们写下的每一个地址,它自己就是它的线性地址了,再也不用做任何拼算了。上一站咱们读模式列表时把远指针拍平的那套乘加,进了新世界就彻底成为历史了。
那表里为什么只有三项呢?因为引导阶段的需求就只有两段:一段装的是代码,一段装的是数据。后面内核真要立用户态、要 TSS 的时候,会自己重建一张大得多的表,大表里的事跟 boot 没关系。咱们也不给 64 位预留任何段——长模式用的段,格式是另说的,到时候重建就行了。所以眼前小表的使命,就是护送咱们过门槛的,用到下一站开工就足够了。
OSDev 的 GDT 页上摆着最小表的通行写法和各字段的位定义,跟咱们的三项一一能对上。至于字节层面怎么用 C++ 落地,咱们下一节亲手拼。