为了方便程序的浮动装配(重定位),处理器访问内存时采用了 [段地址+偏移地址] 的策略,这是 IA-32 的基因。在保护模式下,段有了新的作用——权限管理的单位,操作系统将一定权限赋予给某些段 ,当段内指令访问内存或进行其他操作时,CPU 会根据段的权限来检查其行为的可行性 ,如果其行为越界,则会阻止并抛出异常。那么这些权限是如何记录的呢?请见下文。

段描述符

段的各类权限信息和其地址都被记录在 段描述符 中,其结构如下:

  • 段基址共 32 位,段界限共 20 位。可见它们都是“断开”的,这是因为需要兼容 80286 的 16 位保护模式,详细原因可见下一点。这种散乱分布不利于 CPU 获取段基址和段界限,所以段描述符中的内容会被整理好后存入描述符高速缓存器,CPU 直接从缓存器中获取段基址和段界限 。注意,对于数据段和代码段来说,段界限决定了偏移量的最大值;对于栈段而言,段界限决定了偏移量的最小值 ,其细节差异在内存保护与寄存器保护中有详细阐述。实际段界限等于 (段界限+1)*粒度-1 (减1是因为偏移从0开始)。段基址可选在任何地方,但最好与 16 位对齐。以段基址为起点开始偏移,如下:
    栈段与代码/数据段的段基址可以相同, 后续详述

  • G :段界限的粒度。G=0 时,粒度为 1 字节,则段最大扩展范围为 64KB;G=1 时,粒度为 4KB,则段最大扩展范围为 4GB。

  • D/B :默认 操作数/堆栈指针 大小。D=0 时表示该段指令中的偏移地址和操作数为 16 位,栈操作使用 SP 寄存器,偏址用 IP;D=1 时表示该段指令中的偏移地址和操作数为 32 位,栈操作使用 ESP 寄存器,偏址用 EIP。设置该标志位是为了兼容 80286 的 16 位保护模式。16 位保护模式基本绝迹,该位总是为 1。

  • L :64 位代码段标志,保留给 64 位处理器使用,现在直接置 0 即可。

  • AVL :软件可以使用的位,处理器不使用它,但使用该位是不安全的,谁也不知道 Intel 公司未来是否会使用该位。

  • P :段存在位。用来描述该段是否在内存中。当内存紧张时,有可能只建立描述符但对应的内存空间不存在,此时应将 P 设为 0;另外,内存紧张时,可能会把用得很少的段从内存移到硬盘中,腾出空间给急需内存的应用,此时同样应该将 P 清零,当需要该段时,再移入内存并置 P 为 1。这是多任务系统下常见的虚拟内存调度策略 。P 位通常由操作系统负责设置,由 CPU 负责检查。

  • DPL :Descriptor Privilege Level,描述符特权级。CPU 支持 4 种特权级:0,1,2,3,数字越小特权越高。注意,特权级描述的是要访问该段的最低特权级 。比如,若该段 DPL=2,则只有 DPL 为 0、1、2 的段才能访问该段。

  • S :指定描述符的类型。S=0 表示该段为系统段;S=1 表示该段为数据段/代码段/栈段。S 位和 TYPE 位配合才能确定段描述符的确切类型

  • TYPE :共 4 位,用于表示内存段或门的子类型。
    当 S=1 时 :对于代码段而言,这 4 位是 X, C ,R ,A;对于数据段/栈段而言,这 4 位是 X, E ,W ,A:

    • X:是否可执行。数据段/栈段总是不可执行,代码总是可执行。
    • E:扩展方向。栈段向下扩展,数据段向上扩展
    • W:是否可写。
    • C:指示段是否为特权级依从(Conforming);C=0 表示非依从的代码段,这样的代码段只能供与它特权级相同的代码段调用,或通过门调用;C=1 表示可依从的代码段,可以被特权级比它低的代码段调用。好奇的同学可提前阅读特权级全面剖析
    • R:是否可读。代码段一定可执行,一定不可写。是否可读,取决于 R 。若 R=1,则可以把此段当作 ROM 使用 。注意,是否可读是针对于其他代码段而言,而非 CPU ,CPU 不能读,哪还怎么运行。
    • A:指示最近是否访问过。创建该描述符时总是置 A 为 0;每当该段被访问,就置此位为 1。对 A 置 1 由 CPU 负责,置 0 由操作系统负责(创建时除外),操作系统通过定期监视该位状态来统计该段的使用频率。当内存紧张时,就可以把不经常用的段转移到硬盘中,从而实现虚拟内存管理。

    当 S=0 时

段描述符一共占 8 字节,每个段在使用之前都必须用段描述符“注册登记”。现代计算机都是多任务系统,所以会同时存在多个段,这些段描述符会被集中存放在内存中,这片集中存放的区域就构成了一个描述符表。

描述符高速缓存器

8086 CPU中,访问内存时,会先将段寄存器中的值左移四位,再和 IP 的值相加,得到物理地址。在 32 位 CPU 的实模式下,获取物理段地址的方式得到了优化:当引用一个段时,先将 CS 的中左移四位得到物理段地址,然后将该值放入 描述符高速缓存器 。此后就一直使用该缓冲器中的值,直到该段寄存器被重新赋值。这样一来,就省去了左移四位的时间,进一步提高了 CPU 访问内存的效率。注意,在实模式下,缓存器仅低 20 位有效,其他位全部为 0在保护模式下,当引用一个段时,段描述符中的内容会被整理好后存入缓存器,之后 CPU 访问内存时直接使用缓存器,直到该段寄存器被重新赋值。也就是说,在 32 位 CPU 下,实模式和保护模式都能够使用描述符高速缓存器 ,只是细节上略有差别。

整理的结果包括:1)结合零散的段界限和段基址;2)粒度*段界限,得到真实的段界限。

描述符高速缓存器是 32 位 CPU 中段寄存器的扩展部分,用来“整齐”存放段基址和段界限以及段属性。描述符高速缓存器是不可见的,由 CPU 内部使用 ,其结构如下:

可见,80386 后的处理器将段描述符整理进缓冲器前,都事先将粒度(G位)乘以段界限(20位)得到真实的段界限(32位),再存入缓存器。

再次强调,32 位 CPU 下,每个段寄存器都具有一个描述符高速缓存器。 其实准确来说,32 位 CPU 的段寄存器分为 16 位可见部分和不可见部分,不可见部分就是描述符高速缓存器,具体位数随 cpu 型号而变。

全局描述符表GDT

全局描述符(Global Descriptor Table,GDT )为整个软硬件系统服务。进入保护模式前,必须定义全局描述符表GDT 可以存放在内存的任意位置 ,为了定位 GDT,CPU 内部有一个全局描述符寄存器( GDTR ),该寄存器为 48 位,其结构如下:

基地址部分保存的是 GDT 在内存中的起始地址,边界在数值上等于 GDT 的大小减 1 ,换句话说,边界的值就是表内最后一字节相对于基地址的偏移量。表最大为 216=64KB2^{16}=64KB ,每个段描述符大小为 8 字节,故 GDT 最多能够装下 8192 个段描述符

GDT 可以在内存中的任何位置,但由于必须在进入保护模式之前定义 GDT ,而实模式下最多能访问 1MB 内存,所以一般将 GDT 定义在 1MB 以内的地址中。可以在进入保护模式后移动 GDT 的位置,但需要重新加载 GDTR 。

使用指令 lgdt (load gdt)将 GDT 的信息加载进 GDTR

1
lgdt  gdt_ptr

gdt_ptr 是标号,代表 GDT 所在的地址,指向一个包含了 48 位的内存区域。该区域的高 32 位必须为 GDT 的基地址,低 16 位为边界 。该指令在实模式和保护模式下都能够使用。

注意,GDT 中第 0 个描述符不可用 ,这是因为,如果使用的段选择子未经初始化,其值就为零,这便会访问到第 0 个段描述符继而处理器发生异常。这样就避免了忘记初始化而直接使用段选择子。

段选择子

我们已经知道,在保护模式下,段寄存器中装的不再是段基址,而是段选择子。段选择子索引到 GDT 表中的段描述符,然后 CPU 通过段描述符获得真实的段基址和偏址,从而进行段访问。段选择子结构如下:

  • 描述符索引:占高 13 位,213=81922^{13}=8192 ,和 GDT 能容纳的最多描述符个数相对应。索引值×8+GDT基地址 就能够定位到 GDT 中的表项。

  • TI:Table Indicator,描述符表指示器。TI=0 时,表示描述符在 GDT 中;TI=1 时,表示描述符在 LDT 。一般设置为 0 即可。

  • RPL:Request Privilege Leve,请求特权级。

    RPL,RCL,RDL 的区别?

总结

用一张图来总结以上四者的关系:

关于 G 位和 D/B 位的疑惑

前面说过,D/B 位为 0 时,表明该段模拟 16 位保护模式,段最大为 64KB 。但描述符中是 20 位段界限,即使粒度为 1 字节,最大段界限也能达到 1MB 。那么,CPU是否允许在 D/B 为 0 时,20 位段界限全部有效?或者,仅允许 16 位段界限有效(这似乎很好解释了为什么描述符中的段界限是断开的两部分)?粒度是否能为 4KB?

文章参考:《操作系统真相还原》《x86汇编语言:从实模式到保护模式》