x86 的 CPU 工作模式与内存模型

Posted by HX on 2022-02-18 | 👓

从本科的操作系统课开始,我就一直无法把这门课所讲的段页式内存管理和 Windows 的内存管理对应上。这门课的教材把段页式内存管理作为分段内存和分页内存的一种折中方案来教学,并强调因为这种折中在分段和分页之间取长补短,所以成为现代操作系统主要的内存管理方式。然而,事实上现在的 32 /64 位 Windows/Linux 都弱化了分段机制,基本等同于只有分页机制,这是我无法将它们与段页式内存管理对应起来的原因之一。这篇文章总结一下最近学到的关于 x86 上 CPU 工作模式和内存模型的知识,这些知识大致解决了我之前的疑惑。

三种地址空间

在 x86 的体系结构下有三种内存地址空间:逻辑地址空间(又叫虚拟地址空间)、线性地址空间物理地址空间。这些空间中的地址转换过程如下:

1
逻辑地址(虚拟地址) ===段表===> 线性地址 ===页表===> 物理地址

也就是说 x86 确实是支持段页式内存管理的。为了后文说明方便,现在回顾一下分段机制的地址翻译过程(省去越界检查):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
逻辑地址
--------------
|段号|段内偏移|----------------
-------------- |
| |
| 段表 |
| -------------- ↓
| |段号0|段基址0| ⊕--------> 线性地址/物理地址
| -------------- ↑
| |.....|......| |
| -------------- |
--->|段号n|段基址n|----------
--------------
|.....|......|

分页机制的地址翻译完全类似,只是各个“段”字改成“页”字。

CPU 工作模式

x86 有两种 CPU 工作模式:实模式保护模式。实模式由老式机器和操作系统使用,保护模式由现在的所有新机器和操作系统使用,一个例外是刚开机时默认进入的是实模式,等操作系统做好准备才会切换至保护模式。

在实模式下,程序使用的所有逻辑地址被看作与真实的物理地址有某种直接对应关系,程序可由逻辑地址自行计算出物理地址。在保护模式下,逻辑地址需要经过段表的转换才能变为物理地址,这一过程必须经由操作系统完成。此外,保护模式还增加了段特权级的概念,低特权段的代码无法访问高特权段的数据和代码。

内存模型

这里只讲两种内存模型:平坦模型分段模型

所谓平坦模型,就是把整个内存地址空间看作一维的不划分的一个大数组,因此又叫线性模型。分段模型则是把地址看作二维的:段地址+段内偏移。

x86 的实现细节

8086 时代

在 8086 机的时代,保护模式尚未出现,因此所有机器都工作于实模式。8086 的逻辑地址分为两部分:段地址和段内偏移,一般用冒号隔开,一个逻辑地址就记作 段地址:段内偏移。也就是说,8086 的内存模型是分段模型。x86 有一组专门存放段地址的段寄存器。

8086(实模式)的逻辑地址到物理地址转换很简单,如下公式所示:

\[ physical\ address = (segment \ll 4) + offset. \]

其中,physical address 是物理地址,segment 是段寄存器中的段地址,左移四位成为段基址,再和段内偏移 offset 相加。

为什么要搞一个稍显复杂的方案,为什么要左移四位呢?因为当时 8086 是 16 位机,通用寄存器和段寄存器都只有 16 位,但它却有 20 根地址线,以便能寻址 \(2 ^{20} = 1MiB\) 的内存。那一个寄存器根本放不下 20 位的内存地址,就只能用两个寄存器分别放段地址和偏移咯,16 位的段地址左移四位不就成了 20 位的段基址嘛。

32/64 位时代

进入 32/64 位时代以后,大部分机器主要工作于保护模式。这种模式下,x86 要求操作系统维护一个段表,称为全局描述符表GDT。事实上段表只是 GDT 的一部分,里面还有其他信息,但在本文里我们暂且将二者视为同义词。

保护模式中的逻辑地址到物理地址转换不再适用实模式的公式,而必须查 GDT,查表的过程见上文“三种地址空间”一节。段寄存器中也不再直接存放段地址,而是存放段选择子值。段选择子值分为三部分:GDT 内的索引(段号)、表指示符、请求权级。段号很好理解,是几就对应 GDT 里第几项;表指示符表示应该查哪张表:本地描述符表LDT全局描述符表GDT还是中断描述符表IDT,实际使用中只有后两个在用,本文里只考虑 GDT;请求权级就是之前说的保护模式新增的概念。事实上 32/64 位机的段寄存器是 48 位的,段选择子值占高 16 位,剩下的 32 位记录关于段的其他信息(比如段基址,这部分信息从 GDT 表项中得到),对程序员一般不可见。总之,段寄存器和段选择子值的结构如下:

1
2
3
4
5
6
7
段寄存器
|段选择子值|段基址等其他信息|
占 16 位 | 占 32 位
段选择子值
| 段号 |表指示符|请求权级|
占 13 位 |占 1 位 |占 2 位

到这里,似乎 x86 都在用分段模型,平坦模型去哪了?一开始不是说 Windows/Linux 都弱化了分段吗?答案是 Windows/Linux 用了点小技巧,把分段模型转换成了平坦模型。这个技巧就是:把段基址全部设为 0,而段长覆盖整个 32 位(4GiB)线性地址空间(之所以是线性地址空间,是因为分段完之后还有分页,分段的地址转换只是把逻辑地址转成了线性地址,还要进一步查页表才转成物理地址)。也就是说,所有段都重叠在一起,而范围是整个线性地址空间,这样就等效于没有分段,整个内存是一个整体了。

各 CPU/操作系统的比较

CPU/操作系统 CPU 工作模式 内存模型
8086 实模式 分段模型
Windows 3.1 保护模式 分段模型
> Windows 3.1/现在的 Linux 保护模式 平坦模型