IA-32 内存模型与地址映射

8086 的内部寄存器为 16 bits,同时有 20 根地址线,是第一款支持内存分段模型的处理器,它只工作在实模式下。IA-32(Intel Architecture, 32-bit) 由 1987 年的 8086 处理器发展而来,它的寄存器为 32 bits,有 32 根地址线,可以访问 2^32(4G)的内存,每次读写单位为 4 bytes。IA-32 支持平坦内存模型和分段内存模型。

0x01 内存模型

1. 平坦内存模型

IA-32 架构的处理器是基于分段模型的,因此需要以段为单位访问内存。平坦内存模型(Flat Model)下内存只分为一个段(相当于不分段),段基址为 0x00000000,段长度为 2^32 bytes。此时,使用的地址为线性地址,对应的地址空间为线性地址空间(0~2^32-1)。代码、数据和堆栈都分布在同一个地址空间。

2. 实地址内存模型

实模式用于早期的 8086(16 位)处理器,由于其内部寄存器为 16 bits,但是有 20 根地址线。IA-32 向后兼容 8086,也支持实模式。该模式下逻辑地址为以下形式:

1
16 bits 段寄存器:16 bits 段内偏移

其中,段寄存器的值为段基址的高 16 bits。为了解决由 16 bits 的段地址和偏移地址产生 20 bytes 的线性地址的问题,使用以下方法将逻辑地址转换为线性地址:

1
线性地址(20 bits) = 段选择器 << 4 + 段内偏移(16 bits)

由此可知,实模式具有以下特点:

  • 段基址一定是 16 的整数倍;
  • 其线性地址范围为 0~2^20-1(1M),且线性地址空间由一系列 64 KB 的段组成;
  • 该模式下使用的地址为实际物理地址;
  • 进程可以访问所有的内存数据,不存在用户态、内核态之分;

3. 分段内存模型

分段内存模型将内存空间分为独立的段,包括代码段、数据段和堆栈段。程序中使用由段选择子和偏移构成的逻辑地址访问段中内存,段选择子用于确定要访问的段,偏移地址用于定位目标段中的内存单元。IA-32 处理器最多有 16383(2^14)个段,并且每个段最大为 2^32 bytes。所有的段最终映射到处理器的线性地址空间中,访问段中内存时,处理器将逻辑地址转换为线性地址进行访问。

实地址模型下,一个进程可自由地读写其他进程的内存;分段内存模型下,处理器使用段描述符指定段基址、段界限、特权级别、类型等属性,程序访问内存时会进行检查,以防止对内存的违规访问。

0x2 保护模式的内存管理

1982 年,Intel 公司推出的 80286 处理器(16 bits),有 24 根地址线。80286 提出了保护模式的概念,保护模式下段寄存器的值为段选择子,根据段选择子可确定 24 bits 的段基址,因此可访问 16 MB 的内存。

1985 年的 80386 是 Intel 的第一款 32 bits 处理器,其寄存器为 32 bits,且有 32 根地址线,可访问 2^32(4G)的内存。80386 及后续的 32 bits 处理器都兼容实模式,在处理器刚加电时处于实模式下,进行一系列初始化后运行在保护模式下。

保护模式具有以下优点:

  • 横向保护,又称任务间保护。多任务操作系统中,一个任务不能破坏另一个任务的代码,这是通过内存分页以及不同任务的内存页映射到不同物理内存上来实现的。
  • 纵向保护,又称任务内保护。系统代码与应用程序代码虽处于同一地址空间,但系统代码具有高权限级别,应用程序代码处于低权限级别,规定只能高权限级别代码访问低权限级别代码,这样可杜绝用户代码破坏系统代码。

1. 描述符与内存管理寄存器

1)描述符表

保护模式下,内存访问需要通过全局描述符表(GDT)或可选的本地描述符表(LDT)。这些描述符表中每一个描述符对应一个段,段描述符中存放着段基址、访问权限和类型等信息;程序以段选择子作为索引在 GDT/LDT 中找到对应的段描述符;从段描述符中可获得线性地址空间中的段基址,段基址加上偏移地址即可访问相应内存。

段选择子
段寄存器中可见部分为段选择子(16 bits),段选择子指向段描述符,其结构如下图所示:

  • Index:段描述符表索引,用于访问具体的段描述符;
  • TI:描述符表的类型,0 表示 GDT,1 表示 LDT;
  • RPL:指定请求特权级(0~3)。

段描述符
段描述符是 GDT/LDT 中的一个数据结构,用于指定段的大小、地址、访问权限和状态信息,段描述符由编译器、链接器、加载器或操作系统生成。段描述符的结构如下图所示:

  • Base Address(Base 15:00+Base 23:16+Base 31:24):32 bits 的段基址,定义段的线性地址;
  • Segment Limit(Segment Limit 15:00+ Segment Limit 19:16):20 bits 的段限,指定段大小;

2)内存管理寄存器


处理器提供了 4 个内存管理寄存器:GDTR、LDTR、IDTR 和 TR。

  • GDTR
    GDTR(Global Descriptor Tale Register) 用于存放 GDT 的基址(GDT 的起始地址)和 16 bits 的表限(表的大小)。处理器初始化时需在 GDTR 中设置新的基址。
  • LDTR
    LDTR(Local Descriptor Tale Register) 用于存放 16 bits 的段选择码、基址(LDT 的起始地址)、段限(段大小)和属性。
  • IDTR
    IDTR(Interrupt Descriptor Talbe Register) 用于存放 IDT 的基址和表限。
  • TR
    TR(Task Register) 用于存放 16 bits 的段选择码、基址、段限和属性。Linux 中未使用该寄存器。

2. 内存管理

IA-32 中,保护模式下的内存管理分为分段和分页,分段是强制的,分页是可选的,分页机制建立在分段的基础上。分段机制将代码、数据和堆栈分开,当处理器上运行多个程序时,每个程序拥有一系列自己的段,使得不同程序间不会互相影响;分页机制将物理内存以页为单位进行分割,并按需调度,可提高内存的使用效率。在内存管理过程中涉及以下几个地址概念:

  • 逻辑地址(Logical Address):汇编语言(程序员)使用的地址由段选择子(Segment selector)和偏移量(Offset)组成。
  • 线性地址(Linear Address):是逻辑地址到物理地址变换之间的中间层,32 bits 系统中为 32 bits 的无符号整数。
  • 物理地址(Physical Address):CPU 用于寻址的实际物理内存地址,IA-32 的物理地址空间为 4 G(2^32 bytes)。

在未使用分页机制时,段部件将段基址加上段内偏移得到的线性地址即为物理地址;而使用分页机制之后,段部件产生的线性地址不再是物理地址,此时的线性地址也称为虚拟地址,线性地址经过页部件可转换为物理地址。各地址之间的转换关系如下图所示:

保护模式下,由逻辑地址转换为物理地址的详细过程如下图所示,地址转换需要经过逻辑地址转换和线性地址空间映射。

3. 逻辑地址到线性地址

逻辑地址转换为线性地址的过程如下图所示:

地址转换的具体步骤如下:

  • a. 根据指令性质确定段寄存器,如转移指令的地址在代码段,而取数据指令的地址在数据段;
  • b. 根据段寄存器(段选择子)在段描述符表中(GDT 或 LDT/TR/IDT)中找到相应的段描述符,并将其读进处理器;
  • c. 从段描述符中找到段基址;
  • d. 将指令中的地址作为偏移,与段描述符中的段长度相比,检查偏移是否越界;
  • e. 根据指令的性质和段描述符中的访问权限判断是否越权;
  • f. 将找到的段基址与偏移相加得到线性地址。

4. 线性地址到物理地址

开启分页机制时,得到线性地址后还需将其转换为物理地址,转换过程如下图所示:

线性地址转换为物理地址的具体步骤如下:

  • 从 CR3 寄存器中获取页目录(Page Directory)的基址;
  • 以线性地址的 Directory 段为索引,在页目录中找到相应的页目录项(Page Directory Entry),在页目录项中可得到相应页表(Page Table)的基址;
  • 在所得到的页表中,使用线性地址中 Table 段为索引找到页表项(Page Table Entry);
  • 将页表项中给出的页面基址与线性地址中的 Offset 段相加可得到物理地址。

每个进程中 CR3 寄存器的值时独立的,它存放在进程控制块中,如 Linux 中的 task_struct 数据结构中。

页目录项结构如下:

页表项结构如下:

线性地址转换为物理地址的过程由内核完成,用户态进程无法访问页表,但是在 Linux 下可以使用Linux 动态链接中提到的方法在用户态将线性地址转换为物理地址。


References:
[1] 《x86 汇编语言-从实模式到保护模式》
[2] Linux_Memory_Address_Mapping
[3] 《Intel® 64 and IA-32 Architectures Software Developer’s Manual》
[4] 内存寻址