跳到正文
汉松札记
返回

Linux 内存寻址之分段机制

技术笔记

前言

最近在学习 Linux 内核,读到《深入理解 Linux 内核》的内存寻址一章。原本以为自己对分段分页机制已经理解了,结果发现其实是一知半解。于是,查找了很多资料,最终理顺了内存寻址的知识。现在把我的理解记录下来,希望对内核学习者有一定帮助,也希望大家指出错误之处。

分段到底是怎么回事

相信学过操作系统课程的人都知道分段分页,但是奇怪的是书上基本没提分段分页是怎么产生的,这就导致我们知其然不知其所以然。下面我们先扒一下分段机制产生的历史。

实模式的诞生(16 位处理器及寻址)

在 8086 处理器诞生之前,内存寻址方式就是直接访问物理地址。8086 处理器为了寻址 1M 的内存空间,把地址总线扩展到了 20 位。但是,一个尴尬的问题出现了,ALU 的宽度只有 16 位,也就是说,ALU 不能计算 20 位的地址。为了解决这个问题,分段机制被引入,登上了历史舞台。

为了支持分段,8086 处理器设置了四个段寄存器:CS, DS, SS, ES.每个段寄存器都是 16 位的,同时访问内存的指令中的地址也是 16 位的。但是,在送入地址总线之前,CPU 先把它与某个段寄存器内的值相加。这里要注意:段寄存器的值对应于 20 位地址总线的中的高 16 位,所以相加时实际上是 16 位内存地址(即段内偏移值)的高 12 位与段寄存器中的 16 位相加,而低 4 位保留不变,这样就形成一个 20 位的实际地址,也就实现了从 16 位内存地址到 20 位实际地址的转换,或者叫“映射”。

上面关于分段机制计算内存地址的描述比较难理解,画了一个图帮助理解

+-----------------+
|       20        |  20 位地址总线
+-----------------+
+------------+
|      16    |       16 位段地址
+------------+
    +-------------+
    |    12  | 4  |  16 位内存地址(段内偏移量)
    +--------+----+

实际物理地址 = (段寄存器地址 << 4) + (CPU 提交的访存地址)

保护模式的诞生(32 位处理器及寻址)

IA32 的内存寻址机制

寻址硬件

在 8086 的实模式下,把某一段寄存器左移 4 位,然后与地址 ADDR 相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的这个地址就叫逻辑地址(或叫虚地址)。在 IA32 的保护模式下,这个逻辑地址不是被直接送到内存总线而是被送到内存管理单元(MMU)。MMU 由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,即进行地址转换,如图所示。 MMU

IA32 的三种地址

MMU 地址转化过程

MMU 是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,在此,我们把它们分别叫做分段机制和分页机制,以利于从逻辑的角度来理解硬件的实现机制。分段机制把一个逻辑地址转换为线性地址;接着,分页机制把一个线性地址转换为物理地址。 MMU_translate

IA32 的段寄存器

IA32 中有六个 16 位段寄存器:CS, DS, SS, ES,FS, GS.跟 8086 的段寄存器不同的是,这些寄存器存放的不再是某个段的基地址,而是某个段的选择符(Selector)。

分段机制的实现

段是虚拟地址空间的基本单位,分段机制必须把虚拟地址空间的一个地址转换为线性地址空间的一个线性地址。

为了实现这种映射,仅仅用段寄存器来确定一个基地址是不够的,至少还得描述段的长度,并且还需要段的一些其他信息,比如访问权之类。所以,这里需要的是一个数据结构,这个结构包括三个方面的内容:

  1. 段的基地址 (Base Address):在线性地址空间中段的起始地址。
  2. 段的界限 (Limit):在虚拟地址空间中,段内可以使用的最大偏移量。
  3. 段的保护属性 (Attribute):表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等等。

上面的数据结构我们称为段描述符,多个段描述符组成的表称为段描述符表

段描述符

所谓描述符 (Descriptor),就是描述段的属性的一个 8 字节存储单元。在实模式下,段的属性不外乎是代码段、堆栈段、数据段、段的起始地址、段的长度等等,而在保护模式下则复杂一些。IA32 将它们结合在一起用一个 8 字节的数表示,称为描述符。 IA32 的一个通用的段描述符的结构 从图可以看出,一个段描述符指出了段的 32 位基地址和 20 位段界限 (即段长)。这里我们只关注基地址和段界限,其他的属性略过。

段描述符表

各种各样的用户描述符和系统描述符,都放在对应的全局描述符表、局部描述符表和中断描述符表中。描述符表 (即段表) 定义了 IA32 系统的所有段的情况。所有的描述符表本身都占据一个字节为 8 的倍数的存储器空间,空间大小在 8 个字节 (至少含一个描述符) 到 64K 字节 (至多含 8K) 个描述符之间。

  1. 全局描述符表 (GDT) 全局描述符表 GDT(Global Descriptor Table),除了任务门,中断门和陷阱门描述符外,包含着系统中所有任务都共用的那些段的描述符。它的第一个 8 字节位置没有使用。
  2. 中断描述符表 IDT(Interrupt Descriptor Table) 中断描述符表 IDT(Interrupt Descriptor Table),包含 256 个门描述符。IDT 中只能包含任务门、中断门和陷阱门描述符,虽然 IDT 表最长也可以为 64K 字节,但只能存取 2K 字节以内的描述符,即 256 个描述符,这个数字是为了和 8086 保持兼容。
  3. 局部描述符表 (LDT) 局部描述符表 LDT(local Descriptor Table),包含了与一个给定任务有关的描述符,每个任务各自有一个的 LDT。有了 LDT,就可以使给定任务的代码、数据与别的任务相隔离。每一个任务的局部描述符表 LDT 本身也用一个描述符来表示,称为 LDT 描述符,它包含了有关局部描述符表的信息,被放在全局描述符表 GDT 中。

总结

IA32 的内存寻址机制完成从逻辑地址—线性地址—物理地址的转换。其中,逻辑地址的段寄存器中的值提供段描述符,然后从段描述符中得到段基址和段界限,然后加上逻辑地址的偏移量,就得到了线性地址,线性地址通过分页机制得到物理地址。 首先,我们要明确,分段机制是 IA32 提供的寻址方式,这是硬件层面的。就是说,不管你是 windows 还是 linux,只要使用 IA32 的 CPU 访问内存,都要经过 MMU 的转换流程才能得到物理地址,也就是说必须经过逻辑地址—线性地址—物理地址的转换。

Linux 中分段的实现

前面说了那么多关于分段机制的实现,其实,对于 Linux 来说,并没有什么卵用。因为,Linux 基本不使用分段的机制,或者说,Linux 中的分段机制只是为了兼容 IA32 的硬件而设计的。

Intel 微处理器的段机制是从 8086 开始提出的,那时引入的段机制解决了从 CPU 内部 16 位地址到 20 位实地址的转换。为了保持这种兼容性,386 仍然使用段机制,但比以前复杂得多。因此,Linux 内核的设计并没有全部采用 Intel 所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了 Linux 内核的设计,而且为把 Linux 移植到其他平台创造了条件,因为很多 RISC 处理器并不支持段机制。但是,对段机制相关知识的了解是进入 Linux 内核的必经之路。

从 2.2 版开始,Linux 让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表 LDT。但内核中也用到 LDT,那只是在 VM86 模式中运行 Wine,因为就是说在 Linux 上模拟运行 Winodws 软件或 DOS 软件的程序时才使用。

在 IA32 上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在 IA32 上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让 Linux 具有更好的可移植性,我们需要去掉段机制而只使用分页机制。但不幸的是,IA32 规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。万般无奈之下,Linux 的设计人员干脆让段的基地址为 0,而段的界限为 4GB,这时任意给出一个偏移量,则等式为“0+ 偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量<4GB”,所以偏移量的范围为 0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux 在没有回避段机制的情况下巧妙地把段机制给绕过去了。

另外,由于 IA32 段机制还规定,必须为代码段和数据段创建不同的段,所以 Linux 必须为代码段和数据段分别创建一个基地址为 0,段界限为 4GB 的段描述符。不仅如此,由于 Linux 内核运行在特权级 0,而用户程序运行在特权级别 3,根据 IA32 段保护机制规定,特权级 3 的程序是无法访问特权级为 0 的段的,所以 Linux 必须为内核用户程序分别创建其代码段和数据段。这就意味着 Linux 必须创建 4 个段描述符——特权级 0 的代码段和数据段,特权级 3 的代码段和数据段。

总结

分段机制是 IA32 架构 CPU 的特色,并不是操作系统寻址方式的必然选择。Linux 为了跨平台,巧妙的绕开段机制,主要使用分页机制来寻址。

参考资料 《深入分析 Linux 内核源码》


订阅 技术笔记

分享这篇文章:


上一篇
Linux 内存寻址之分页机制
下一篇
ucore 实验之操作系统启动流程