Linux0.11源码趣读

用源码打开操作系统“黑盒子”

Linux,Linux 0.11,内核,源码,操作系统,

闪客 “低并发编程”公号作者

06|解决段寄存器的历史包袱问题

讲述:闪客 大小:7.38MB 时长:00:07:42
00:00
1.0×

你好,我是闪客。

这一讲我们要探索的是,在实模式和保护模式下,物理地址的计算方式有怎样的不同?这又和段寄存器的历史包袱有什么关系?

上一讲咱们说到,操作系统又折腾了一下内存,之后的很长一段时间内存布局就不会变了,终于稳定下来了,目前它长这个样子:

图片

0 地址开始处存放着操作系统的全部代码,也就是 system 模块,0x90000 位置处往后的几十个字节存放着一些设备的信息,方便以后使用。

图片

是不是十分清晰?不过别高兴得太早,清爽的内存布局,是为了方便后续操作系统的大显身手!

模式的转换

接下来就要进行真正的第一项大工程了,那就是模式的转换,需要从现在的 16 位的实模式转变为之后 32 位的保护模式

当然,虽说是一项非常难啃的大工程,但从代码量看,却是少得可怜,所以你不必太过担心。

每次讲这里都十分的麻烦,因为这是 x86 的历史包袱问题,现在的 CPU 几乎都是支持 32 位模式甚至 64 位模式了,很少有还仅仅停留在 16 位的实模式下的 CPU。

所以,我们要为了这个历史包袱,写一段模式转换的代码,如果 Intel CPU 被重新设计而不用考虑兼容性,那么今天的代码将会减少很多,甚至不复存在。

这里我不打算直接和你说实模式和保护模式的区别,我们还是跟着代码慢慢品味。

保护模式下的物理地址计算方式

接着上一讲,这里仍然是 setup.s 文件中的代码:

lidt  idt_48      ; load idt with 0,0
lgdt  gdt_48      ; load gdt with whatever appropriate

idt_48:
    .word   0     ; idt limit=0
    .word   0,0   ; idt base=0L

上来就是两行看不懂的指令?别急,跟住我的思路,慢慢就能看出门道了。

要理解这两条指令,就涉及到实模式和保护模式的第一个区别了。我们现在还处于实模式下,这个模式的 CPU 计算物理地址的方式还记得么?不记得的话看一下第1讲。就是段基址左移四位,再加上偏移地址。比如:

图片

是不是觉得很别扭,那更别扭的地方就要来了。当 CPU 切换到保护模式后,同样的代码,内存地址的计算方式还不一样,你说气不气人?

变成啥样了呢?刚刚那个 ds 寄存器里存储的值,在实模式下叫做段基址,在保护模式下叫段选择子。段选择子里存储着段描述符的索引。

图片

通过段描述符索引,可以从全局描述符表 gdt 中找到一个段描述符,段描述符里存储着段基址。

图片

段基址取出来,再和偏移地址相加,就得到了物理地址(准确说是线性地址,再经过分页转换后才是物理地址),整个过程如下:

图片

你就说烦不烦吧?同样一段代码,实模式下和保护模式下的结果还不同,但没办法,x86 的历史包袱我们不得不考虑,谁让我们没其他 CPU 可选呢。

总结一下就是,段寄存器(比如 ds、ss、cs)里存储的是段选择子,段选择子去全局描述符表中寻找段描述符,从中取出段基址。然后再加上偏移地址,就得到了最终的物理地址。

好了,那问题自然就出来了,全局描述符表(gdt)长什么样?它在哪?怎么让 CPU 知道它在哪?

全局描述符表 gdt

长什么样先别管,一定又是一个令人头疼的数据结构,先说说它在哪?肯定在内存中呗。

那么怎么告诉 CPU 全局描述符表(gdt)在内存中的什么位置呢?答案是由操作系统把这个位置信息存储在一个叫 gdtr 的寄存器中。

图片

具体怎么存呢?用的就是刚刚那条指令。

lgdt    gdt_48

其中 lgdt 就表示把后面的值(gdt_48)放在 gdtr 寄存器中,gdt_48 标签,我们看看它长什么样。

gdt_48:
    .word   0x800       ; gdt limit=2048, 256 GDT entries
    .word   512+gdt,0x9 ; gdt base = 0X9xxxx

可以看到这个标签位置处表示一个 48 位的数据,其中高 32 位存储着的正是全局描述符表 gdt 的内存地址 0x90200 + gdt。

gdt 是个标签,表示在本文件 setup.s 内的偏移量,不过准确说是 setup.s 被编译成 setup 二进制文件后,gdt 所在的内存偏移量。

之前分析过,setup.s 编译后是放在 0x90200 这个内存地址的,而 gdt 表示 setup.s 内的偏移量,所以要加上 0x90200 这个值,才能表示 gdt 这个标签在整个内存中的准确地址。

图片

那 gdt 这个标签处,就是全局描述符表在内存中的真正数据了。

gdt:
    .word   0,0,0,0     ; dummy

    .word   0x07FF      ; 8Mb - limit=2047 (2048*4096=8Mb)
    .word   0x0000      ; base address=0
    .word   0x9A00      ; code read/exec
    .word   0x00C0      ; granularity=4096, 386

    .word   0x07FF      ; 8Mb - limit=2047 (2048*4096=8Mb)
    .word   0x0000      ; base address=0
    .word   0x9200      ; data read/write
    .word   0x00C0      ; granularity=4096, 386

具体细节不用关心,跟我看重点。根据刚刚的段描述符格式:

图片

可以看出目前全局描述符表有三个段描述符,第一个为,第二个是代码段描述符(type=code),第三个是数据段描述符(type=data)

第二个和第三个段描述符的段基址都是 0,也就是之后在逻辑地址转换物理地址的时候,通过段选择子查找到无论是代码段还是数据段,取出的段基址都是 0,那么物理地址将直接等于程序员给出的逻辑地址(准确说是逻辑地址中的偏移地址),你先记住这点就好。

图片

具体段描述符的细节还有很多,比如这里的高 22 位就表示它是代码段还是数据段。更多内容和我们主线无关,就不展开了,感兴趣的同学可以阅读 Intel 手册 Volumn 3 Chapter 3.4.5 Segment Descriptor

图片

接下来我们看看目前的内存布局,还是别管比例。

图片

这里我把 idtr 寄存器也画出来了,这个是中断描述符表,其原理和全局描述符表一样。发生中断时,CPU会拿着中断号从中断描述符表里寻找中断处理程序的地址,找到以后,就会跳转到相应的中断程序去执行。

总结时刻

今天这一讲,我们讨论了实模式与保护模式下,逻辑地址到物理地址的计算方式有什么不同。

如果不考虑分页的话,在实模式下,逻辑地址到物理地址,仅仅需要将段寄存器中的值左移四位,加上偏移地址,即可得到物理地址。

而在保护模式下,段寄存器中的值仅仅表示了全局描述符表的索引,需要查表得到段基址,再加上偏移地址得到物理地址。

而这些变化仅仅是进入保护模式前准备工作的其中一个,后面的路还很长,别急,我们慢慢来看。

好,我是闪客。后面的世界越来越精彩,欲知后事如何,且听下回分解。