0 引言
从80386开始,CPU有三种工作方式:实模式,保护模式和虚拟8086模式(v86模式)。只有在刚刚启动的时候是real-mode,等到操作系统运行起来以后就切换到protected-mode。实模式只能访问地址在1M以下的内存称为常规内存,我们把地址在1M 以上的内存称为扩展内存。在保护模式下,全部32条地址线有效,可寻址高达4G字节的物理地址空间;同时,保护模式还引入了“权限”,这是操作系统演变过程中非常重要的一步。
实模式与保护模式的最大差别在于内存寻址,可以认为实模式与保护模式下CPU对物理内存地址采用了两种不同的计算方式。
1 实模式下物理地址的计算方式
图1.1 实模式下物理地址的计算方式
如图1.1所示,实模式下CPU对内存物理地址的计算非常简单粗暴,即将CS段寄存器的值左移4位然后与IP寄存器中的值相加,得出的结果(20位的地址)就是要访问的内存的物理地址。简单粗暴的同时带来的问题也很明显,就是非常的不安全!所有程序都可以修改所有的物理内存,试想一下在写程序输出“hello world”的时候不小心把操作系统的关键内容给修改了,这是一件多么可怕的事!
2 保护模式下物理地址的计算方式
80286开始就引入了保护模式,由于286是一个过渡产品,本文选取了更具有代表性的80386来讲解保护模式。在保护模式下CPU对物理内存地址的计算与实模式下可以说是完全不同。保护模式采用分段的方式来为每个应用规定其能访问的内存区域,从而实现了保护的目的。保护模式下的段与实模式下的段不同,保护模式下的段的长度是可变的,而实模式下段的长度是不可变的,因为实模式下引入段的概念是为了更好的让只有16位寄存器的8086CPU去访问20位的地址空间,而非出于保护的目的。
2.1保护模式下的重要数据结构
(1)描述符(Descriptor)
图2.1 段描述符(Descriptor)
段描述符记录的是每个段的基址、界限、属性信息。如图2.1所示,每个段描述符占8个字节,段描述符由三大主要部分组成:段界限(20位)、段基址(32位)、段属性(12位)。(由于历史遗留问题他们都被分开存放)。段界限记录的是段内偏移地址的最大值,段基址记录的是该段的起始物理地址,段属性记录的是段的特权级、描述符类型等信息。
(2)全局描述符表(Global Descriptor Table,GDT)
每个段描述符记录的是一个内存段的信息,多个段描述符就构成了一个段描述符表(其实就是一个存放段描述符的数组)。全局描述符表(GDT)记录的是系统中所有可用的段的描述符。它存放在物理内存中,那么,操作系统是如何知道GDT的存在在哪呢?——通过特殊的硬件GDTR(Global Descriptor Table Register)。支持保护模式的CPU需要具有一个特殊的硬件GDTR。GDTR是一个48位的寄存器,它记录着GDT的起始地址以及界限。具体结构如2.2所示。
图2.2 GDTR结构示意图
在由实模式向保护模式转化的过程中,GDTR通过lgdt指令进行初始化。
(3)段选择子(Selector)
上面介绍了段描述符记录的段的基本信息,GDT记录的全局可用的段的信息,那么每个应用程序如何访问自己的内存段呢?——通过段选择子(Selector)。段选择子可以理解为是一个GDT中的索引,但与索引又略有不同。段选择子的结构如图2.3所示。
图2.3 段选择子结构示意图
段选择子占2个字节,其中RPL(Request Privilege Level)记录的是请求特权级,即以什么样的权限去访问段。
TI(Table Indicator)记录的是该段位于GDT还是LDT(Local Descriptor Table,与GDT类似)。描述符索引记录的是访问的段在描述符表(GDT or LDT)中的位置(相对偏移量)。值得注意的是,在启动保护模式之后段寄存器(CS、DS、ES等等)存储的就不再是段的物理基址而是段选择子。了解这一点很重要!!但是我们使用汇编编程时无需完成段选择子到段基址的转换,因为从段选择子到段基址的转换由硬件自动完成。
2.2保护模式下物理地址的计算
前文中说到了保护模式下计算物理地址所用到的三个重要数据结构,下面就讲讲保护模式下究竟是如何计算物理地址的。
图2.4 保护模式下物理地址的计算方式
如图2.4所示,在保护模式下物理地址计算可分为如下几步:
- 从段寄存器中取出选择子。
- 通过段选择子的索引信息以及GDTR记录的GDT的地址找到段描述符的地址。
- 检查权限等信息,完成后取得段的物理基址。
- 将物理基址与EIP寄存器中的偏移量相加得到实际物理地址。
其中保护模式的关键步骤在于第3步,该部分完成了对该访问请求是否有访问权限,也正是该部分实现了对内存的“保护”。
每次进行物理地址计算时都要进行访问内存中段描述符吗?——不是的,请看↓↓↓
3 关于段寄存器的一些不为人知的“秘密”
段寄存器(DS、CS、SS、ES、GS、FS)真的是只有16位吗?关于段寄存器为16位寄存器的说法是不严谨的。
图3.1 段寄存器的结构
如图3.1所示,每个段寄存器的可见部分(即我们可以通过mov指令读写的部分)为16位。可见部分记录着段选择子的信息,其隐藏部分记录着该选择子对应的段的信息,其中隐藏部分是在使用段寄存器第一次访问内存时自动从段描述符中加载过来的。那为什么这么做呢?原因很简单,就是为了加快访问内存。上文中说到GDT是存储在内存中的,若每次使用段选择子时都需要去内存中找到段描述符,然后再进行地址计算,这不就相当于访问了两次内存了嘛,访问速度也是可想而知的。硬件工程师也考虑到了这一点,于是就把段的信息加载到段寄存器的隐藏部分,每次需要段基地址时需要直接从段寄存器中获取。
参考文献
2.《Orange's一个操作系统的实现》于渊