2

羽夏看Linux内核——段相关入门知识 - 寂静的羽夏

 1 year ago
source link: https://www.cnblogs.com/wingsummer/p/16551579.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

羽夏看Linux内核——段相关入门知识

  此系列是本人一个字一个字码出来的,包括示例和实验截图。如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏看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,它们各有自己特殊的用途。
  段寄存器的结构可用下图表示:

o_2109181304413-1.png

  段寄存器具有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分级示意图如下:

o_2109200928364-8.png

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位的描述符,指向了定义该段的段描述符(段描述符比较复杂,后面将会完整介绍)。段选择子结构如下图所示:

o_2109200226344-2.png

  它的成员解释如下:

  • RPL:请求特权级别,通俗的讲我用什么权限来请求。
  • TI:TI=0时,查GDT表;TI=1时,查LDT表。
  • Index:处理器将索引值乘以8在加上GDT或者LDT的基地址,就是要加载的段描述符。

  既然提到段描述符,那我来介绍一下它的结构如下图所示:

o_2109191244474-1.png

  段描述符有很多成员,它的成员将会在下面详细介绍,学习的时候一定要按照我介绍的顺序进行学习:

  P = 1段描述符有效,P = 0段描述符无效。

  Base被分成了三个部分,从图可知:Base的低16位被放到了段描述符的低四个字节,高16位被均分到段描述符的高四个字节的头和尾。把它们依次拼接起来就是完整的Base

Limit

  由图可知,把段描述符中所有的Limit拼接起来就只有20位。上一节教程说它有32位的Limit。那就是要看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 = 1代码段或者数据段描述符,S = 0系统段描述符。

TYPE域

  TYPE域是比较复杂的成员,它表示的含义受S位的影响。

  • 当S位为1时

  此时段描述符表示的是代码段或者数据段,如下图所示:

o_2109200324584--4.png

  对于表格中Type域的属性和含义,如下表格所示:

属性 含义 属性 含义
A 访问位 E 向下扩展位
R 可读位 W 可写位
C 一致位

  对于比较特殊的属性,我们将进一步介绍:

  C = 1:一致代码段;C = 0:非一致代码段。什么是一致代码段,什么是非一致代码段,将在后面的教程进行介绍。

  什么是向下拓展位,我们以fs为例来看一下如下示意图:

o_2109200327104-5.png

  左边表示向上拓展,右边是向下拓展。即向上拓展basebase+limit之间区域有效,其余无效;向下拓展basebase+limit之间的区域无效,其余有效。这个位针对数据段有效。

  • 当S位为0时

  此时段描述符表示的是系统段,系统段有很多种,将会在后面的教程进行详细讲解。Type域每一个数值的含义如下图所示:

o_2109200325124-6.png

  DB位对不同的段具有不同的影响,情况如下:

1️⃣ 对CS段的影响
  D = 1采用32位寻址方式,D = 0采用16位寻址方式。

2️⃣ 对SS段的影响
  D = 1隐式堆栈访问指令(如:PUSH POP CALL)使用32位堆栈指针寄存器ESPD = 0隐式堆栈访问指令(如:PUSH POP CALL)使用16位堆栈指针寄存器SP

3️⃣ 向下拓展的数据段
  D = 1段上线为4GBD = 0段上线为64KB。至于是什么意思,我们来看下面一张图。

o_2109200412524-7.png

  红色表示向下拓展能寻址的范围。可以看出,如果D = 0,就算原来能寻址4GB,因为DB位的限制导致最大范围是64KB

  DPL(Descriptor Privilege Level),即描述符特权级别,规定了访问该段所需要的特权级别是什么。如果通俗的理解,就是:如果你想访问我,那么你应该具备什么权限。

  AVL指示是否可供系统软件使用,由操作系统来使用,CPU并不使用它。

加载段描述符至段寄存器

  除了MOV指令,我们还可以使用LESLSSLDSLFSLGS指令修改寄存器。CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要改CS,必须要保证CSEIP同时改,后面会讲解。

CPL/RPL/DPL

  • CPL:CPU当前的权限级别
  • DPL:如果你想访问我,你应该具备什么样的权限(CPL)
  • RPL:用什么权限去访问一个段

RPL存在的意义

  举个例子,我们本可以用读写的权限去打开一个文件,但为了避免出错,有些时候我们使用只读的权限去打开。

一致代码段与非一致代码段

对于一致代码段,也称为共享段:

  • 特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据
  • 特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态还是用户态

对于非一致代码段:

  • 只允许同级访问
  • 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态

数据段的权限检查

  数值上,CPL<=DPLRPL<=DPL。同时满足上述条件才能通过。

代码段的权限检查

  下面的比较都是数值上的比较:

  • 如果是非一致代码段,要求:CPL==DPLRPL<=DPL
  • 如果是一致代码段,要求:CPL>=DPL

代码跨段基础

  代码跨段本质就是修改CS段寄存器。前面的教程介绍过段寄存器读写,除CS外,其他的段寄存器都可以通过MOV/LES/LSS/LDS/LFS/LGS指令进行修改。但是CS为什么不可以直接修改呢?CS的改变意味着EIP的改变,改变CS的同时必须修改EIP,故我们无法使用上面的指令来进行修改,这个也是CPU不允许的。

代码间的段间跳转

  段间跳转,有2种情况,即要跳转的段是一致代码段还是非一致代码段,它们不同做的权限检查就不同。
  同时修改CSEIP的指令如下:JMP FAR/CALL FAR/RETF/INT/IRETED

  本篇只介绍段间跳转,故只使用JMP FAR,即为长跳转。下面我举个示例来进行讲解:

CPU如何执行这行代码JMP 0x20:0x004183D7

1️⃣ 段选择子拆分
  0x20对应二进制形式:0000 0000 0010 0000

  • 解析结果:
    • RPL = 0
    • Index = 4

2️⃣ 查表得到段描述符

  TI=0 所以查GDT表,Index=4找到对应的段描述符。注意四种情况可以跳转:代码段、调用门、TSS任务段、任务门。后面的几种将会在以后的教程详细讲解。

3️⃣ 权限检查

  请参考本节的代码段的权限检查

4️⃣ 加载段描述符
  通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中。

5️⃣ 代码执行
  CPUCS.Base + Offset的值写入EIP然后跳转到将要执行的CS:EIP处的代码,段间跳转结束。

直接对代码段进行JMP或者CALL的操作,无论目标是一致代码段还是非一致代码段,CPL都不会发生改变。如果要提升CPL的权限,只能通过调用门。

练习与思考

本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。

  俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习不多,请保质保量的完成。

  1. 为什么20位的寻址可以达到1MB
  2. 拆分如下的段描述符:
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
  1. 拆分如下段选择子:
002B 0023 0010 001B 003B
  1. 快速辨别问题2给定段描述符是否可用以及段基址、段长(至少10个)
  2. 记住代码段间跳转的执行流程。

  羽夏看Linux内核——门相关入门知识


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK