跨过门槛之后
远跳落地,PmEntry 接住了执行流。咱们把这个函数安在了 pm.cpp 里,它是 32 位世界的头一份产业。来看它的开场:
extern "C" [[noreturn]] void PmEntry() {
asm volatile(
"movw $0x10, %%ax\n"
"movw %%ax, %%ds\n"
"movw %%ax, %%es\n"
"movw %%ax, %%fs\n"
"movw %%ax, %%gs\n"
"movw %%ax, %%ss\n"
"movl %[stack], %%esp\n"
:
: [stack] "n"(cinux::boot::kPmStackTop)
: "ax");为什么一上来就刷五个段寄存器?因为远跳只照顾了 CS 一个。DS、ES、FS、GS、SS 剩下的五个,身上还穿着实模式的旧衣服:里面装的还是段基址一路用下来的脏值,放在新世界的查表章法底下,全是些来历不明的东西。咱们挨个把 0x10 装了进去——它就是数据段选择子,指着表里住在 0x10 的那一项,base、limit 分别是零和满开的 4GB。从此它们就跟 CS 站到了一起,老老实实地指着扁平模型了。这步要是漏了哪个,尤其是漏了 SS,接下来的任何一次栈操作都在拿旧值乱撞,炸得毫无征兆的。
然后咱们看栈。ESP 换到的是 kPmStackTop,取的值就是 0x90000。栈为什么非换不可?咱们在实模式里用的栈,是 0x0000:0x7000 那一套段左移拼偏移的记法,而这套记法在新世界里不存在了,栈地址就是 ESP 自己一个 32 位数。旧栈要是原样带过来会发生什么,拿当年第一遍的实模式栈一算就明白:0x0900:0xFFFE 这样的段加偏移,到了新模型底下会被读成 ESP=0xFFFE,一个贴着 64KB 的低地址。它往下的邻居们离得倒是不近:最近的 stage2 镜像尾巴也在二十几 KB 外,页表、VESA 的缓冲区、BIOS 的数据区还在更深处,可它们全排在栈往下长的路上,哪天栈真长起来了总有一家头一个挨踩。所以新世界要有新栈,而且地址得挑个安稳的。
0x90000 是按三条依据挑出来的,咱们在 layout 里给每一条都配了断言:它得在内核加载区 0x20000 之上,内核将来从那儿往上长的,咱们把栈划在高地,两不相扰的。它得待在 1MB 的下面,没有页表的日子里,那边是咱们直接够得着的地界。它还得落在 4 字节的对齐上,这是 32 位世界里 call 和 push 的最低要求。当年的 0x90000 本来就满足这些条件,这一遍三条都写成了断言,哪天谁手滑了一下,头一个叫的就是构建。
段也净了,栈也稳了,咱们说第一句话:
cinux::boot::serial::PutString("[pm] 32-bit world alive\n");
for (;;) {
asm volatile("hlt");
}这一行值得您多看一眼。咱们在前面讲两个编译世界的时候说过,PutString 住在头文件的 inline 里,pm.cpp 这边 include 了一句,32 位的版本就白拿到了。它走的路子还是 debugcon,往的还是 0xE9 号端口。这里有一件安静但要紧的事:端口 IO 跟 CPU 处在什么模式,是一点关系都没有的。它就是一条独立的输出暗线,咱们在实模式里用得着它,进了保护模式也照样用得着。所以咱们跨过门槛,输出通道是一天都没断过的。
对照着当年的狼狈,这一行的分量就出来了。第一遍的时候,过了远跳,咱们手里就没有任何能打印的东西了:BIOS 的 INT 10h 没了,屏幕成了没字体的图形模式,串口驱动更是没影儿的事,标准的输出真空。当年的办法是往 0xE9 硬塞一个字节 P,看它落进了日志文件,就那么一个字母,还是裸的机器码 out——能证明 pm_entry 到了,别的什么都说不了。到了这一遍,输出通道早在前面几站就搬进了 debugcon,过了门照样整行整行地打。端口还是那个 0xE9:当年靠它挤出的是那么一个字母,如今咱们整行整行的字,走的也还是它。
末尾的循环也有讲究。hlt 是让 CPU 睡到下一个中断来敲门的,可咱们 EnterProtectedMode 里那句 cli 还在生效,门根本就不会响——所以这一睡就是长眠,QEMU 体面地停在那儿,面板上的最后一行字亮着。“没有中断表,就不开中断”,这句纪律这一路已经出现过两回了:一回是咱们立栈的时候 cli 打头,一回是本站开头咱们把中断门彻底关死。到了这儿,它的两半终于合了体:关的那一半在 16 位世界,睡的那一半在 32 位世界,中间隔着的也就是一句远跳。