段描述符,描述符与GDT
在本节中我们开始编写IA-32e模式描述符部分,在此之前我们还是需要了解关于IA-32e的内容,我们从开始的实模式寻址开始讲解(本文理论内容有些偏多)
描述符(Descriptor)和全局描述符表(GDT)
实模式寻址方式
在16位模式中我们的寻址方式以段基址*16+偏移地址
形式寻址,其中段基址*16+偏移地址
得到的地址是线性地址,在16位实模式中线性地址可以跟物理地址做映射,因此我们可以把线性地址看成物理地址,段基址*16+偏移地址
得到的就是16位物理地址,在8086CPU中,一个寄存器的最大能够存储16位数据(2^16=65536(byte),65536(byte)>>10=64(kb)) ,但是8086CPU有20根地址线,因此最大寻址范围是 2^20 = 1048576(byte) 我们转换一下,结果就是 1048576(byte) >> 20 = 1(MB)
这里我们通过位移运算计算出来的,我们知道KB转MB的转换需要除以1024,1024转为二进制为
100 0000 0000
刚好10位,因此我们可以通过右移10位完成了KB转MB的操作,Byte转MB时需要除以2次1024,因此我们需要右移20位刚好得到
单个寄存器的最大只有64KB,因此一个段最大只支持64KB(65535byte,转为十六进制为0x10000
),单个寄存器不能表示1MB内存地址空间因此需要2个寄存器共同表示20位地址。
假设我们在ES(附加段寄存器)中存入0x1000,DI(变址寄存器)中存入0xFFFF,那么ES:DI=0x1000*0x10+0xFFFF=0x1FFFF(0x10转为十进制刚好是16)。
保护模式寻址方式
保护模式采用了全新的分段管理基址和分页管理机制来代替实模式基于段的寻址方式,保护模式支持分段和分页两种管理基址,处理器必须经过分段机制将逻辑地址转为线性地址后才能使用分页基址把线性地址转为物理地址(分页机制是可选项,但是分段机制是必选项)
虽然保护模式下依旧可以使用分段的形式进行寻址但是寻址的方式发生很大的变化了,段寄存器保存的不再是段的基地址而是一个16位索引值(称为段选择子Segment Selector),在保护模式中引入了一个新的结构称为描述符表(Descriptor Table),描述符表有2个,一个是全局描述符表(Global Descriptor Table,GDT)另一个是局部描述符表(Local Descriptor Table,LDT),在寻址的时候,处理首先将选择子从全局描述符表中索引出对应的段描述符并加载到段寄存器内,段寄存器再从刚加载的段描述符中取得段基地址
段寻址模式如下
段选择子结构如下
15 3 2 1 0
| | | |
+------------+-----+-----+
| Index | TI | RPL |
+------------+-----+-----+
段选择子位功能说明
名称 | 功能描述 |
---|---|
Index | 用于索引目标段描述符 |
TI | 目标段描述符所在的描述符表类型 |
RPL | 请求特权级 |
在保护模式中段寄存器也发生了变化,CS,SS,DS,ES,FS,GS寄存器中加入了缓存区域,这些缓存区域是不可见的(不能手动设置)记录着段描述符的基地址,段限长属性等信息,系统可以定义8192个段描述符,但是同一时刻只能使用6个段
程序要想访问某个段,必须将段加载到段寄存器中,例如执行程序需要用到数据段,代码段和堆栈段,那么必须将有效的段选择子加载到CS,DS,SS寄存器中(结构如下)
每个段寄存器都被划分为2个部分,隐藏部分(Hidden Part)和可见部分(Visible Part)隐藏部分称为影子寄存器(Shadow Register),当段选择子加载到段寄存器的可见部分时,处理器还会同时加载段基址,段限长,段属性到影子寄存器中,处理会根据这些缓存的信息,直接进行地址转换,避免了重复读取内存的开销
全局描述符表
在加载段选择子的过程中,处理器会将选择子作为索引,从GDTR寄存器指向的描述符表中索引(找出)段描述符,GDTR寄存器是一个48位伪描述符(Pseudo-Descriptor)保存着全局描述符表的首地址和长度,GDT不是段描述符,而是在线性地址中的一个数据结构,GDT需要将自己的基地址(线性地址)和长度使用lgdt
指令加载到GDTR寄存器中,GDT的长度为8N-1(N为描述符的个数)
全局描述符表的第0项被作为空选择子(NULL Segment Selector),处理器的CS和SS段寄存器不能加载空段,否则会发生#GP
异常,其他寄存器可以使用空段选择子初始化
段描述符
保护模式段描述符
63 56 55 54 53 52 51 48 47 46 45 44 43 40 39 32 31 16 15 0
| | | | | || | || | || || || || |
+-----------+--+---+--+---+--------+--+-----+--+----+-----------+-----------+--------+
|BaseAddr(H)|G |D/B|L |AVL|Limit(H)|P | DPL |S |Type|BaseAddr(M)|BaseAddr(L)|Limit(L)|
+-----------+--+---+--+---+--------+--+-----+--+----+-----------+-----------+--------+
- 段限长字段(Limit):段限长字段用于指定段的长度,处理器会把段描述符中两个长字段组合成一个20位的值,根据颗粒度标识指定段限长的实际含义,如果G=0则段长度的范围可从1字节到1MB字节,如果G=1则段长度可从4KB-4GB
- 基地址字段(BaseAddr):该字段在4GB线性地址空间中一个字节长度0所处的位置,处理器会把3个分立的基地址字段组合形成32位值,段地址应该对齐16字节边界
- 段类型字段(Type):指定段或门的类型,说明段的访问种类以及段的扩展方向,该段的解释依赖于描述符类型标识符S指明是一个应用描述符还是一个系统描述符
- 描述符类型标志(S):指明一个段描述符时系统段描述符还是代码或数据段描述符
- 段特权级(DPL):指明段描述符的特权级,特权级从0到3,0级最高,3级最低,DPL用于控制对段的访问
- 段存在标志(P):指出一个段时在内存中(p=1)还是不在内存中(p=0)
- 默认操作大小/默认栈指针大小标志(D/B):根据段描述是一个可执行代码段,下扩数据段,还是一个堆栈段(对于32位代码和数据这个标志总是设置为1,对于16位代码段,这个标志被设置为0)
- 颗粒度标识符(G):该字段用于确定限长字段的Limit值的单位,如果颗粒度标志为0则段限长值单位时字节,如果设置了颗粒度标志,则段限长使用4KB单位
- 可用和保留比特位(AVL):段描述符第二个双字的位20可供系统软件使用,位21时保留并总是设置为0
代码段描述符
代码段描述符Type结构如下
| 43| 42|41 |40 |
+---+---+---+---+
| - | C | R | A |
+---+---+---+---+
- A标志位(Accessed,已访问):记录代码是否被访问过,当A=1表示被访问过否则表示未访问,处理器只负责置位不负责复位
- R标志位(Readable,可读): 如果想读取程序段中的数据必须将该位置位
- C标志位(Conforming,一致性): 代码段分为一致性代码段和非一致性代码段,处理器通过此标志位可以进行标识
一致性代码段和非一致性代码段,我在刚看到这个的时候也是一头雾水,这里的一致性我的理解是在低特权级程序可以执行或跳转到高特权级(相同特权级)的代码段,并在执行过程中底特权级程序的CPL不变(执行前和执行后特CPL都是一致的,不会因为跳转到高特权级代码段的时候发生改变),所有的数据段都是非一致性的,意味着不能被底特权级程序访问
这样就好比你是一个普通职员,有一天领导让你去收拾一份保密文件,在收拾保密文件的过程中你的身份还是普通职员,并不会因为你收拾了保密文件而变成高级职员
数据段的非一致性就好比你工作时使用的数据可以被你上级查看,但是上级使用的数据你不能随意查看
如果段描述符的S位与第43位同时被置1,表示这个段描