实模式与保护模式下的内存访问

Intel x86 架构下的内存访问过程的寄存器级阐述,涉及内存管理之分段机制

Posted by Surflyan on 2018-01-16

本文所有配图皆来源于 Intel® 64 and IA-32 Architectures Software Developer’s Manual

本文主要从 x86 硬件层面内存访问过程进行一定讲解,若对操作系统的段页式分配不甚了解,可查看博主另一篇文章 内存管理之段页式机制


1. 实模式下的内存访问

Intell 早期的 8086 CPU 提供了20根地址线,可寻址空间范围为0~2^20,即 1MB 空间,但那时的段寄存器、指令指针寄存器和通用寄存器都是16位的,无法直接寻址1MB空间,所以8086提供了逻辑段地址加偏移地址的转换机制,就是常见的将段寄存器内容左移四位加上偏移地址,以形成构成20位物理地址。
在实模式下,用户程序对内存的访问非常自由,几乎没有任何限制,随随便便就可以修改任何一个内存单元,这是一个非常恐怖的事情,后果也是灾难性的。


2. 保护模式下的内存访问

1985年,intell发布了第一款32位处理器80386,并取得了巨大成功。80386处理器寄存器是32位的,拥有32根地址线,可以访问2^32,即4GB内存空间。虽然保护模式在80286就提出了,但这里还是以最常见的32位处理器做出说明。

处理器引入保护模式的目的是提供保护功能。要理解32位架构处理器保护模式下的内存访问,必须先从它的分段机制入手。在保护模式下,传统的段寄存器保存的不再是16位段基地址,而是段选择子,也就是要访存的段的标识符。

2.1 分段机制

2.1.1 平坦模型

最简单的内存管理模型就是平坦模型,将处理器可访问的4G线性空间看作一个段,段的基地址是0x00000000,段的长度是4GB,这就是平坦模型。这在多任务情况下,显然不利于硬件保护一个程序的代码、数据、堆栈。

平坦模型

因此,分段模型就诞生了。

2.1.2 分段模型

分段机制把处理器可寻址的线性地址空间划分成一些受保护的段空间,提供了隔绝各个代码的数据、代码和堆栈区域的机制,因此多个程序可以运行在同一个处理器上而不相互干扰。每个任务都分配自己的段描述符表和段。一般来说,一个任务有如下几个段:数据段、代码段、堆栈段以及用来存放系统数据结构 (如TSS或者LDT) 的段。对程序来说段可以是完全私有的,也可以是多程序之间共享的。对所有程序的各自执行环境的访问都是由硬件来控制的。

多段模型

2.1.3 段描述符

已经将线性地址空间为每个任务分配了自己的段,那么如何描述一个段呢,这就有了段描述符。段描述符,用于向处理器提供有关一个段的位置、大小信息以及访问控制的状态信息。每个段描述符是8字节,含有三个主要字段:段基地址、段限长和都段属性。段描述符的一般格式:

段描述符

值得注意的是描述符类型标志S用于指明一个段描述符是一个系统描述符(S = 0) 还是代码段或者数据段描述符(S=1);
段类型字段(TYPE)指定段或者门的类型、说明段的访问种类以及段的扩展方向。

系统描述符包含以下几种描述符:

  • 局部描述符表(LDT)的段描述符
  • 任务状态段(TSS)描述符
  • 调用门描述符
  • 中断门描述符
  • 陷阱门描述符
  • 任务门描述符

2.1.4 段描述符表

每个段都需要一个段描述符,需要在内存中开辟一块空间来存放这些段描述符。这就构成了一个描述符表。

主要有两种描述符表:
a. 全局描述符表GDT:该表是为整个软硬件系统服务的
b. 局部描述符表LDT: 每个任务对应一个局部描述符表或多个任务共享一个LDT。

理论上GDT可以放置在内存中的任意地方,但是必须在进入保护模式之前就定义好GDT,由于在实模式下只能访问1MB内存空间,所以通常GDT定义在1MB一下的内存空间中。但是也允许在进入保护模式之后换个位置重新定义GDT。在保护模式下,在访问某个段之前,必须要在GDT内定义要访问的内存段,这就增加了一重保护机制。由于描述符是由操作系统根据具体的程序结构建立的,而不是用户程序自己建立的,所以在这种情况下,操作系统为程序建立了几个表、表的位置、大小都固定了,程序就只能老老实实在自己的段内工作。超出范围,都会被处理器阻止。

处理器不使用GDT的第一个描述符,该描述符必须设为空。

为了跟踪全局描述符表,处理器提供了一个48位的全局描述符寄存器GDTR。GDTR的32位线性基地址部分保存的是全局描述符在内存中的起始线性地址,在保护模式下,如果没有开启分页功能,那么该线性地址就是内存的物理地址。16位边界部分保存的是全局描述符表的边界,数值上等于表的大小。

内存管理寄存器

若定义了LDT,那么GDT中必须包含LDT的段描述符。如果支持多LDT的话,那么每个LDT都必须在GDT中由一个段描述符和段选择符。为了在访问LDT时减少地址转换次数,提高处理效率,处理器提供了IDTR来存放LDT的段选择符、线性基地址、段限长以及段的属性。

2.1.5 32位段寄存器

在32位处理器内,一共有6个段寄存器。段寄存器又分为两个部分,可见部分和不可见部分。在实模式下,可见部分的16位按照传统的内存寻址方法寻址1MB内存。在保护模式下,传统的段寄存器保存的不再是16位逻辑段地址,而是段选择子,也就是所要访存的段,而不可见部分,称为描述符高速缓存器,包含了段的线性基地址、段界限和各种访问属性。这部份内容程序不可见,由处理器使用。

32位处理器的段寄存器

2.1.6 段选择子

在保护模式下,传给段寄存器的不再是逻辑段地址,而是段选择子。段选择子是段的一个16位标识符,如图所示。段选择子指向段描述符表中的段描述符。含有三个字段:
a. 描述符的索引号
b. TI描述符表指示器
c. RPL是请求特权级

段选择子结构

请求特权级字段提供了该任务的特权级。TI字段表明了包含该段描述符的段描述符表是GDT(TI=0) 还是LDT(TI = 1)。然后根据描述符索引去GDT或者LDT找到该描述符。如到GDT寻找第2个描述符,那么选择子为 0x08,先到GDTR找到GDT 的 线性基地址base, 然后描述符起始地址为 base + 1 * 8;一个描述符8个字节。

2.2 访存过程

处理器在执行任何改变段寄存器的指令时,就将指令中提供的索引号乘以8作为偏移地址,同GDTR中的线性基地址相加,以访问GDT。如果,各种保护机制没有出现问题(如访问GDT越界),就自动将找到的描述符加载到不可见的描述符高速缓存部分(包含,段的线性基地址、段界限、段的访问属性),这样,以后若还有访问内存的该段的指令时,就不需要再去GDT寻找描述符,直接使用对应段寄存器里高速缓存器提供的线性基地址就可以了。提高了访存效率。获取到 段的线性基地址 + 指令提供的段内偏移地址,这样就可达到访问段内任意地址空间的目的。

逻辑地址到线性地址的转换

如果没有启用分页机制,那么分段机制产生的线性地址空间就直接映射到物理地址空间上了。

事实上,现在的主流操作系统都会采用段页式内存分配方案,故在由逻辑地址转换为线性地址之后,还需将线性地址根据分页机制转换为物理地址,过程如下图:

段页式机制地址转换

Reference

  1. Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C)
  2. 赵炯,Linux 内核完全注释
  3. 李忠,王晓波,x86 汇编语言从实模式到保护模式,2013

请多多指教!