前言
在早期8086体系结构中,由于CPU性能有限,所以只提供了20位地址总线,因此它的寻址范围只有1M. 随着80386, 80486的出现,提供了32位地址总线,寻址范围变成了我们所熟悉的4GB. 现代的操作系统,都是运行在保护模式下的.保护模式下,出现分段机制,使得操作系统有一定的保护能力. 但是有人会问,既然有了保护模式,为什么需要存在实模式?其原因主要是为了软件向后兼容.
实模式(Real Mode)
实模式下最显著的特征就是,你所看到的地址都是物理地址,而且地址不会超过0xFFFFF(1M). 实模式下,有8个16位的通用寄存器以及4个16位的段寄存器.CPU取指依赖于cs寄存器和pc寄存器的内容. 既然段寄存器都是16,怎么寻址20位地址空间呢? 原因在于,CPU通过($cs << 4) + $pc
算得下一条指令的地址.
例如:cs寄存器为0xf000, pc寄存器为0xfff0, 则CPU下一条指令的地址为0xffff0.
保护模式(Protected Mode)
实模式下,寻址空间就1M,并且没有分段分页机制的存在,所以比较适合单道程序. 但是随着CPU技术和工艺的发展,以及用户需求的增加,现在的操作系统普遍都是多用户多道程序分时的模式. 不仅如此, 内存的需求量也日益增长, 8086的1M内存,到80386的4GB内存,到现在32GB内存的出现也让操作系统允许多个程序争抢系统资源.因此,保护模式的存在是十分有必要的. 保护模式的存在,不仅为存储共享和保护提供了硬件支持,而且为实现虚拟存储提供了硬件支持.通过提供4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码数据的安全及任务的隔离.
在保护模式下,CPU取指仍然依赖于cs寄存器和pc寄存器的内容,但是其规则已经不是通过($cs << 4) + $pc
算得下一条指令的地址了. 当然,为了了解保护模式下,CPU取指的工作机制,现在有必要先介绍下三个概念.
- 物理地址
- 线性地址
- 逻辑地址
在保护模式下,允许分段机制的存在,程序员看到的内存地址实际上是逻辑地址(虚拟地址), 从逻辑地址到线性地址的转变是由分段机制存在而决定的.逻辑地址通过段翻译单元(segment translation unit)变成线性地址.如果不存在分页机制,那么线性地址就是物理地址;如果存在分页机制,那么线性地址还要通过也页翻译单元(page translation unit)变成物理地址;

小结: 无分页机制: 逻辑地址 ===> 线性地址 (物理地址) 有分页机制: 逻辑地址 ===> 线性地址 ===> 物理地址
分段存储管理机制
在这一小节,我们将介绍如何将逻辑地址转换成线性地址. 分页机制暂时不再本篇文章介绍. 所以假定线性地址就是物理地址.
只有在保护模式下,才能使用分段管理机制. 分段机制将内存划分成以起始地址和长度限制这两个二维参数表示的内存块,这些内存块就称之为段(segment). 编译器把源程序编译成执行程序时用到的代码段、数据段、堆和栈等概念在这里可以与段联系起来,二者在含义上是一致的.
在保护模式这一大节刚开始就介绍了,CPU取指不再通过($cs << 4) + $pc
算得下一条指令地址,而是通过段选择子(selector)和段偏移(offset)组成. 段选择子,通过段描述符表,索引到段基址(base address). 段基址+段偏移=线性地址.

接下来详细介绍给定一个段选择子和段偏移,如何一步步形成线性地址的过程. 首先,介绍三个重要的数据结构: - 段描述符表 - 段描述符 - 段选择子
段描述符表: 顾名思义,就是存放段描述符的一张表.典型地,拥有两种类型段描述符表:全局描述符表(global descriptor table, GDT)和本地描述符表(local descriptor table, LDT). 由于全局描述符表中不包含自己的信息,所以全局描述符表的起始地址存放在一个叫做全局描述符表寄存器中(GDTR). GDTR一共有48位,其中高32位为GDT的起始地址, 低16位为段界限,也就是GDT表大小.另外注意,全局描述符表中第一个段描述符设定为空段描述符.
段描述符: 其为段描述符表的一个表项(entry), 占8字节. 在分段存储管理机制的保护模式下,每个段由如下三个参数进行定义:段基地址(base address)、段界限(limit)和段属性(attributes).
- 段基地址:规定线性地址空间中段的起始地址.在80386保护模式下,段基地址长32位.因为基地址长度与寻址地址的长度相同,所以任何一个段都可以从32位线性地址空间中的任何一个字节开始,而不像实模式下规定的边界必须被16整除.
- 段界限:规定段的大小.在80386保护模式下,段界限用20位表示,而且段界限可以是以字节为单位或以4K字节为单位.
- 段属性:确定段的各种性质.
- 粒度位(granularity),用符号G标记.G=0表示段界限以字节位位单位,20位的界限可表示的范围是1字节至1M字节,增量为1字节;G=1表示段界限以4K字节为单位,于是20位的界限可表示的范围是4K字节至4G字节,增量为4K字节.
- 类型(type): 用于区别不同类型的描述符.可表示所描述的段是代码段还是数据段,所描述的段是否可读/写/执行,段的扩展方向等.
- 描述符特权级(descriptor privilege level, DPL): 用来实现保护机制.
- 段存在位(segment-present bit): 如果这一位为0,则此描述符为非法的,不能被用来实现地址转换. 如果一个非法描述符被加载进一个段寄存器,处理器会立即产生异常. 下图显示了当存在位为0时,描述符的格式.操作系统可以任意的使用被标识为可用(available)的位.
- 已访问位(accessed bit):当处理器访问该段(当一个指向该段描述符的选择子被加载进一个段寄存器)时,将自动设置访问位.操作系统可清除该位.

段选择子: 这是一个16位的数据结构,用来索引段描述符的.选择子可以做为指针变量的一部分,从而对应用程序员是可见的,但是一般是由连接加载器来设置的.其包含三个参数:索引(index), 表指示位(table indicator, TI), 请求特权级(requested privilege level, RPL). - 索引:在描述符表中从8192个描述符中选择一个描述符.处理器自动将这个索引值乘以8(描述符的长度),再加上描述符表的基址来索引描述符表,从而选出一个合适的描述符. - 表指示位:选择应该访问哪一个描述符表.0代表应该访问全局描述符表(GDT),1代表应该访问局部描述符表(LDT). - 请求特权级:在保护模式下,特权级总共有4个,编号从0(最高特权)到3(最低特权).有3种主要的资源受到保护:内存,I/O端口以及执行特殊机器指令的能力.在任一时刻,x86 CPU都是在一个特定的特权级下运行的,从而决定了代码可以做什么,不可以做什么.这些特权级经常被称为为保护环(protection ring),最内的环(ring 0)对应于最高特权0,最外面的环(ring 3)一般给应用程序使用,对应最低特权3.

注: 全局描述符表的第一项是不能被CPU使用,所以当一个段选择子的索引部分和表指示位都为0的时(即段选择子指向全局描述符表的第一项时),可以当做一个空的选择子. 当一个段寄存器被加载一个空选择子时,处理器并不会产生一个异常.但是,当用一个空选择子去访问内存时,则会产生异常.
小结: 有了以上三个概念后, 我们可以概括为当给定一个段选择子(selector)和段偏移(offset),我们可以得到线性地址过程为: 1. 取selector的高13位, 并且乘以8(左移3位), 记为sel; 2. 从GDTR中取高32位,加上sel得到段描述符起始地址,从而取得对应的段描述符; 3. 通过段描述符, 取得段基址; 4. 段基址 + 段偏移 = 线性地址;
补充
数据段选择子的整个内容可由程序直接加载到各个段寄存器(如SS或DS等)当中.这些内容里包含了请求特权级(Requested Privilege Level,简称RPL)字段.然而,代码段寄存器(CS)的内容不能由装载指令(如MOV)直接设置,而只能被那些会改变程序执行顺序的指令(如JMP、INT、CALL)间接地设置.而且CS拥有一个由CPU维护的当前特权级字段(Current Privilege Level,简称CPL).二者结构如下图所示:

代码段寄存器中的CPL字段(2位)的值总是等于CPU的当前特权级. CPU会在两个关键点上保护内存:当一个段选择符被加载时,以及,当通过线性地址访问一个内存页时.因此,保护也反映在内存地址转换的过程之中,既包括分段又包括分页.当一个数据段选择符被加载时,就会发生下述的检测过程:

因为越高的数值代表越低的特权,上图中的MAX()用于选择CPL和RPL中特权最低的一个,并与描述符特权级(Descriptor Privilege Level,简称DPL)比较.如果DPL的值大于等于它,那么这个访问可正常进行了.RPL背后的设计思想是:允许内核代码加载特权较低的段.比如,你可以使用RPL=3的段描述符来确保给定的操作所使用的段可以在用户模式中访问.但堆栈段寄存器是个例外,它要求CPL,RPL和DPL这3个值必须完全一致,才可以被加载.下面再总结一下CPL、RPL和DPL:
- CPL:当前特权级(Current Privilege Level) 保存在CS段寄存器(选择子)的最低两位,CPL就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别)
- DPL:描述符特权(Descriptor Privilege Level) 存储在段描述符中的权限位,用于描述对应段所属的特权等级,也就是段本身能被访问的真正特权级.
- RPL:请求特权级RPL(Request Privilege Level) RPL保存在选择子的最低两位.RPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限.RPL的值可自由设置,并不一定要求RPL>=CPL,但是当RPL<CPL时,实际起作用的就是CPL了,因为访问时的特权级保护检查要判断:max(RPL,CPL)<=DPL是否成立.所以RPL可以看成是每次访问时的附加限制,RPL=0时附加限制最小,RPL=3时附加限制最大.
总结
CPU从实模式进入保护模式后,内存寻址的方式发生了很大改变,从原来的($cs << 4) + $pc
寻址,变成通过段选择子,段描述符,段偏移确定线性地址. 当然为了同时兼容实模式和保护模式的存在,x86CPU引入了A20 Gate这一机制.A20 Gate简单来说,就是通过一种特殊的机制,利用CPU给键盘I/O端口输入指定指令,激活20~31地址总线(从0开始).从而使得能够使用32根地址总线进入保护模式.所以,总的来说,从实模式进入保护模式需要做两件事情.第一,激活A20 Gate; 第二, 建立段描述符表;
参考文献
- 清华 ucore lab1
- MIT 6.828 JOS学习笔记6. Appendix 1: 实模式(real mode)与保护模式(protected mode)