x86 内存组织是在寄存器宽度、地址总线宽度、保护机制和多任务处理方面的限制逐步变化的过程中发展起来的。物理内存仍然是按字节寻址的空间:完成转换后,处理器会在内存接口上输出物理地址。不过,抵达该地址的路径已经发生了显著变化。
x86 指令首先计算一个有效地址(EA),也称为偏移量。在支持可用分段的模式中,选定的段提供基址,从而生成线性地址。启用分页后,页表将线性地址转换为物理地址。因此,其阶段如下:
实地址模式没有分页,因此段运算会直接生成物理地址。现代 64 位系统仍保留相同的概念顺序,但长模式会禁用用于地址生成的普通分段,并要求启用分页。
使用物理地址编程 #
Intel 8080 和 8085 等早期处理器将物理地址空间直接暴露给指令。它们的指令长度最多为三个字节:操作码占用一个字节,因此立即数操作数最多占用两个字节。由于这些处理器具有 16 条地址线,两个字节的立即数可以表示任意物理地址。
汇编代码可以通过两种方式引用内存位置:
- 将已知地址编码到指令中。 汇编器将标签解析为目标地址,或者由程序员提供数值地址。例如,
SHLD 0x0055将HL寄存器对存储到物理地址0x0055,而JMP foo将控制转移到分配给foo的地址。标签使源代码更易维护,但生成的指令仍然包含固定地址。 - 解引用寄存器中保存的地址。 8080 和 8085 的通用寄存器为 8 位,而地址为 16 位。因此,寄存器对
BC、DE和HL用于保存地址。STAX B和STAX D分别通过BC和DE存储A;MOV M,A则通过HL存储。
这种直接模型很简单,但固定地址会使代码绑定到其加载位置。重定位需要修改每个嵌入的地址,而独立加载的程序必须协调各自的物理地址范围。
8086/8088:分段 v1 及其原因 #
8086 和 8088 具有 20 个地址引脚,可以寻址 1 MiB,但它们的通用寄存器和段寄存器宽度为 16 位。它们既没有虚拟内存,也没有分页。内存引用仍然会到达物理内存,但指令编码不再将 20 位物理地址作为通常的寻址形式。
编码一个 20 位立即数会占用完整的三个字节,并浪费四个位。它还会保留固定加载地址带来的不灵活性。将地址加载到寄存器中可以表示动态计算的位置,因此,专用的 20 位立即数形式只会增加硬件和编码复杂度,却不会覆盖新的使用场景。后续的一些 x86 指令保留了直接的偏移量形式(moffs),但它仍然会结合段进行解释;它不是无分段的物理地址操作数。
分段 v1 #
8086 的解决方案是将 16 位段值与 16 位偏移量组合起来:
- 共有四个段寄存器:
CS、SS、DS和ES。 - 每个内存引用都有一个默认段。取指使用
CS;普通数据引用使用DS;有效地址使用BP的引用以及栈操作使用SS。字符串指令使用规定的源段和目标段。对于许多引用,段覆盖前缀可以选择另一个允许使用的段。 - 指令根据可选的基址寄存器(
BX或BP)、可选的索引寄存器(SI或DI)以及可选的位移计算偏移量。合法的 16 位寻址组合受编码约束,但实际运算为base + index + displacement。至少有一个编码组件用于标识内存操作数。整数指令通常最多编码一个显式内存操作数;字符串指令是重要的例外,因为它们包含隐式的源和目标访问。
例如,MOV AX, [4] 使用默认的 DS 段。汇编器可以编码类似 MOV AX, CS:[4] 的覆盖形式;在常见的 moffs 编码中,后者会额外带有 0x2E 前缀。汇编器的书写方式可能不同,但该前缀的体系结构含义相同。
执行单元计算 EA。随后,总线接口单元将选定的段值左移四位,并加上 EA:
segment 4 5 0 0
offset F C 3 2
-------------------
sum 5 4 C 3 2
physical address = (segment << 4) + effective address8086 只公开 20 个地址位。如果加法产生了超出第 19 位的进位,总线地址会按 1 MiB 取模回绕。因此,在原始 8086 上,熟悉的 FFFF:FFFF 计算并不能用于访问 1 MiB 以上的某个唯一地址。由此可见,即使偏移量看起来像绝对地址,以 [BX] 或 [40] 形式书写的引用也不会绕过段运算。在实模式的描述中,“实物理寻址”意味着分页不会将最终地址虚拟化;它并不意味着指令可以在不使用段的情况下编码任意物理地址。
段值是段落号。它们提供 4,096 个可能的基址,每个基址相隔 16 字节,而每个偏移量可以覆盖一个 64 KiB 窗口。段可以相邻、不相邻、部分重叠或完全相同。因此,一个物理位置可以有多种 segment:offset 表示形式。
结论:8086/8088 内存分段 #
分段主要用于弥补寄存器宽度小于地址总线宽度的问题。隐式段还缩短了许多指令,因为指令只需编码偏移量。Intel 曾考虑将段值左移八位而不是四位,这会创建一个 24 位地址空间和 256 字节的段对齐粒度。四位移位在保持 1 MiB 设计目标的同时,提供了更细的放置粒度。
这种安排使软件复杂化,因为指针不再是一个单一的扁平数值。当通用寄存器达到地址空间宽度后,这种成本越来越难以证明合理,但兼容性要求使分段继续保留在体系结构中。
80286:分段 v2 #
80286 保留了 16 位通用寄存器,同时将物理地址总线扩展到 24 位,支持最多 16 MiB。它引入了实地址模式和保护模式。复位时,它从实地址模式开始运行。
80286 的 MSW.PE 位启用保护模式。与后续 x86 处理器不同,80286 不能仅通过清除此位从保护模式返回实地址模式;软件通常通过复位处理器来返回。这一限制对早期操作系统很重要,但不会改变任一模式下的地址转换规则。
实地址模式 #
实地址模式保留与 8086 兼容的 segment:offset 寻址。段寄存器包含 16 位段值,处理器将段值左移四位,再加上 16 位偏移量来形成地址。与原始 8086 封装不同,80286 暴露了地址线 A20。启用 A20 后,FFFF:FFFF 附近的引用可以访问 1 MiB 以上的高端内存区域;兼容性硬件可以屏蔽 A20,以复现 20 位回绕。因而,为 8086 编写的程序可以在不修改源代码的情况下运行,但仍受其其他硬件假设的影响。
80286 还使新指令可以在实地址模式下使用。这些指令不会将实地址分段变为保护分段,也不会添加分页。
保护虚拟地址模式 #
80286 保护模式增加了受保护的可变大小段,但尚未实现基于页表的虚拟内存。因此,“保护虚拟地址模式”指的是保护分段,而不是与 80386 及后续处理器相关的按需分页虚拟内存模型。
驻留在内存中的两个描述符表定义了各个段:
- 全局描述符表(GDT)是系统范围的。
- 局部描述符表(LDT)可以描述与某个任务关联的段。
现在,每个段寄存器的可见部分是一个 16 位的选择子,而不是移位后的基址。其格式如下:
index选择一个八字节描述符。TI清零时选择 GDT,置位时选择当前 LDT。RPL是请求特权级,并参与保护检查。
加载选择子时,处理器会验证它,并将所选描述符加载到与段寄存器关联的、体系结构上不可见的描述符缓存中。缓存包含后续引用所需的基址、界限和访问权限。读取段寄存器时只会暴露选择子;软件不能直接读取或写入其缓存字段。缓存避免了每次内存引用都访问描述符表。
全局描述符表(GDT) #
GDTR 定位 GDT。在 80286 上,它包含一个 24 位物理基址和一个 16 位界限:
界限是表的字节大小减一,因此允许编码的最大大小为 65,536 字节。由于每个描述符占用八个字节,有效的完整表会在描述符边界结束。特权指令 LGDT 和 SGDT 负责加载和存储该寄存器;普通数据指令不能寻址 GDTR。
GDT 是描述符数组。S = 1 的描述符是代码段或数据段描述符。S = 0 的描述符是系统描述符,例如 LDT 或任务状态描述符。80286 段描述符存储 24 位基址、按字节粒度计的 16 位界限以及一个访问字节:
该图概括的是字段布局,而不是最后四字节组内部的字节序:描述符的低 16 位是界限,随后是基址的第 15 至第 0 位。此时,一个段成为具有任意 24 位基址和可变界限的连续范围;不再使用 16 字节移位。由于 80286 没有页转换,描述符基址标识的是物理内存。
GDT 的第 0 项必须是全零的空描述符。当 TI 选择 GDT 时,它使选择子索引 0 无效。
局部描述符表(LDT) #
LDT 与 GDT 具有相同的描述符布局,但其第一个条目可以有效。内存中可以存在多个 LDT,而机器同一时间只使用一个 GDT。LDTR 的可见选择子标识 GDT 中的一个 S = 0 的 LDT 描述符;其不可见缓存提供该 LDT 的基址和界限。LLDT 和 SLDT 负责加载和存储可见的 LDTR 选择子。
80286 的硬件任务机制可以在切换任务时重新加载 LDTR,从而使 LDT 具有任务特定性。操作系统通常避免使用硬件任务切换,因为软件上下文切换更灵活,也更易移植。在 IA-32e 模式下,硬件任务切换不可用,但任务状态段仍用于特权栈和中断栈管理等其他目的。
段寄存器 #
在任意时刻,选择子可以引用 GDT,也可以引用 LDTR 选定的 LDT。体系结构上,CS、SS、DS 和 ES 都由一个可见的 16 位选择子和一个不可访问的描述符缓存组成。通过 MOV、POP、LDS、远控制转移或其他相关指令加载其中一个寄存器时,处理器会从以下位置选择描述符:
缓存不是第二种软件可见的寄存器布局;该图表示选择子加载成功后由处理器保留的值。后续内存引用使用这些缓存值,而不是重新读取描述符表。
TI为 0 时使用 GDT;TI为 1 时使用当前 LDT。
随后,处理器会检查类型、存在性、界限和特权规则,然后安装描述符缓存。远控制转移 JMP、CALL、返回、中断或门可以加载 CS;普通 MOV 不能加载它。
索引为 0 且 TI = 0 的选择子是空选择子。将空选择子加载到 CS 或 SS 会触发故障。DS 和 ES 可以保存空选择子,但任何尝试使用这些寄存器进行内存引用的操作都会触发故障。后续处理器对 FS 和 GS 也采用相同的有效无效数据段约定。
结论:没有分页的 80286 分段 #
80286 增加了基于描述符的保护、特权检查和可变段大小。它还具有中断描述符表和 IDTR,但中断分派不会改变普通数据地址路径。由于不存在页表阶段,保护模式下的内存仍然是分段的物理内存。
自 80386 起:分段与虚拟分页 #
80386 引入了 32 位通用寄存器、32 位段基址和基于页表的虚拟内存。该模型同样适用于后续处理器上的 32 位保护模式软件。它定义了三个相关的执行模式:
- 实地址模式仍与 8086 兼容。
- 保护模式扩展了 80286 的保护分段,并可以启用分页。
- 虚拟 8086 模式将实模式风格的程序作为保护模式任务运行。分页和保护属于外部的保护模式环境,因此虚拟 8086 任务无法逃逸到其他进程的映射之外。
重新定义的保护模式 #
在 32 位保护模式下,原始 80386 的通用寄存器、偏移量、线性地址空间、段基址和物理地址总线全部为 32 位宽。描述符条目将其基址字段扩展为 32 位,并增加粒度和默认操作数/地址大小等标志。80286 中保留的描述符高字节获得了这些含义。
分段仍然完整有效,包括非零基址、界限、向下扩展数据段和特权检查。然而,32 位操作系统可以将其代码段和数据段的基址设为 0,将界限设为 4 GiB。这种平坦分段使偏移量等于线性地址,并允许分页执行进程隔离。段选择子本身并不为零:内核通常会为有效的平坦描述符加载诸如 0x08 或 0x10 这样的非空选择子。
80386 还增加了 SIB(scale-index-base)编码。它将 EA 的形成扩展为:
effective address = base + index × scale + displacement
scale = 1, 2, 4, or 8ModR/M 和可选的 SIB 字节选择参与计算的寄存器和位移大小。对于元素大小为 2、4 或 8 字节的数组,缩放因子很有用,但它只改变 EA 的形成,不改变分段或分页模型。
分页 #
设置 CR0.PG 会在保护模式下启用分页。此后,地址形成分为四步:
- 指令根据适用的
ModR/M、SIB、寄存器和位移计算 EA。 - 所选段的缓存描述符验证偏移量并提供基址。结果是一个 32 位线性地址。
- 分页使用该线性地址遍历以
CR3中的物理地址为根的页表。 - 所选页表项提供物理页帧基址;加上页内偏移后得到物理地址。
处理器会在分段地址阶段之前或期间检查描述符权限,并在转换期间检查页权限。不存在的条目或被禁止的访问会产生页错误。TLB(转换后备缓冲器)会缓存成功的线性地址到物理地址转换,因此操作系统在修改活动页表后必须使缓存的转换失效,或以其他方式完成同步。
传统 32 位分页 #
最初的 80386 方案使用 4 KiB 页和两级分页层次。CR3 标识当前地址空间的页目录;系统并不存在一个供所有地址空间使用的页目录。上下文切换可以加载另一个 CR3,而操作系统可以在多个页目录之间共享选定的内核映射。
页目录和每个页表都占用一个 4 KiB 页。每个表包含 1,024 个四字节条目:
页目录项(PDE)指向页表;页表项(PTE)指向一个 4 KiB 物理页帧。它们的低位包含相关但依赖层级的标志,包括 present、read/write、user/supervisor、accessed 和缓存控制属性。PTE 还具有 dirty 状态;启用页大小扩展后,PDE 可以直接表示大页。
32 位线性地址划分如下:
directory 选择 PDE,table 选择其页表中的 PTE,offset 选择所得 4 KiB 帧内的字节。启用 CR4.PSE 后,设置页大小位的 PDE 可以直接映射一个 4 MiB 页,从而省略页表阶段。
物理地址扩展(PAE) #
PAE 保留 32 位线性地址,但改变分页层次,并使用 64 位分页条目。通过 CR4.PAE 启用。早期 PAE 实现将物理地址扩展到 36 位;后续处理器的体系结构物理地址宽度取决于具体实现。
在 PAE 分页中,CR3 指向一个包含四个条目的页目录指针表(PDPT)。每个选定的页目录包含 512 个条目,每个选定的页表也包含 512 个条目。32 位线性地址划分如下:
页目录指针选择一个 PDPTE,后两个字段分别选择 PDE 和 PTE。设置页大小位的 PAE PDE 映射一个 2 MiB 页。当 EFER.NXE 启用时,PAE 还提供 no-execute 权限位所需的条目格式。没有 NXE 时,高位是保留位,而不是可执行策略控制位。
IA-32e 与长模式分页 #
IA-32e 模式是支持 64 位的执行环境,包含 64 位模式和兼容模式。两者都使用 IA-32e 分页,而且分页是强制的:IA-32e 模式处于活动状态时不能清除 CR0.PG。
进入 IA-32e 模式时,特权软件构造有效的 PAE 格式页表,设置 CR4.PAE,设置 EFER.LME,然后启用 CR0.PG。在 LME 已设置的情况下启用分页会锁存 EFER.LMA;该只读字段表示 IA-32e 模式处于活动状态。跳转到 L 位已设置的代码段会进入 64 位模式;L = 0 且默认大小合适的代码段会进入兼容模式。转换代码必须在启用分页前建立映射,因为下一条指令的取指会通过新的层次结构完成转换。
四级分页使用 PML4、页目录指针表、页目录和页表。每个表有 512 个八字节条目,因此每个索引为九位:
四级实现使用 48 位虚拟地址。第 63 至第 48 位必须复制第 47 位,从而使地址成为规范地址(canonical)。非规范地址会在页表遍历前触发故障。支持五级分页(CR4.LA57)的处理器增加一个九位的 PML5 索引,支持 57 位虚拟地址,并要求第 63 至第 57 位复制第 56 位。物理地址宽度仍取决于处理器,并通过 CPUID 报告;不能根据线性地址宽度推断物理地址宽度。
IA-32e 页表可以在 PTE 层映射 4 KiB 页,在设置页大小位的 PDE 中映射 2 MiB 页,并在处理器报告支持时,在设置该位的 PDPTE 中映射 1 GiB 页。页表项包含物理帧地址位以及权限、内存类型、accessed、适用时的 dirty、global 和 execute-disable 控制位。execute-disable 位只有在启用 EFER.NXE 时才生效。
IA-32e 模式中的分段 #
长模式改变了分段,但不会移除段选择子或描述符:
- 在64 位模式下,
CS、DS、ES和SS在普通地址生成中提供零基址。普通引用不会使用它们的界限。FS和GS保留有用的基址,这些基址由专用模型特定寄存器提供;操作系统使用它们保存线程本地数据或处理器本地数据。在加上FS或GS基址后,仍然要执行规范地址检查和分页。 - 在兼容模式下,16 位和 32 位保护模式应用程序保留保护模式分段语义,同时使用 IA-32e 分页。
- IA-32e 模式处于活动状态时,虚拟 8086 模式和硬件任务切换不可用。任务状态段对于特权转换和中断栈管理仍然是必需的。
因此,长模式内核并不是仅仅通过加载值为零的段寄存器来获得扁平地址空间。它根据 64 位模式规则获得普通的零基址,在需要的位置选择有效描述符,并依赖规范地址规则和分页来实现地址空间隔离与权限控制。
跨模式的地址转换 #
历史演进解释了今天仍可见的各层:
| 处理器和模式 | 有效地址与分段 | 分页 | 结果 |
|---|---|---|---|
| 8086/8088 实模式 | 16 位 EA 加 segment << 4 | 无 | 20 位物理地址 |
| 80286 实模式 | 与 8086 兼容的段运算 | 无 | 实地址物理访问 |
| 80286 保护模式 | 选择子解析为 24 位基址描述符 | 无 | 受保护的分段物理访问 |
| 80386 保护模式 | 32 位描述符基址加 EA | 可选的传统分页 | 线性地址转换为物理内存 |
| 32 位 PAE 模式 | 32 位保护分段 | 必需的 PAE 层次结构 | 更宽的物理地址转换 |
| IA-32e 64 位模式 | 除 FS 和 GS 外均为零基址 | 必需的四级或五级分页 | 规范线性地址转换为物理内存 |
因此,地址形成并不是一个通用的“x86 内存模型”。8086 使用分段将 16 位偏移量扩展为 20 位物理地址。80286 将段值转换为受保护的描述符选择子。80386 增加线性地址分页。PAE 改变分页条目的宽度和层次结构,而 IA-32e 强制要求分页,并在 64 位模式下减少普通分段的作用。
描述符的详细信息见 x86 保护模式与特权,用于编码有效地址的指令字段见 x86 指令编码。
评论
在 GitHub 上阅读或评论(发表评论需要登录)