x86的段机制是实现程序的逻辑地址到线性地址的映射的一种机制。
我们先介绍实现这个机制的几个组成部件,它包括一些软件的东西和一些硬件的东西:硬件的东西有:段寄存器、分段部件、段描述符高速缓冲器;软件的东西有(即几个数据结构):段描述符表、段描述符;另外得知道的两个概念前面已经介绍过了:逻辑地址和线性地址。
下面我们看段机制是怎么用这些概念实现的。
由于段寄存器是16位的,所以它不能存储32位的基地址,但是这32位基地址又必须得存,于是我们把这32位基址存储到一块内存中,而把这个地址的索引放段寄存器里边,从而得到段基址,这是大体思路。但x86做得更好。
我们知道程序都有模块性,一个复杂的大程序总可以分解成多个在逻辑上相对独立的模块或者线程。每个模块都是一个单独的段,都以该段的起点为0相对编址。也就是说,我们得用一个数据结构描述各个段的相关信息,比如该段装进内存的基址(段基址),该段的长度,以及该段是否存在内存中等信息,这个数据结构就是段描述符。言下之意,我们每个模块都有一个段描述符,用来描述该段的属性,而段描述符表其实就是用来盛载这些段描述符的一个表格,一个段描述表可能长这样:
其中的表项就是段描述符,段描述要存储三个信息:基地址、段长度和段属性。我们用8个字节来存储这三个信息,首先我们说段基址是32位,所以基址占四个字节32位,然后我们用20位来描述段长度。那么这8个字节64位还剩下12位,就用来存储段的属性信息了。
G:一位,G=0:表示以字节为单位表示段的长度,刚才我们说了我们用20位存储段的长度,那么这样我们段的最大长度就是2^20次方,即1M大小,即我们给程序分段的时候,一个段最大是1M。G=1:表示以4KB表示段的长度,这样我们一个段的最大长度就是2^20次方乘以4KB,即4GB。后边我们会知道linux内核就让这个G等于1,从而直接越过x86的段机制,使逻辑地址没映射线性地址,再映射到物理地址,而是直接映射到物理地址(分页机制)。
D:一位,表示操作数的位数,D=1表示32位操作数,D=0表示16位操作数,这显然是为向下兼容而设计的。
接下来两位暂时没用,可留作扩展。
P:一位,表示这个段是否在内存中,如果我们要把这个段加载进内存,那么在加载进去的时候把这个值修改为1,否则让它等于0。
DPL:两位,表示段描述符的特权级。
S:一位,表示这个段是系统段还是用户段。S=0,则为系统段,即内核专门使用的段,S=1,表示用户段,即为程序的代码段、数据段或堆栈段。
类型占三位:依次是E、D、W。E=0,为数据段描述符,这时D位表示数据的扩展方向,D=0,表示向地址增大的方向扩展,反之,向地址减小的方向扩展。E=1时,直接表示的是数据段,此时W=0时表示数据段不能写(我们就可以想到C++中定义const变量的时候,最后肯定是修改了这个值的),W=1,数据段可写。
保护模式下,有三种类型的描述符表,分别是全局描述符表(GDT),中段描述符表(IDT),局部描述符表(LDT)。为了加快对这些表的访问,Inter设计了三个专门的寄存器,GDTR、IDTR、LDTT,以存放这些表的基地址和表的长度界限。
下来我们看段寄存器。我们说过段寄存器存放的就是段描述符在段描述符表中的索引,其实段寄存器还存了另外两个信息,这意味着,我们不能用着16位全部来存储索引值。其实Inter只用了13位来存储段描述符的索引,另外还用1位标志我们是从全局描述符表(GDT)中选择段描述符还是从局部描述符表(LDT)中选择段描述符。剩下两位表示请求者的特权级。
保护模式提供了4个特权级,用0-3表示。0表示最高特权级,对应内核态,此时它可以访问内核代码,也可以访问用户代码;3表示最低特权级,对应用户态,此时它只能访问用户态代码。
下面是段机制的硬件构成:
这样就完成了逻辑地址到线性地址的映射。