在实模式下,地址总线宽度是20位,但是寄存器大小只有16位,寄存器的位数小于总线宽度,访问物理地址的寻址方式为“段基址:段偏移”,段基址由段寄存器提供。在保护模式下,地址总线是32位,通用寄存器也变成32位。这就意味着段基址寄存器已经失去了作用,因为寄存器的位数足够存储一个完整的地址。但实际上,保护模式并没有直接放弃段寄存器,而是沿用了实模式下的16位段寄存器,只不过其中存的不再是段基址,而是被称为段选择子(Selector)的东西。
相比于8086芯片,i386中多了一个名为GDT(Global Descriptor Table,全局描述符表)的结构。它实际上就是内存中的一个数组,其中的每一项都是一个全局描述符。全局描述符中详细定义了段的起始地址、界限、段的属性等内容。而16位的段寄存器中存储的就是GDT这个数组的下标,也就是说在保护模式下,段寄存器变成了全局描述符的索引,这个索引也被称为段选择子。
段选择子的结构如图2-1所示。它的低两位代表选择子自己的RPL(Request Privelege Level,请求特权级),特权级的相关知识会在后面的章节专门加以介绍,这里只需要知道它的作用是控制访问权限。选择子从低位数第2个位是TI,它会当前选择子对应的描述符表是全局的还是局部的。在创建进程的时候,操作系统才会遇到局部描述符表,这里先不用深入了解它。
图2-1 段选择子的结构
在实模式下段寄存器只起到了段基址的作用,对于段的各种属性并没有加以定义,任何指令都可以对代码段进行随意更改。但是保护模式使用了GDT来记录段基址信息,描述符中除了记录段基址之外,还记录了段的长度,以及定义了一些与段相关的属性。段描述符共占据8个字节,64位,它的结构如图2-2所示。
图2-2 段描述符的结构
段描述符中的段基址(即图中的Base)分成了两段,占据了2、3、4、7这4个字节,共32位。段界限值(Limit)也分成了两段,共20位。下面,结合图2-2,再详细介绍段描述符的各个属性位。
1)P位:指示了段在内存中是否存在:1表示段在内存中存在;0则表示不存在。
2)DPL:占据两个位,代表了描述符的特权级。Intel规定了CPU工作的4个特权级,分别是0、1、2、3,数字越小,权限越高。以Linux为例,Linux只使用了0和3两个特权级,并且规定0是内核态,3是用户态。后面的章节将会详细介绍特权级,这里先不展开论述。
3)S位:其值为1时代表该描述符对应的段是数据段或者代码段,值为0时代表该段是系统段或者门描述符。
4)TYPE:定义描述符类型。具体的类型如表2-1所示。
5)G位:代表段界限粒度(Granularity),值为0时粒度为1B(字节),值为1时粒度为4KB。在段描述符中,段界限共20位,当粒度为4KB时,段界限的最大值为4GB。这是i386 CPU上可以支持的最大段长度。
6)AVL位:保留位,可供系统软件使用。
7)D/B位:这一位又分三种情况。
在可执行代码段描述符中,这一位称为D位。其值为1时,默认情况下指令使用32位地址及32位或8位操作数。值为0时,默认情况下使用16位地址及16位或者8位操作数。
在向下扩展数据段的描述符中,这一位叫作B位。其值为1时,段的上部界限为4GB。其值为0时,段的上部界限为64KB。
在堆栈段描述符中,即由ss寄存器指向的段,这一位叫B位。其值为1时,隐式的栈访问指令(例如push、pop)使用32位栈指针寄存器esp。其值为0时,隐式的栈访问指令使用16位栈指针寄存器sp。
表2-1 描述符类型
表2-1中出现的一致代码段、TSS、中断门、调用门等专用名词,在当前阶段还用不到。本书的目标是构建操作系统内核,而不是全面地介绍CPU的工作原理。所以,读者只需要掌握与操作系统相关的机制即可,这里就先不介绍了,等用到的时候再详细研究。
实际上,Linux内核并没有使用CPU的全部机制,也能实现全面强大的功能,甚至有些机制的实现比硬件实现得更好。这一点,大家在学习操作系统源码的时候是要加以注意的。
GDT的本质是内存中的一个数组,它的表项是全局描述符,全局描述符是由操作系统设置,供CPU使用的一个结构。理论上,它可以放在内存中的任意位置,只需要将它的起始地址告诉CPU即可。这就是GDTR(Global Descriptor Table Register,全局描述符表寄存器)的作用。
操作系统设置完GDT以后,要使用一条特殊的指令将GDT的起始地址加载进GDTR。CPU在处理一个逻辑地址cs:offset的时候,就会将GDTR中的基址加上cs中的下标值来得到一个段描述符,再从这个段描述符中取出段基址,最后将段基址与偏移值相加,这样就可以得到线性地址了。在保护模式下,CPU将逻辑地址转换成线性地址的过程如图2-3所示。
线性地址是虚拟地址,它的取值范围是从0~4GB,CPU还不能直接通过线性地址访问物理内存,还需要再通过页表将线性地址转换成物理地址。
在开始介绍页表之前,我们对段式管理的特点做一点总结。段式管理会按功能把内存空间分割成不同段,有代码段、数据段、只读数据段、堆栈段等,为不同的段赋予了不同的读写权限和特权级。通过段式管理,操作系统可以进一步区分内核数据段、内核代码段、用户态数据段、用户态代码段等,为系统提供了更好的安全性。
图2-3 逻辑地址转换成线性地址
但是段的长度往往是不能固定的,例如在不同的应用程序中,代码段的长度各不相同。如果以段为单位进行内存的分配和回收的话,数据结构非常难于设计,而且难免会造成各种内存空间的浪费。页式管理则不按照功能区分,而是按照固定大小将内存分割成很多大小相同的页面,不管是存放数据,还是存放代码,都要先分配一个页,再将内容存进页里。
页式管理的优点是大小固定,分配回收都比较容易。而且段式管理所能提供的安全性,在现代CPU上也可以由页表项中的属性替代实现,所以现在段式管理已经变得越来越不重要了。像64位Linux系统,它把所有段的基地址都设成了从0开始,段长度都设置为最大。这样,段式管理的重要性就大大下降了。