保护模式篇——段描述符与段选择子
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
看此教程之前,问几个问题,基础知识储备好了吗?搭建好环境了吗?上一节教程学会了吗?没有的话就不要继续了。
🔒 华丽的分割线 🔒
CPU分级
如果要讲段描述符与段选择子,先介绍CPU
分级的概念。数值上越小,权限越大。如果低权限访问高权限的东西,会导致失败。0环
被内核使用,虽然1环
和2环
存在,但Windows
只用了3环
。注意在学习保护模式是时候不要把操作系统的概念扯进去,还没到操作系统层面。 CPU分级示意图如下:
GDT 与 LDT
GDT
是全局描述符表。LDT
为局部描述符表,但Windows
并没有使用它,故不再介绍,感兴趣请查询Intel白皮书。当我们执行类似MOV DS,AX
指令时,CPU
会查表,根据AX
的值来决定查找GDT
还是LDT
,并找到对应的段描述符。段描述符将会在后面部分进行介绍。
GDT表
存在于内存之中。CPU
要想找到它,就必须知道它的位置。于是乎CPU
有一个寄存器。它被称之为GDTR
,存储了GDT表的位置和大小,是一个48位的寄存器,用C语言表示如下:
struct GDTR
{
DWORD GDTBase; //GDT表的地址
SHORT limit; //GDT表的大小
}
那么,我们如何通过WinDbg
来获取GDT
的地址、大小和GDT
表里的成员呢?首先提一句,段描述符大小为64位。有关其操作见下图:
r gdtr
指令表示读取GDT表的地址;r gdtl
指令表示读取GDT的大小;dq 8003f000 l20
指令表示从0x8003f000
地址(即为GDT表的地址)读取0x20
个64位数据,如果没有l20
,默认0x10
个。这些都是以后常用的指令,需要熟练掌握。
段选择子
段选择子结构简单,那我先介绍它。它是一个16位的描述符,指向了定义该段的段描述符(段描述符比较复杂,后面将会完整介绍)。段选择子结构如下图所示:
它的成员解释如下:
- RPL:请求特权级别,通俗的讲我用什么权限来请求。
- TI:TI=0时,查GDT表;TI=1时,查LDT表。
- Index:处理器将索引值乘以8在加上GDT或者LDT的基地址,就是要加载的段描述符。
是不是很简单?该结构一定要牢记于心,后面将给出练习训练。
段描述符
既然提到段描述符,那我来介绍一下它的结构如下图所示:
段描述符有很多成员,它的成员将会在下面详细介绍,学习的时候一定要按照我介绍的顺序进行学习:
P位
P = 1
段描述符有效,P = 0
段描述符无效。
Base
Base
被分成了三个部分,从图可知:Base
的低16位被放到了段描述符的低四个字节,高16位被均分到段描述符的高四个字节的头和尾。把它们依次拼接起来就是完整的Base
。
Limit
由图可知,把段描述符中所有的Limit拼接起来就只有20位。上一节教程说它有32位的Limit。那就是要看G位
了。
G位
如果G = 0
,说明段描述符中的Limit的单位是字节,段长度Limit
范围可从1B~1MB,即在20位的前面补3个0即可;如果G = 1
,说明段描述符中的Limit的单位是字节为4KB,即段长度Limit范围可从4KB~4GB,在20位的后面补充FFF
即可。举个例子,如果Limit拼接后的为FFFFF
,如果G为0则为000FFFFF
,反之为FFFFFFF
。
S位
S = 1
代码段或者数据段描述符,S = 0
系统段描述符。
TYPE域
TYPE域
是比较复杂的成员,它表示的含义受S位
的影响。
当S位为1时
此时段描述符表示的是代码段或者数据段,如下图所示:
对于表格中Type域的属性和含义,如下表格所示:
属性 | 含义 | 属性 | 含义 |
---|---|---|---|
A | 访问位 | E | 向下扩展位 |
R | 可读位 | W | 可写位 |
C | 一致位 |
对于比较特殊的属性,我们将进一步介绍:
C位
C = 1
:一致代码段;C = 0
:非一致代码段。什么是一致代码段,什么是非一致代码段,将在后面的教程进行介绍。
E位
什么是向下拓展位,我们以fs
为例来看一下如下示意图:
左边表示向上拓展,右边是向下拓展。即向上拓展base
到base+limit
之间区域有效,其余无效;向下拓展base
到base+limit
之间的区域无效,其余有效。这个位针对数据段有效。
当S位为0时
此时段描述符表示的是系统段,系统段有很多种,将会在后面的教程进行详细讲解。Type域每一个数值的含义如下图所示:
DB位
DB位
对不同的段具有不同的影响,情况如下:
1️⃣ 对CS段的影响
D = 1
采用32位寻址方式,D = 0
采用16位寻址方式。
2️⃣ 对SS段的影响
D = 1
隐式堆栈访问指令(如:PUSH POP CALL)使用32位堆栈指针寄存器ESP
,D = 0
隐式堆栈访问指令(如:PUSH POP CALL)使用16位堆栈指针寄存器SP
。
3️⃣ 向下拓展的数据段
D = 1
段上线为4GB
,D = 0
段上线为64KB
。至于是什么意思,我们来看下面一张图。
红色表示向下拓展能寻址的范围。可以看出,如果D = 0
,就算原来能寻址4GB
,因为DB位
的限制导致最大范围是64KB
。
DPL
DPL(Descriptor Privilege Level),即描述符特权级别,规定了访问该段所需要的特权级别是什么。如果通俗的理解,就是:如果你想访问我,那么你应该具备什么权限。
AVL
AVL指示是否可供系统软件使用,由操作系统来使用,CPU
并不使用它。
加载段描述符至段寄存器
除了MOV
指令,我们还可以使用LES
、LSS
、LDS
、LFS
、LGS
指令修改寄存器。CS
不能通过上述的指令进行修改,CS
为代码段,CS
的改变会导致EIP
的改变,要改CS
,必须要保证CS
与EIP
同时改,后面会讲解。
给一个代码模板,具体如下,作为本节的一个作业。
char buffer[6];
//自己构造一个段选择子
//自己自行赋值,你构造的的段描述符位置不一样,段选择子就不一样
_asm
{
les ecx,fword ptr ds:[buffer] //高2个字节给es,低四个字节给ecx
}
本节练习
本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。
俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习比较多,请保质保量的完成,不得使用任何拆分工具。
如下是从虚拟机读取的GDT表的前18个段描述符,下面的实验均按照此进行练习。
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 00000000`00000000
8003f050 80008955`22000068 80008955`22680068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
8003f080 80009240`0000ffff 00009200`00000000
1️⃣ 练习读取GDT表的位置和长度,并显示GDT表前48个段描述符。
2️⃣ 在给定的段描述符中,进行拆分练习(至少10个)。
3️⃣ 拆分如下段选择子。
002B 0023 0010 001B 003B
4️⃣ 快速辨别给定段描述符是否可用以及段基址、段长(至少10个)。
5️⃣ 从给定段描述符,请按照下面的要求进行练习(全部):
- 快速找出所有数据段,并分析该段属性:只读、已访问、可读可写、拓展方向
- 快速找出所有代码段,并分析该段属性:只执行、可读可执行、已访问、一致代码
- 快速找出所有系统段,并分析属性
6️⃣ 自行构造段选择子和段描述符,并用加载段描述符至段寄存器
中的代码模板和要求取得成功。如果有时间同样把LSS
、LDS
、LFS
、LGS
的实验也类比做了。
7️⃣ 如何在调试器中快速判断程序在几环权限。
8️⃣ 自学修改GDT表的相关知识,并思考如下问题。
r gdtr
dq 8003f090 00cffb00`0000ffff
r gdtr
8003f090
是GDT表中的一个段描述符的地址,更改后发现并没有更改,请思考为什么会这样。
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15312627.html