这是本系列第三篇了,我们这次来谈谈x86的段页式内存管理。这篇文章的定位是阐述分段分页的来历和要解决的问题。需要阐述细节的地方,我会贴出相关的文档和代码。

首先,如果我这个标题让你觉得段页式是一种方式而且密不可分的话,那我先说声抱歉了。其实分段和分页没什么必然联系。只不过Intel从8086开始,其制造的CPU就以段地址+偏移地址的方式来访问内存。后来要兼容以前的CPU,Intel不得不一直保留着这个传统。分段可以说是Intel的CPU一直保持着的一种机制,而分页只是保护模式下的一种内存管理策略。不过想开启分页机制,CPU就必须工作在保护模式,而工作在保护模式时候可以不开启分页。

关于保护模式的段机制我们在系列一里面已经谈过不少,而且我们也谈过“绕过”分段的平坦模式。那么,我们下文的重点就是谈谈在设置平坦模式的环境之后,进行内存分页管理的问题了。光说不练是假把式,这次我们就贴上来一些代码具体感受一下吧。

首先是设置全局段描述符表。我们给出全局段描述符表和全局描述符表寄存器的结构体定义:

注意结构体定义后面的那个 __attribute__((packed)) 很重要,这是GCC的扩展,用来设置该结构体不进行字节对齐。什么?你不知道什么是字节对齐?那么你先去谷歌一下再回来接着看吧。

好了,为了方便和Intel的文档比对,我们贴出相关的定义参照着看吧。

我们再贴出GDTR的定义:

这样对比着结构体的定义很清楚吧?需要注意的是我们把段描述符表的部分以二进制位来表示的设置信息合并到了相应的字节里,这里按照位域去定义不是不可以,但是太过于臃肿了,而且等我贴出设置一个段描述符的函数时,你就觉得其实这样做更清晰。

我们给出全局描述符表的定义以及设置一项描述符的函数实现:

怎么样?几张图片对比着看很容易就理解了吧?那么具体的初始化函数呢?别急,接下来就是:

这里唯一麻烦的就是需要对照着Intel文档的说明,去为每一个段描述符计算权限位的数值了。不过细心的你肯定发现了最后有一个加载全局描述附表的函数,这个函数用汇编来实现了。代码如下:

因为对具体寄存器的操作超过了C语言的能力范围,与其内联汇编还不如直接用用汇编实现简单(我们采用的汇编编译器是nasm)。

我想这个汇编函数中唯一需要解释的就是jmp跳转那一句了,首先0x08是我们跳转目标段的段选择子(这个不陌生吧?),其对应段描述符第2项。后面的跳转目标标号可能会让你诧异,因为它就是下一行代码。这是为何?当然有深意了,第一,Intel不允许直接修改段寄存器cs的值,我们只好这样通过这种方式更新cs段寄存器;第二,x86以后CPU所增加的指令流水线和高速缓存可能会在新的全局描述符表加载之后依旧保持之前的缓存,那么修改GDTR之后最安全的做法就是立即清空流水线和更新高速缓存。说的这么牛逼的样子,其实只要一句jmp跳转就能强迫CPU自动更新了,很简单吧?

到这里段描述符表的创建就告一段落了,其实我们完全可以直接计算出这些段具体的表示数值然后硬编码进去,但是出于学习的目的,我们还是写了这些函数进行处理。当然了,我们没有谈及一些具体的描述符细节问题,因为Intel文档的描述都很详细。

接下来我们来聊分页吧。我们先给出CPU在保护模式下分页未开启和分页开启的不同状态时,MMU组件处理地址的流程。

如果没有开启分页:

逻辑地址->段机制处理->线性地址=物理地址

如果开启分页:

逻辑地址->段机制处理->线性地址->页机制处理->物理地址

因为我们采用了平坦模式,所以给出的访问地址实际上已经是线性地址了(段基址为0),那么剩下的问题就是所谓的页机制处理了。

时间关系,页机制的细节我们下次再说。如果你已经迫不及待想知道了,那就先去谷歌看看吧。我们下期再见~

赏杯咖啡鼓励下~