保护模式(一)--段机制

c++

Posted by YiMiTuMi on March 24, 2021

保护模式–段机制

保护模式主要保护内存靠段和页的机制,段保护简单点说就是一个寄存器去访问一个虚拟地址跨段之间的保护,页保护是通过虚拟地址去查找物理地址时,对当前地址的读写等保护。

在X86处理器中地址分段模式包括一下2种形式:

1)平坦模式:在平坦模式下,系统能访问一个连续的、不分段的地址空间,所有的段被映射到同一个地址空间(虚拟的),所有的段都有相同的基地址0,界限为4GB(32位)。我们所使用的操作系统,无论是 windows 还是 Linux 都运行在这种模式下。

2)多段模式:不同的段,如代码段、数据段、堆栈段等位于不同的段中。实际上Windwos 和 Linux 都使用了平坦模式。

GDT和LDT

1) 全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。

基地址指定GDT表中字节0在线性地址空间中的地址,表长度指明GDT表的字节长度值。指令LGDT和SGDT分别用于加载和保存GDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。在保护模式初始化过程中必须给GDTR加载一个新值。

2) 局部描述符表LDT(Local Descriptor Table)局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。

LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。LDTR记录局部描述符表的起始位置,与GDTR不同,LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。

GDT表段描述符:

高16位:

Base(31:24) 31:24

G    23 

D/B  22
 
0    21

AVL  20

SegLimit 19:16

P    15 

DPL  14:13

S    12

Type 11:8

Base(23:16) 7:0

低16位:

BaseAddress(15:00) 31:16

SegmentLimit(15:00) 15:0

注意:GDT表第一个元素为NULL。

1.段寄存器具有96位,我们只能看到16位

1) Selector //16位 不在段描述符里
2) Atrribute //16位 8:23 高四字节
3) Base     //32位 Base 31:24 + Base 23:16 + Base 15:00
4) Limit    //32位 segLimit 19:16 + segmengt 16 = 20位 

2.Gs调试会进入内核,一但进入内核数据就会被清空。

3.CUP 两个表:GDT 和 IDT

4.段描述符(64位) P位为1为有效,0为无效

5.G位决定Limit的大小,G位为1Limit FFFFFFFF,0为Limit 000FFFFF

6.描述符分为:系统段描述符或者数据段、代码段描述符

7.S位为1 数据段、代码段描述符,S位为0 系统段描述符

8.TYPE域(4位)(显示数据段、代码段、系统段的属性)

9.P位:有效位。

系统段:

可以知道是啥。

数据段:

4 最高位为0为数据段

3 E:扩展方向,1为向下拓展,0向上

2 W:是否可写

1 A:访问位,表示该位最后一次被操作系统清零后,该段是否被访问过,每当处理器将该段选择符置入某个寄存器时,就将该位置1.

代码段:

4 最高位为1为代码段 

3 C:

2 R:是否可读

1 A:访问位,表示该位最后一次被操作系统清零后,该段是否被访问过,每当处理器将该段选择符置入某个寄存器时,就将该位置1.

9.D/B位 (兼容保护模式和实模式)

1) 对CS段(代码段)的影响

D = 1 采用32位寻址方式

D = 0 采用16位寻址方式

前缀67指令 改变寻址方式 (不会改变D/B位的值)

2) 对SS段的影响

D = 1 隐士堆栈访问指令(如: PUSH POP CALL)使用32位堆栈指针寄存器ESP

D = 0 隐士堆栈访问指令(如: PUSH POP CALL)使用16位堆栈指针寄存器SP

3) 向下拓展的数据段

D = 1段上限为4GB  (大段)

D = 0段上限为64KB (小段)

段选择子(16位)根据段的选择子去GDT表查找段的描述符:

段寄存器具有96位,我们只能看到16位,这16位就是选择子:

Index 15:3 --索引(处理器将索引值乘以8在加上GDT或者LDT的基地址,就是要加载的段描述符)

TI 2 -- TI = 0:查GDT表 TI = 1 查LDT表

RPL 1:0 -- 请求特权级别(数字越小级别越高) 

数据段权限检查

DPL:描述符特权

RPL:请求特权级

CPL:当前任务特权

CPL(代码执行时的权限,CS,SS) <= DPL 并且 RPL <= DPL (数值上的比较)

mov ax, 000B
//000B的DPL,000B的RPL和CS的CPL 要满足条件

一致代码段和非一致代码段

一致代码段:

简单理解,就是操作系统拿出来被共享的代码段,可以被低特权级的用户直接调用访问的代码。

通常这些共享代码,是”不访问”受保护的资源和某些类型异常处理。比如一些数学计算函数库,为纯粹的数学运算计算,被作为一致代码段。

一致代码段的限制作用:

1.特权级高的程序不允许访问特权级低的数据:核心态不允许调用用户态的数据。

2.特权级低的程序可以访问到特权级高的数据.但是特权级不会改变:用户态还是用户态。

非一致代码段:

为了避免低特权级的访问而被操作系统保护起来的系统代码。

非一致代码段的限制作用:

1.只允许同级间访问。

2.绝对禁止不同级访问:核心态不用用户态.用户态也不使用核心态。

代码间的跳转

段间跳转,有2种情况,即要跳转的段是一致代码段还是非一致代码段。

CS.base + eip = 真正的地址

同时修改CS与EIP的指令:(跨段)

JMP FAR/CALL FAR/RETF/INT/IRETED

只改变EIP的指令:

JMP/CALL/JCC/RET

JMP FAR流程(长跳转):

JMP 0x20:0x004183D7 (长跳转,需要查表)

1)段选择子拆分

0x20对应二进制: 0000 0000 0010 0000

RPL = 0
TI = 0
Index = 4 

2)查表得到段描述符

TI = 0 所以查找GDT表

Index = 4 找到对应的段描述符

四种情况可以跳转:代码段、调用门、TSS任务段、任务门

3)权限检查

如果是非一致代码段,要求:CPL == DPL 并且 RPL <= DPL

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

4)加载段描述符

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

5)执行代码

CPU将 CS.Base(段描述符里的Base) + Offset 的值写入EIP然后执行CS:EIP处的代码,段间跳转结束。 

调用门

指令格式:

CALL CS:EIP(EIP废弃)

执行步骤:

1)根据CS的值查GDT表找到段描述符,这个段描述符是一个调用门。调用门里Segment Selector是一个段选择子,当前段选择子对应的段描述符就是要替换的CS的值。

2)在调用门描述符中存储另一个代码段段的选择子。

3)选择子指向的段 段.Base + Offset(门描述符里的2个Offset的和) 就是真正要执行的地址。

门描述符(64位):

高32位:

Offset in Segment(31:16) : 31:16

P : 15

DPL : 13:14 

0 : 12

Type: 8:11(1100) 调用门

000 : 7:5

ParamCount 0:4

低32位:

Segment Selector: 16:31 (代码段的段选择子,即CS索要替换的段地址,当前地址可以指向0环或者3环,0环即提权,3环不提权)(查GDT)

Offset in Segment(15:00) : 0:15

CALL FAR流程(长调用):

指令格式:

CALL CS:EIP (CALL指令后面跟6个字节其中,前两个选择子,后面EIP是废弃的)

CALL跳转的地址由CS(段描述符)决定和EIP没有关系。

跨段不提权:

CPL = 3

1)CALL先push一个之前的CS

2)PUSH返回地址,当前指令的下一条指令

3)然后通过和JMP一样的检查后,赋值给CS。

用Retf返回。

跨段并提权(调用门):

CPL = 0

CS和SS 权限必须一样,所以SS、ESP、EIP也要改变

CS和EIP从段描述符中获取。

SS和ESP从TSS中获取。

EBP的值不会变化。

调用提权CALL:(如果存在参数还会push参数)

1)PUSH调用者的SS

2)PUSH调用者的ESP

3)PUSH调用者的CS

4)PUSH返回地址,当前指令的下一条指令

4)然后通过和JMP一样的检查后

5)CS和EIP从段描述符中获取,后赋值。

6)SS和ESP从TSS中获取后,赋值

IDT表

IDT:中断描述表,和GDT一样,IDT也是有一系列描述符组成的,每个描述符占8个字节,注意IDT表的第一个元素不是NULL。

IDT包含: 任务门描述符、中断门描述符、陷阱门描述符

windbg中查看IDT表的基址和长度:

r idtr

段描述符

高32位:

Offset(31:16) : 16:31 //偏移
 
P: 15

DPL: 13:14

0D110(s、Type): 8:12

000: 5:7

0000: 0:4

低32位:

Segment Selector : 16:31 //要找的段选择子(查GDT)

Offset(15:0): 0:15 //偏移

中断门

INT指令触发中断门,INT后面跟的值为IDT表中的索引。

P = 1
S = 0
Type = 1110(E)

中断返回:

INT N 指令:

1.在没有权限切换时,会向堆栈 PUSH 3个值,分别为:

CS EFLAG EIP(返回地址)

2.在有权限切换时,会向堆栈 PUSH 5个值,分别为:

SS ESP EFLAG CS EIP(返回地址)

在中断门中,不能通过RETF返回,而应该通过IRET/IRETD指令返回,不能带参数。

调用门和中断门的区别:

1)调用门通过CALL FAR指令执行,但中断门通过INT指令

2)调用门查询GDT表,中断门查IDT表

3)CALL CS:EIP 中的CS是段选择子,由3部分组成,但INT N指令中的N只是索引,中断门不检查RPL,只检查CPL

4)调用门可以有参数,但中断门没有参数。

陷阱门:(一般不用)

P = 1
S = 0
Type = 1111(F)

TSS结构(任务状态段,task state segment)

TSS,104字节。

kd> dt _KTSS
nt!_KTSS
   +0x000 Backlink         : Uint2B
   +0x002 Reserved0        : Uint2B
   +0x004 Esp0             : Uint4B
   +0x008 Ss0              : Uint2B
   +0x00a Reserved1        : Uint2B
   +0x00c NotUsed1         : [4] Uint4B
   +0x01c CR3              : Uint4B
   +0x020 Eip              : Uint4B
   +0x024 EFlags           : Uint4B
   +0x028 Eax              : Uint4B
   +0x02c Ecx              : Uint4B
   +0x030 Edx              : Uint4B
   +0x034 Ebx              : Uint4B
   +0x038 Esp              : Uint4B
   +0x03c Ebp              : Uint4B
   +0x040 Esi              : Uint4B
   +0x044 Edi              : Uint4B
   +0x048 Es               : Uint2B
   +0x04a Reserved2        : Uint2B
   +0x04c Cs               : Uint2B
   +0x04e Reserved3        : Uint2B
   +0x050 Ss               : Uint2B
   +0x052 Reserved4        : Uint2B
   +0x054 Ds               : Uint2B
   +0x056 Reserved5        : Uint2B
   +0x058 Fs               : Uint2B
   +0x05a Reserved6        : Uint2B
   +0x05c Gs               : Uint2B
   +0x05e Reserved7        : Uint2B
   +0x060 LDT              : Uint2B
   +0x062 Reserved8        : Uint2B
   +0x064 Flags            : Uint2B
   +0x066 IoMapBase        : Uint2B
   +0x068 IoMaps           : [1] _KiIoAccessMap
   +0x208c IntDirectionMap  : [32] UChar

TSS主要作用是替换所有的寄存器。(切换任务的时候,会需要替换所有。)

TSS是一块内存,大小104字节,并不在CPU上。

一个CPU只有一个TSS。

CPU通过TR段寄存器找到TSS,TR寄存器Beas记录TSS在内存中开始的位置,Limit保存大小,TR是一个段寄存器来自GDT表,里面存的是TSS,所以说TR的段描述符和其他段的描述符差不多。

WINDOWS只是用了TSS中的ESP和SS。

TR寄存器读写

当用到或者替换TSS时用到TR寄存器去查找TSS。通过TR段选择子找到的段描述符称为TSS描述符。

1)将TSS段描述加载到TR寄存器

指令:LTR (特权指令)

说明:

用LTR指令去装载的话,仅仅是改变TR寄存器的值,并没有真正改变TSS。

LTR指令只能在系统层使用

加载后TSS段描述符也会状态位发生变化

2)读TR寄存器

指令:STR

说明:

如果用STR去读的话,只读了TR的16位也就是选择子。

修改TR寄存器:

1) 在Ring0 我们可以通过LTR指令去修改TR寄存器

2) 在Ring3 我们可以通过CALL FAR 或者 JMP FAR指令去修改TR寄存器

用JMP去访问一个代码段的时候,改变的时CS和EIP:

JMP 0x48:0x123456 

如果0x48是代码段,执行后:

CS -> 0x48 EIP -> x0123456

用JMP去访问一个任务段的时候:

如果0x48是TSS段描述符,先修改TR寄存器,再用TR.Base指向的TSS中的值修改当前的寄存器。

通过JMP切换任务时,TSS中的PreviousTaskLink不会发生变化,EFLAG中NT位不变。

通过CLL切换任务时,TSS中的PreviousTaskLink会添加原来段的段选择子,EFLAGS中NT位置1。

NT位为0时,IRET返回值从堆栈中获取,即中断返回。

NT位为1时,IRET从TSS中PreviousTaskLink返回。

任务门:

任务门是为异常(INT)提供了可切换任务的机制,是一种被动的机制,而单纯的任务段必须主动调用CALL、JMP。

GDT和LDT:

即 LDTSegmentSelector 保存在LDTR段寄存器中,是这个段寄存器的选择子,指出LDT表的位置。

			      	GDT表(全局一个)
					  ........
					  ........
	selector1   ->  LDT Desctiptor1  ->   LDT1

					other Desctiptor1 

	selector2   ->  LDT Desctiptor1  ->   LDT2     <-  LDTR (此时LDTR中是选择子selector2)
					
					  ........
					  ........

	selector3   ->  LDT Desctiptor3  ->   LDT3	

总结:

对于一致代码段(共享的段):

1)特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据。

2)特权级低的程序可以访问到特权高的数据,但特权级不会改变:用户态还是用户态

普通代码段(非一致代码段):

1)只允许同级访问

2)绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态

跨段调用时,一旦有权限切换,就会切换堆栈。

CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样。

JMP FAR只能跳到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL的权限。

任务段:

在调用门、中断门与陷阱门中,一旦出现权限切换,那么就会有堆栈的切换。而且,由于CS的CPL发生变化,也导致了SS也必须要切换,切换时,会有新的ESP和SS(CS是由中断门或者调用门指定),这两个值从TSS中获取,即任务状态段。

矢车菊 – 幸福