进入32位保护模式(一):加载GDTR

实时模式下运行中的程序能访问所有内存,随着计算机上运行的程序越来越多,这个设计的风险也越来越大。保护模式在这个背景下应运而生。保护模式的主要思路就是把内存划分成若干个段,操作系统给这些段表明权限以及相关属性,在访问时就会根据权限和属性给予放行或拦截。

GDT

上面说了保护模式的思路就是把内存分段,这个分段的列表就是GDT(global descriptor table)全局描述符表。这个表中每个表项长度为8个字节,所以,表长度=表项数*8,这个表的起始地址存放在cpu的一个专用寄存器上,称之为GDTR(global descriptor table register)全局描述符表寄存器。

GDTR有48位,其中32位用来存放在内存中的起始地址,16位用来存放表的长度(其实严谨的来说是长度偏移量,因为在数值上总是等于表的大小减一,和c语言中的数组下标类似)。

32Mode_1_1

上面已经提到了,表项的大小是固定的8个字节,所以要想访问任何一项,只需要用GDTR中保存的基地址+表项*8就得到该项的地址,和通过下标对数组进行随机访问一样。

下面就来介绍GDT表项结构。

GDT表项的主要由3部分组成:内存区域的起始地址、内存区域的大小、内存区域的属性。

32Mode_1_2

由图可见GDT表项中的段基地址,段界限,还有属性等位置并不是连续分布的,这主要是考虑和前代CPU兼容性而妥协的设计。下面简单介绍以下各个部分的结构:

  • 段基地址:就是该描述符所定义的段的起始地址,总共由32位组成
  • 段长度(界限):段的长度,数值等于长度的大小减一,总共由20位组成
  • G(Granularity):粒度位,用于解释段长度的单位。当为0时,长度单位就是字节,当为1时,长度单位就是4KB,这时,一个段的最大长度能达到4GB
  • D / B:用来做16位保护模式兼容的位,现在使用的非常少了。如果不做16位兼容,设置成1就行
  • L:64位代码段标志
  • AVL(Available):通常给操作系统使用,处理器很少使用
  • P(Presence):用来保存段是否存在于内存中的标志,有时候没有内存时,操作系统会把部分不常用的段换入磁盘中,此位就会置为0。反过来如果从磁盘换入内存,此位就会置为1。
  • DPL(Descriptor Privilege Level):用来表示段的特权级别,从大到小分别是,0,1,2,3
  • S:用来表示段的类型,0为系统段,1为代码/数据段
  • TYPE:用来表述段的子权限,有点类似于Linux用RWX位来表文件的读写执行权限。对于数据段4位分别是XEWA,表示执行/扩展/写/访问权限,对于代码段4位分别是XCRA,表示执行/特权依从/可读/访问权限

GDT的加载

根据上述的规格设置好GDT的数据后,就需要把GDT表的地址和长度加载到系统的GDTR寄存器中,这个步骤有一个专用的指令

1
lgdt m48  ;该指令的操作数是一个48位的内存区域

指令的参数的48位对应的是GDT表的位置和长度,前(低)16位是长度,后面(高)32位是GDT的地址。

设置好GDT以后,需要进入保护模式才会生效,x86 cpu是通过设置CR0寄存器的PE位为1来进入保护模式。PE(Protection Enable)位在CR0的第1位(位0)。

1
2
3
4
5
mov eax,cr0   ;读取cr0
or eax,1 ;把第0位置为1
mov cr0,eax ;设置cr0

jmp dword 0x0008:flush ;进入32位模式后,要用jmp指令清空流水线(流水线里残留着16位的指令,会有问题)

保护模式下的段访问

段选择子

保护模式下访问内存地址还是需要通过段寄存器来进行,不过此时段寄存器的内容由以前的段地址变成了段选择子。所谓的段选择子最主要的就是要访问的段在GDT表中的序号。

32Model_1_3

段选择子的最主要的信息就是描述符索引,也就是段在段表内的序号。还有就是TI(table indicator)位,用来表示段在哪个表中,TI=0时,表示在GDT表中,TI=1时,表示在LDT(Local Descriptor Table)中。以及RPL(Require Privilege Level)位,请求特权级别,用来表示给出当前选择子的程序的特权级别。

选择子检查

进入保护模式以后,用段选择子设置段寄存器时,段寄存器会缓存这个选择子,下次再用同样的选择子访问时,就会直接用缓存的数据。而且在访问的过程中也同时对选择子的权限,以及访问范围进行检查,如果权限不够或者超出范围就会产生异常。

其他

第21条地址线A20

8086时代都是20条地址线,最大地址为0xFFFFF,再加一就会变成0x100000。但因为只有20位,所以最高位被丢弃,此时又会回到最低位地址0x00000。当年很多特性都依赖于这个特性,后来在80286后地址线扩展到24位,这个特性就不管用了。为了兼容以前的旧代码,IBM在键盘控制器的x60端口放置了一个控制门,后来因为太繁琐在80486以后直接添加了A20M#引脚。

A20m#引脚通过0x92端口控制,它的第7~2位没有使用,第0位是INIT_NOW,它从0变成1时,会重启CPU。而第1位就是用来控制A20的开关,当它为1时会启动第21条地址线。在INIT_NOW被从0设置成1时,会自动把A20位设置成1,所以现在一般不用手动来设置。这个历史遗留问题还是挺有趣的,理解x86的各种历史包袱的过程也不啻于看一部计算机发展史啊。

引用

  • GDT wiki

  • 《x86汇编语言-从实时模式到保护模式》,page 189