桥、门闩,和推门的那一下
咱们现在住在 32 位的世界里,小日子其实过得不坏:段是平的,地址也就是它自己的线性地址,4GB 的空间随便咱们走,栈安在了 0x90000,debugcon 也一行一行地打着字。可上一卷收尾时撂下的那句话还热乎:保护模式只是咱们的中转站,咱们真正要去的 64 位还在前头。咱们把要去那边的理由摆正。当然不是嫌 4GB 不够花,虚拟机拢共也就给了 128MB 的内存。真正的理由其实是:咱们要交给执行流的那个内核,是一个 64 位的内核,而它将来的伙伴,从驱动、用户程序到整个上层的生态,全都默认住在 64 位的世界里。桥通了,对岸才开得了工。
64 位的正式名字,咱们从这儿起叫它长模式。它是 AMD 在 x86-64 架构里定下的设计,Intel 后来照着兼容了。咱们在这一卷里不背它的完整家谱,只看跟眼下的活有关的一条脾气:长模式是架在分页上的。32 位的保护模式里,分页是可选的,上一卷咱们不开它,日子也照样过了。长模式就没有这个选项了。而要是没有页表,咱们连关着分页的 64 位都到不了,因为这样的状态是根本不存在的——这话的门道,咱们到讲门闩的那一篇里再看清。所以说,页表其实是路本身,算不得额外的工程量。而上一卷虽然进了保护模式,咱们还得再多走一站:河上的桥,咱们那时候还没影。
咱们把本站的活计再排一遍,四样家当其实是一根链条上的四个环节。头一件是铺桥:咱们在 0x1000 起的三张表里建一个最小组,把低区的内存原样铺过去。第二件是安门闩:长模式的开关不住在 CR0、CR4 这些老地方,它住在一个叫 EFER 的寄存器里,而那个寄存器,咱们得用一对没见过的指令去读写。第三件是推门:CR0 上的 PG 位置起来的那一拍,分页就生效了,长模式也跟着激活了,CPU 从这一拍起就按新的翻译取指令。第四件是远跳:咱们带着一个 64 位的代码段选择子跨过去,译码宽度才算真正换到了 64 位,而它跟上一卷 PE 与远跳是同一对动作,只不过这回门里门外隔着的,是一整套的地址翻译。
推门的那一下,值得咱们提前掂一掂分量。PG 亮起的瞬间,CPU 就要拿正在执行的下一条指令的地址,去走咱们刚立好的表了。页表里要是铺好了路,执行流就纹丝不动地继续。而一旦没有路,一次翻译的失败就是异常,咱们手里又没有中断表,所以异常一来就没人接,再往上走就是三重故障了,机器就直接复位了。上一卷咱们也提过三重故障,这一卷它离咱们更近了:本站的每一步都站在它的射程里,而错一个位或者错一步序,得到的回应全都一样:QEMU 的窗口安安静静地没了,连一个字的解释都不给咱们。所以本站的节奏也跟着变了:能在编译期查的,咱们全升成了断言。能在 host 上验的,也全进了单测里。留给 QEMU 里跑着的机器的,只剩那几条非跑不可的指令。
构建那头的大事,咱们开头提过一句,现在咱们把它定下形状:工程里也要住进第四户编译世界了。上一卷咱们数过,工程里住着的就是三户:host、16 位 boot、32 位 boot。本站添的是按 -m64 编的一户,住着过桥之后要跑的代码。而麻烦在于,这一户连链接都进不了别人的家。32 位世界的目标文件是 ELF32 的容器,而 64 位的件是 ELF64,链接器见了就直接拒收。所以这户人家得自己立一条链、自己定地址,链完了再剥成裸字节,被当成一包数据拼进 16 位的镜像。两个世界谁也看不见谁的符号,握手的凭据,就是 layout 里写下的一个数。搬运的细节咱们留到后面专讲,而眼下,咱们就从桥墩铺起。
到了收工的时候,咱们会把面板的尾巴从两行看到三行:
[stage2] leaving real mode
[pm] 32-bit world alive
[lm] 64-bit world alive眼下的面板尾巴还停在两行。会多出来的那一行是 [lm] 64-bit world alive,它得等咱们把表铺好、门闩拨开、门推开、跳过桥,四样都齐了它才肯亮。本站从头到尾忙的,就是为了把它亲手点亮。