在跟随《操作系统真象还原》进行实际操作时,遭遇了一个非常奇怪的问题。

问题现象是以下两段代码会导致不一样的结果

第一段代码,在后面实现了用户进程地址空间后,程序会跑飞

image-20230913234115567

相关代码如下:

1
2
3
4
5
void idt_init() {
    ...
    uint64_t idt_operand = ((sizeof(idt)-1) | ((uint64_t)((uint32_t)idt<<16))); // 这里是先对idt做了左移16位,此时可能会溢出阶段,因此此时还是32位的,然后再转换为64位的操作数
    asm volatile("lidt %0" : : "m" (idt_operand));
}

以及

1
2
3
4
5
void idt_init() {
    ...
    uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
    asm volatile("lidt %0" : : "m" (idt_operand));
}

这两端代码都是为了加载中断描述符表,但是只有第二种写法能正常运行,第一种不行。

两段代码实际运行时得到的idt_operand值是相同的。直观上从代码上已经看不出端倪了。

image-20230913232010194

如图所示,第一个代码中计算得到的idt_operand值为0x8320017f ,高4字节是0,按照IDTR寄存器指令的要求: lidt 48位内存地址, 低16位是IDT界限(此处为0x017f=0x30*8-1),高32位为IDT的线性地址(此处为0x00008320),可以看见这个线性地址是在1MB空间内的,在内核执行过程中没有任何问题(内核寻址3GB以上1MB空间与1MB线性空间映射到相同的低1MB物理地址空间)。可是当加载用户空间代码时,会进行切页操作,切换页表后,新的线性地址空间的低1MB空间不在映射到3GB空间以上了,因此在执行用户空间代码时,一旦有中断发生,CPU会发现原先的LIDT加载的地址不再可用了,因此服务异常。

这里再说一下为什么第二种代码可以,第二种代码里面idt_operand的值实际为0xc0008320017f,可以看见其值在3GB空间以上,这里也可以在gdb中打印看出。

image-20230913233458555

这里的疏忽在于我当前实现的简易操作系统,其支持打印函数是32位,因此不容易发现这个问题

image-20230913233657278