x86内存分段

x86架构中,存储器分段(英语:Memory Segmentation)是在不改变16位段选择符时,使用单个索引寄存器(保存了段内地址偏移值)所能够寻址的的存储器范围部分。也指在英特尔x86指令集体系结构下存储器分段的实现方式。

8086开始到随后的各款x86架构CPU,无论是实模式还是保护模式,内存寻址时都使用16位段寄存器(segment register)。段寄存器默认使用情况为:

  • 代码段寄存器CS与寄存器IP相配合获得当前线程代码执行到的内存位置;
  • 数据段寄存器DS与各通用寄存器配合访问内存中的数据;
  • 栈段寄存器SS与寄存器(E)SP、(E)BP配合访问线程的调用栈(call stack);
  • 扩展段寄存器ES用于特定字符串指令(如MOVS或CMPS)。
  • 80386引入了2个额外的段寄存器FS与GS,并无特定的硬件用途。

这些段寄存器除了有16位的可见部分,还有不可见的隐藏部分,称为描述符缓存“descriptor cache”或隐藏寄存器“shadow register”[1]。当一个段选择符(segment selector)装入段寄存器的可见部分,处理器同时也把该段描述符的其它数据装入到段寄存器的隐藏部分,这包括段开始的基地址、段长度、访问控制资讯等。这些资讯缓存到段寄存器中,避免了处理器在转址(translate address)时花费额外的总线周期从段选择符表中读入数据。处理器指令中可以明示使用哪些段寄存器,这将替换掉默认使用的段寄存器。[2]

历史

1978年的Intel 8086开始引入了内存分段。这使得16位CPU可以访问超过64 KB (65,536字节)的内存,实际上8086 CPU到内存的地址总线是20位,即可访问220=1MB内存。在16位模式,要让应用程式使用多个存储器分段(为了存取比任一64K分段还要大的存储器)是相当复杂。这个问题的根源在于没有适当的适合做整个存储器范围的平面寻址的地址算术指令。平面寻址方式也可以乘法指令来完成,但这会导致较慢的程序执行速度。

1982年面世的80286处理器的实模式保护模式,以及80386及其后的处理器的虚拟86模式,一个分段的大小是64 KiB(使用16位索引寄存器)。在Intelx86实模式下的分段架构的内存空间会有所重叠,这是一种不好的设计。 80286的保护模式下,16位的段寄存器中的13位(称作段选择符segment selector)是描述符表的条目(descriptor table entry)的索引;该条目包含了24位的段开始地址以及16位的段长度;段开始地址与段内偏移地址相加即为内存物理地址。16位段寄存器中的剩余3位分别是全局/局部描述符表指示位、请求特权级(request privilege level)。

1985年面世的80386及其后续处理器的32位保护模式下,一个分段长度上限是220个粒度单位,粒度可以是1字节或4K字节,因此分段长度上限可以是4 GB,这与索引寄存器是32位相配合。

随着32位操作系统的推出,以及更舒适的32位平面存储器模式,到1990年末期几乎淘汰了使用分段寻址。然而,使用32位平面存储器模式产生的最多只能访问4 GB地址空间的限制并没有远离日常的使用。分段允许操作系统对每个行程虚拟寻址空间的限制,最大可利用64 GB的系统存储器,但这种最终回归到分段的尴尬,经常被引述为朝着64位处理器发展的动机。

2003年问世的x86-64架构下,强制实现了平面存储器模式,但保留了使用段寄存器FS或GS的64位下的分段寻址。

实模式 (Real Mode)

实模式虚拟86模式下,可访问内存固定为1 MB。对于8086处理器,可使用寄存器长度为16字节,但是拥有20字节的到内存总线地址。为了使用16字节寄存器访问20字节地址,整个内存被划分为多个区块。一个区块长65,536字节即64 KB。在实模式下,访问内存必须通过segment base address和segment offset(分别储存在任意segment寄存器和任意通用寄存器里。) 真实访问的物理地址可由以下公式得到:

物理地址=(segment base address*0x10) + segment offset [3]

注意同样的物理地址可以由多个不同对的selector,offset组成。 实模式下,任何程序都可以访问全部内存空间。没有对内存的访问权限保护。

80286保护模式

Intel 80286处理器仍然使用16位段寄存器与16位的段内偏移地址,但保护模式下支持访问224(16M)字节的内存。16位段寄存器内不再是段地址,16位段寄存器的高13位被称作段选择符(segment selector),其值是到段描述符英语segment descriptors表的索引值。段描述符中包含了24位的段开始的基地址,20位的段长度。段开始地址与段内偏移地址相加即为内存物理地址。段的长度上限为220=1M字节。

80386保护模式

Intel 80386处理器继续使用286的分段保护模式,但段描述符中包含了32位的段开始的基地址。段内偏移地址也是32位。在分段转址与物理地址之间又增加了一层分页(paging)转址。分段寻址是不能关闭的。分页可以使能或关闭(enabled or disabled),如果关闭就与286保护模式一样。如果使用分页机制,则由段开始的基地址与段内偏移地址相加得到的是线性地址(虚地址),线性地址还需要分页转址才得到内存物理地址。

386的段描述符中的段长度为20位,段长度的粒度可设为1字节或212字节。因此段长度可以为1字节-1M字节,或者为1×4K字节-1M×4K字节。段描述符的数据结构为;

  1. Byte offset inside entry.
  2. First range is the bit offset inside entry; second range is the bit offset inside byte.


386处理器增加了两个段寄存器FS、GS,这两个寄存器并无硬件绑定的用途。Windows操作系统在地址FS:0中保存了当前线程资讯块。Linux中GS指向了线程局部存储

通过清除控制寄存器CR0中的最低位,可由386保护模式转为实模式。

Linux操作系统在386保护模式下把段基址设为0,段长度设为4GiB,从而模拟了平面内存模型。

段寄存器名字 描述 基地址 段长度 段描述符特权级
__KERNEL_CS 内核代码段 0 4 GiB 0
__KERNEL_DS 内核数据段 0 4 GiB 0
__USER_CS 用户代码段 0 4 GiB 3
__USER_DS 用户数据段 0 4 GiB 3

x86-64的64位模式

在x86-64体系结构64位的long mode,段寄存器CS, SS, DS, ES强制为0。段长度强制为264。形式上还有内存分段,但实际上所有内存都在唯一的一个分段中。段寄存器FS、GS可以有非0值,被操作系统用于其它用途。即硬件支持如“FS:[RAX]”这样的寄存器间接寻址。

参考文献

  1. ^ "Intel 64 and IA-32 Architectures Software Developer's Manual", Volume 3, "System Programming Guide", published in 2011, Page "Vol. 3A 3-11".
  2. ^ Intel Corporation (2004). IA-32 Intel Architecture Software Developer's Manual Volume 1: Basic Architecture页面存档备份,存于互联网档案馆
  3. ^ OsDev. OsDev Segmentation. osdev.org. [2019-08-08]. (原始内容存档于2022-05-10).