一、保护模式的内存寻址过程
与实模式不同的是,保护模式下内存段不再是简单地用段寄存器加载一下段基址然后乘以16位结合偏移地址得出实际要访问的内存地址,而是通过选择子在全局描述符表中找到对应的段描述符,CPU从段描述符中提取段基址,再与偏移地址结合得出实际要访问的内存地址。
以下我们来模拟一下保护模式寻址过程,下面会出现大量的专业术语,暂时看不懂也没关系,后面会有各个名词的详解:
首先,在保护模式下,每一个内存段都不能再被简单地直接读取,而是用一个表来记录每一个内存段的详细信息(为了安全),该表包括这段内存谁可以访问(权限),起始地址在哪里(段基址)等等。每一段内存段都会有一个表格来记录,这个用来描述某一个内存段的表就叫做段描述符:
段描述符可不是随随便便乱扔的,系统会将这些段描述符全部收集整理在一起,并且汇总成一个list目录,这个list目录就称之为描述符表。按照描述符的作用域,又可以将描述符表分为全局描述符表(GDT)和局部描述符表(LDT)。
由于段基址存放在了段描述符中,所以那些段寄存器就不再存放段基址了,而是存放一个称之为选择子(selector)的东西。选择子是一个索引值,此索引值用于在段描述符中索引相应的段描述符,从段描述符中得到内存段的起始地址和段界限等相关信息。
至此,所有主角已经全部登场,我们可以从头到尾推理一遍保护模式的寻址过程。首先,保护模式下的寻址方式为“段基址:段内偏移地址”。在访问具体某个内存段时,根据段寄存器中的选择子索引到全局描述符中该内存段的段描述符,并从该段描述符中提取出需要访问的内存段的段基址部分,最后CPU将段基址与段内偏移地址相加,得到实际的访存地址。
由此也可以得出实模式下与保护模式下的区别:
功能 | 实模式 | 保护模式 |
访问内存的方式 | 段基址:段内偏移地址 | |
段寄存器保存的内容 | 段基址 | 选择子 |
获取内存地址的方式 | 段基址乘以16,再与段内偏移地址相加得出实际内存地址 | 选择子索引到描述符后,CPU自动从段描述符中取出段基址,再加上段内偏移地址得出内存地址 |
二、选择子
我们重新将选择子的结构图示端上来:
可以看到选择子是16位的(因为段寄存器也是16位的),其中0~2位是RPL位,用于存储请求者的当前权限级别,权限级别有0、1、2、3四个等级。2~3位为TI位(Table indicator),用来表明选择子是在全局描述符表(GDT)中,还是在局部局部描述符表(LDT)中索引描述符,剩下的3~15位就是描述符的索引值,用于在描述符表中索引对应的描述符,选择子的索引值一共13位,即2的13次方8192,所以选择子最多可以索引8192个段,这与全局描述符表最多能容纳8192个描述符是吻合的。
三、段描述符
段描述符用于描述一段内存段的属性信息,该结构一共8个字节,结构如图先所示:
PS:可以看到,结构里面“段基址”和“段界限”两个属性被人为拆分放置在各个位置上,这其实属于一个历史遗留的问题,具体的原因不在此赘述,你可以理解为Intel为了兼容旧时代的CPU而做的兼容性措施,实际上使用时CPU会将其合并在一起形成一个连续完整的信息,并且存放在段描述符缓冲寄存器(如果不知道什么时段描述符缓冲寄存器,可以查看以前写的一篇文章:XXX)中以备后续使用。
下面我们在将每个位置都进行讲解:
(1)段基址位:这个没有什么好说的,将低32位的16~31位、高32位的0~7位以及高32位的24~31位组合在一起就是32位的段基址了
(2)G字段:Granularity粒度,处于高32位的23位中,用来指定段界限的单位大小的,如果G位为0,则代表段界限的单位为1字节,如果G位为1,则代表段界限的单位为4KB。
(3)段界限:位于低32位的0~15位,以及高32位的16~19位,凑成20位的段界限属性,该属性值用于限制段内偏移地址的,段内偏移地址必须在段界限范围以内,否则CPU将会认为是非法访问,并且抛出异常。段界限有最大界限和最小界限两种。对于数据段和代码段这种属于向上扩充的,地址越来越高的,此时段界限用来表示段内偏移地址的最大范围,而栈段则是向下扩充的,地址越来越低,此时段界限用来表示段内偏移地址的最小范围。那段界限可以表示的最大的范围是多少呢?这个主要是按照G字段的值来确定的:
- 如果G位为0,那个段界限的单位为1字节,那么段的范围最多可以表示为2的20次方等于1MB
- 如果G位为1,那个段界限的单位为4KB字节,那么段的范围最多可以表示为2的32次方(4KB等于2的12次方,12+20=32)等于4GB
(4)DPL字段:位于高32位的13~14位,Descriptor Privilege Level描述符特权级,用于表示内存段的特权级,特权级分为0~3级,数字越小权限越大,用户一般在3级,而CPU一般在0级。
(5)P字段:位于高32位的15位,Present在位符,如果该值为1,则表示段存在内存中,否则为0,该字段由CPU自行检查,我们来进行赋值。该字段其实也是为了兼容新的CPU做的措施,旧时代的CPU因为内存不足,允许段描述符中对应的内存段暂时换出来存放到硬盘中,待需要使用时再加载进行,现在的CPU即使内存不足也不会将整段换出,而是通过分页的功能按照页的单位将内存换入换出。
(6)AVL字段:位于高32位的20位,该字段没有专门的用途,我们可以随意的操作此位
(7)L字段:位于高32位的21位,用于设置是否位64位代码段,1即为64位代码段,0即为32位代码段。
(8)D/B字段:位于高32位的22位,用来指定有效地址(段内偏移地址)以及操作数的大小,指定操作数的大小,也就是对“指令”来说的,和指令相关的内存段是代码段和栈段,所以该字段会根据代码段/栈段的不同定义也不同。
- 如果是代码段,此位是D位,如果为0,表示指令中的有效地址和操作数是16位,指令有效地址用IP寄存器。如果为1,表示指令中的有效地址和操作数是32位的,指令有效地址用EIP寄存器。
- 如果是栈段,此位是B位,用来指定操作数的大小,如果为0,使用的是sp寄存器,那么栈的起始地址是16位寄存器的最大寻址范围0xFFFF。如果为1,使用的是esp寄存器,那么栈的起始地址是32位寄存器的最大寻址范围0xFFFFFFFF。
(9)S字段:位于高32位的12位,该字段和下面的type字段需要配合着用,该字段用于描述当前描述符是否为系统段,0表示系统段,1表示数据段/非系统段。系统段是各种称呼为“门”的结构,比如调用们、任务门之类的,它是硬件系统需要的结构,我们目前只需要关注非系统段(S字段为1)的就行。
(10)Type字段:位于高32位的8~11位,是整个段描述符中比较重要的字段,用来指定本段描述符的类型的,类型由以下这些:
我们目前只用到非系统段,所以当前只介绍非系统段的属性值。
- A位表示Accessed位,由CPU自行设置,每当该段被CPU访问过后,CPU会将此位设置成1,我们在调试时就可以根据此位判断该描述符是否可用,在创建一个新的段描述符时,需要将此位置设置为0。
- C位表示Conforming位,指如果自己是转移的目标段,并且自己是一致性代码段,那么自己的特权级一定要高于当前特权级,转移后的特权级不予自己的DPL为主,而是与转移前的低特权级一直,也就是听从转移前的低特权级。C为1时表示该段是一致性代码段,0时表示为非一致性代码段。
- R位表示Read位,指是否可读,1表示可读,0表示不可读。一般用来限制代码段的访问。比如指令执行过程中,CPU发现某些指令对R为0的段进行访问,CPU将会抛出异常。
- X位表示Executable位,指该段是否可以执行,X为1是代表代码段是可执行的,X为0时表示数据段是不可执行的。
- E位表示Extend位,用来标识段的扩展方向,E为0表示向上扩展,地址越来越高,用于代码段和数据段,E为1表示向下扩展,地址越来越低,用于栈段。
- W位表示Writable位,用来表明段是否可写,W为1表示可写,通常用于数据端,W为0表示不可写入,通常用于代码段。如果W为0的段有写入行为,CPU同样会抛出异常。
四、全局描述符表
描述符表可以分为全局描述符表(GDT,golbal descriptor table)和局部描述符表(LDT,local descriptor table)。
(1)全局描述符表
全局描述符表相当于描述符的数组,数组中每个元素都是8字节的描述符,可以用选择子中提供的下标在GDT中索引描述符。全局描述符表的“全局”体现在多个成都在里面定义自己的段描述符,是公用的。
(2)局部描述符表
局部描述符表和全局描述表其实差不多都是一个容纳描述符的数组,区别CPU厂商建议每个任务的私有内存段都使用自己的段描述符表来装,这个就是局部描述符表,随着任务之间的切换,局部描述符表也跟着切换,但是现实中很少用到局部描述符表,所以这里点到为止。
(3)描述符表寄存器
描述符表一般都位于内存中,需要使用专门的寄存器来初始化、调用它,用在全局描述符表的寄存器叫做GDTR,也就是GDT Register,用在局部描述符表的寄存器叫做LDTR,也就是LDT Register,寄存器的结构如下:
在使用描述符表寄存器之前,需要对寄存器进行初始化操作,指令格式为:
Lgdt 48位内存数据
48位内存数据刚好对应寄存器的48位,其中前16位为GDT的以字节为单位的界限值,后32为是GDT的起始地址。