
正文
羽夏看Linux内核——段相关入门知识
提示:扫一扫查出行【扫一扫了解最新限行尾号】
复制提示
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但 必须事先通知我 。
前置知识
在开始正式介绍之前,有一些知识需要讲解一下,否则基本就是听天书。但是,有些知识是本教程的前置知识,也就是说,我不会在该教程介绍,但我们会去使用它:
- 程序编写和现代操作系统的基本概念,比如虚拟地址、内存、进程线程等;
- C/C++ 编写以及使用 GCC 编译;
- 8086汇编的编写以及两种语法;
- Make 的使用;
基础知识
下面我们来介绍一些基础知识和硬件的“硬性规定”。
实模式与保护模式
实模式是
Intel 80286
和之后的
8086
兼容
CPU
的操作模式。实模式的特性是一个20位的存储器地址空间,它寻址具有1MB的存储器的能力,可以直接软件访问
BIOS
以及周边硬件,没有硬件支持的分页机制和实时多任务概念。从
80286
开始,所有的
8086 CPU
的开机状态都是实模式。
8086
等早期的
CPU
只有一种操作模式,类似于实模式。
段寄存器
当我们用汇编读写某一个地址时,比如用下面的代码:
mov dword ptr ds:[0x123456], eax
其实我们真正读写的地址是:
ds.base
+
0x123456
。并不是
0x123456
,不过正好的是
ds
段寄存器的基址是
0
而已。
段寄存器有这几个:
ES、CS、SS、DS、FS、GS、LDTR、TR
,它们各有自己特殊的用途。
段寄存器的结构可用下图表示:
段寄存器具有96位,但我们可见的只有16位。我们可以用调试器随意加载一个程序,但由于我是64位系统,无法编译32位程序,也找不到相应的程序,就不给图了。
既然是寄存器了,那就可以进行读写操作,如下将介绍读写段寄存器的操作:
-
Mov指令:
MOV AX,ES,但只能读16位的可见部分;MOV DS,AX写段寄存器,写的是96位。 -
读写
LDTR的指令为:SLDT/LLDT -
读写
TR的指令为:STR/LTR
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表的大小
}
段选择子
段选择子结构简单,那我先介绍它。它是一个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
同时改,后面会讲解。
CPL/RPL/DPL
- CPL:CPU当前的权限级别
- DPL:如果你想访问我,你应该具备什么样的权限(CPL)
- RPL:用什么权限去访问一个段
RPL存在的意义
举个例子,我们本可以用
读写
的权限去打开一个文件,但为了避免出错,有些时候我们使用
只读
的权限去打开。
一致代码段与非一致代码段
对于一致代码段,也称为共享段:
- 特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据
- 特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态还是用户态
对于非一致代码段:
- 只允许同级访问
- 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态
数据段的权限检查
数值上,
CPL
<=
DPL
且
RPL
<=
DPL
。同时满足上述条件才能通过。
代码段的权限检查
下面的比较都是数值上的比较:
-
如果是非一致代码段,要求:
CPL==DPL且RPL<=DPL -
如果是一致代码段,要求:
CPL>=DPL
代码跨段基础
代码跨段本质就是修改CS段寄存器。前面的教程介绍过段寄存器读写,除CS外,其他的段寄存器都可以通过
MOV
/
LES
/
LSS
/
LDS
/
LFS
/
LGS
指令进行修改。但是
CS
为什么不可以直接修改呢?
CS
的改变意味着
EIP
的改变,改变
CS
的同时必须修改
EIP
,故我们无法使用上面的指令来进行修改,这个也是CPU不允许的。
代码间的段间跳转
段间跳转,有2种情况,即要跳转的段是一致代码段还是非一致代码段,它们不同做的权限检查就不同。
同时修改
CS
与
EIP
的指令如下:
JMP FAR
/
CALL FAR
/
RETF
/
INT
/
IRETED
本篇只介绍段间跳转,故只使用
JMP FAR
,即为长跳转。下面我举个示例来进行讲解:
CPU
如何执行这行代码
JMP 0x20:0x004183D7
?
1️⃣ 段选择子拆分
0x20
对应二进制形式:0000 0000 0010 0000
-
解析结果:
- RPL = 0
- TI = 0
- Index = 4
2️⃣ 查表得到段描述符
TI=0
所以查GDT表,
Index=4
找到对应的段描述符。注意四种情况可以跳转:代码段、调用门、TSS任务段、任务门。后面的几种将会在以后的教程详细讲解。
3️⃣ 权限检查
请参考本节的 代码段的权限检查
4️⃣ 加载段描述符
通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中。
5️⃣ 代码执行
CPU
将
CS.Base + Offset
的值写入
EIP
然后跳转到将要执行的
CS:EIP
处的代码,段间跳转结束。
直接对代码段进行
JMP或者CALL的操作,无论目标是一致代码段还是非一致代码段,CPL都不会发生改变。如果要提升CPL的权限,只能通过调用门。
练习与思考
本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。
本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。
俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习不多,请保质保量的完成。
-
为什么
20位的寻址可以达到1MB? - 拆分如下的段描述符:
00000000`00000000 00cf9b00`0000ffff
00cf9300`0000ffff 00cffb00`0000ffff
00cff300`0000ffff 80008b04`200020ab
ffc093df`f0000001 0040f300`00000fff
0000f200`0400ffff 00000000`00000000
80008955`22000068 80008955`22680068
00009302`2f40ffff 0000920b`80003fff
ff0092ff`700003ff 80009a40`0000ffff
80009240`0000ffff 00009200`00000000
- 拆分如下段选择子:
002B 0023 0010 001B 003B
-
快速辨别
问题2给定段描述符是否可用以及段基址、段长(至少10个) - 记住代码段间跳转的执行流程。
下一篇
羽夏看Linux内核——门相关入门知识







