文章目录
0. 发展过程
- 8086-实模式,1M内存,65K段长;
- 80286-保护模式;
- 80386-第一款32位cpu。
保护模式
段寄存器不再存储段基址,而是存储段选择子,不再需要段寄存器左移加偏移。真正的段基址存在描述符高速缓存中。
但80286作为16位cpu,段长度仍然是64K.
80386
1985 intel发布。80386及以后的cpu都是兼容实模式的(包括现在的i9)。加电时,运行在实模式,一番设置之后进入保护模式。
80386后的32位cpu成为x86体系:
- 兼容16位实模式
- os处于保护模式
- 虚拟8086模式,可模拟多个8086执行多任务
我们需要了解x86体系的分段机制和分页机制。
1. 段寄存器
CS/DS/ES/SS + 32位FS/GS.
分段机制涉及特权等级(PL)划分。
每个段寄存器一共96位,分为两部分:
- 16位可见部分,又称为选择子(selector)
- 80位不可见部分,称为描述符高速缓存器:
32位基址+32位段长度+16位属性
。无法通过任何指令来操作它。
可见的选择子用来找到GDT的一个描述符,用这个描述符填充并不可见部分。
1.0 选择子
16位由低到高:
- 2位RPL(Request Priviledge Level);
- 1位TI
- 13位描述符表索引
| 13位index | 1位TI | 2位RPL |
段寄存器80位不可见的描述符高速缓存器的值,来自于描述符表,这个表由CPU维护。选择子的高13位就是这个表的索引。
段寄存器赋值时,会从描述符表的一个描述符读取数据,赋给80位不可见部分。
TI:
- 0,则查找全局描述符表(GDT);
- 1,则查找局部描述符表(LDT)。
windows没有LDT,所以TI始终为0,
GDT是由GDTR找到的。
RPL,Request Priviledge Level. 在CS中,RPL就变成了CPL(当前特权等级),
2. 段描述符
实模式任意内存地址都能执行代码,可以被读写。这非常不安全。CPU为了提供限制/禁止的手段,提出了保护模式。
保护模式实际上就是保护了内存,使得内存中能够被执行代码,能够被读写的地址可以被人为得控制。在保护模式中,系统和用户进程是被隔离开的。用户进程无法修改系统的内存,也无法执行系统的代码.这些也是通过保护模式达成的功能。
保护模式所实现的功能,很大程度上依赖分段机制。
在实模式下,分段机制很简单,没有属性和权限的概念,只是规定了要使用 段基地址x16+段内偏 的寻址方式。
在保护模式下,它兼容实模式下的寻址方式(段基地址x16+段内偏移),并且在这基础之上给一个段增加了段基地址,段的长度,段的属性这三个属性以实现保护模式下的部分功能。
在保护模式下,描述一段的基地址在哪里、段有多长、段有何属性的结构被称之为段描述符.其结构如下所示:
typedef struct Descriptor{
unsigned int base ; // 段基址
unsigned int limit; // 段限长
unsigned short attribute; // 段属性
}
在保护模式下,增加了很多机制,使得段产生了不少种类:
- 数据段
- 代码段
- 系统段
每个段描述既能够描述出一段内存从哪开始,到哪结束,还能描述这个段是什么类型(代码段,数据段,系统段),当然,也能够描述这个段是否可读,是否可写,是否可执行,甚至还能描述这个段的权限是什么,在什么权限下才能使用这一段内存。
段描述符是GDT和LDT的数据结构项,8个字节,3个关键字段:
- 段基地址
- 段限长
- 段属性
需要重点关注的:
- P位,为1则改描述符有效;
- S:描述符类型,应用程序由数据段、代码段,cpu有系统段和门描述符,
- Type:段的属性。type含义由S位决定
- 基地址:3部分,8+8+16;
- 段限长:2部分,16+4(32-12),即1M;
并非所有描述符都描述一个段,门描述符就存储了指向过程入口点的指针,S和Type表明了描述符类型。
其它:
- G位:Granularity,段限长粒度/单位,为1则单位是4KB,0则是字节;
- L位:保留给64位;
- AVL:os使用;
- DPL:描述本段内存所需权限。
- D/B位:操作大小,0则是16位,1是32位。
D/B位的会作用到代码段( CS 段寄存器),栈段( SS 段寄存器),数据段( DS , ES 段寄存器),当使用这些段寄存器时,将会受到不同的影响:
- 寻址模式
- esp使用
2.0 S+Type
S为1,则type描述代码段或数据段。S为0,则是系统段。
type最高位,位于段的0xB位,后面就简称Type.11。
数据段描述符
S==1 && Type.11==0
则该段是数据段。
S | Type |
| 0| E| W| A|
E是扩展方向:
- 0,向下扩展,也叫向外扩展,通常是栈段;
- 1,向上扩展,也叫向内扩展。
向上扩展,就是指[Base, Base + Limit]
为有效范围。
windows只有向上扩展。
W/A就是可写/是否已经访问。
D/B标志位此时被称为B位。
如果是栈段,则
- 0,默认使用16位的 SP 寄存器操作栈,段最大64K;
- 1,默认使用32位的 ESP 寄存器操作栈,段最大4GB.
如果是向下展开的数据段,CPU虽然提供了这个机制,但是操作系统并没有使用这个机制。
代码段描述符
S==1 && Type.11==1
则该段是代码段。
S | Type |
| 1| C| R| A|
C就是Consistency,一致性。
- 0,非一致性代码段,这样的代码段可被同级代码使用,或通过门调用;
- 1, 一致性代码段,R3可以执行该段(R0),但R3部分保持自己的特权等级。
R:
- 0,不可读,只能执行;
- 1,可读可执行;
- 保护模式下,代码段不可写。
D/B标志位此时被称为 D 位,这个位会影响指令和操作数的寻址模式:
- 0,指令默认的寻址模式是16位,操作默认大小为16位或8位。
- 1,指令默认的寻址模式是32位,操作数默认为32位或8位;
opcode前缀67H可以用来切换寻址模式。
例如,当前 D ==1时,寻址模式是32位,切换之后,寻址模式就变成16位模式
opcode前缀 66H 可以用来切换操作数大小。
例如,当前 D==0 时,操作数大小默认为32,切换之后,操作数大小为16位.
系统段描述符
s==0
则该段是系统段。
系统段中的描述符类型一般是门描述符。后面详细整理。
2.1 赋值原理
先看一句32位指令:
mov eax , dword ptr ds:[0x403000]
如果在16位的实模式下,一般就是 ds*16+0x403000,但是在32位的保护模
式下,16位的段寄存器并不能存储一个64位的段描述符。
系统的众多描述符被统一打包存储在内存中,形成的一个数组被称为全局描述符表GDT,由GDTR寄存器找到。
mov ax,2Bh
mov ds,ax
这条指令可看成将 0x2B 赋值给ds,实际不是.
按照13:1:2的格式二进制展开0x2B:
0000000000101 0 11
含义:请求权限为最低的3,GDT中第5个段描述符。
cpu这时会做权限检查,通过检查后mov ds,ax
,将GDT[5]段描述符存储在段寄存器隐藏部分,将段选择子存储到16位可见部分。
上面的检查其实就是一个公式:max(CPL,RPL) <= DPL
2.2 段权限检查
3个概念:
- CPL,CS段低两位(RPL);
- RPL,段寄存器加载时的段选择子中,描述以什么权限访问目标;
- DPL,Descriptor…, 目标GDT段描述符中的2位(B13-14),描述了访问本段所需的最低权限。
1.4.0 数据段权限检查
mov ds,ax
,ax(0x2B)作为段选择子,低两位作为RPL,高13位(0x05)作为索引获取GDT[5],找到DPL。
满足max(CPL,RPL) <= DPL
即通过检查。
也就是说,目标段权限要最小。
1.4.1 代码段权限检查
仅限16位。
jmp/call/ret段内跳转不会检查,jmp far这样的段间跳转才会。
在汇编中,有一些指令是可以跨段跳转的,例如:
jmp 33:401000
call 33:401000
这两条指令在执行后,会将操作数中的段选择子对应的段描述符加载到 CS 中:
s==1 && Type.11 == 1
则代表请求代码段;- Type.c==0,非一致代码段,则要求
CPL==DPL
;一致代码段,则要求CPL>=DPL
。
//段选择子的结构:
// [描述符下标:13 | T1:1 | RPL:2]
unsigned short segSel = 0x33;// 0x33就是要切换的段选择子,在指令jmp 33:401000 中给
unsigned int RPL = segSel & 0x11; // 取段选择自的低2位作为RPL
unsigned int CPL = CS & 0x11 ; // 取`CS`段寄存器的值的低2位作为CPL
if( SegDes.S == 1 && SegDes.Type & 0x1000 ){
if(SegDes.Type.E == 1){ // 一致代码段
if( CPL >= segDes.DPL){
CS = segSel; // 可以切换。
}else{
throw "异常";
}
}else{ // 非一致代码段
if(CPL == SegDes.DPL && RPL <= SegDes.DPL){
CS = segSel;
}else{
throw "异常";
}
}
}else{
throw "异常";
}
2.2 其它检查
有效位检查
p为0则触发异常。
段类型检查
加载段选择符到段寄存器:
- cs只能存放可执行段选择符
- ss存放可写数据段选择符
访问段时:
- 代码段不可写
- 可写位没有设置的数据段不可写
- 可读位没有设置的数据段不可读
3. 系统段描述符-门描述符
当段描述符中的 S 位等于0,表示这个描述符是一个门描述符。门描述符一般用于从3环进入到0环,并能够将3环权限切换成0环权限。
很多时候,用户层的代码需要切换到内核层执行代码。因为有些代码执行时需要用到0环权限。切换0环权限实际就是将CS段寄存器的CPL改成0(也就是0环权限)。
但在3环时, CS 的 CPL是3,是无法直接修改的。如果要修改,就需要切换段选择子,切换段选择子,就需要判断MAX(CPL , RPL)<= DPL
。
门描述符的种类有:
- 调用门,type == 1100B(12),Windows操作系统没有使用此机制
- 中断门,type == 1110B(14),IDT表中的中断处理函数就是这种门描述符
- 陷阱门,type == 1111B(15),IDT表中的陷阱处理函数就是这种门描述符
- 任务门,type == 0101B(5),用于任务切换
3.0 调用门
调用门的出现是为了便于在不同的权限直接切换.
一个调用门中,保存了以下信息:
- 要执行的函数的地址
- 要执行的函数的参数个数(每个参数应当是4字节)
- 段选择子(这个段选择子用于切换权限),使用调用门时,这个选择子就是被切换成 CS 的选择子
如果发生了权限切换,系统也会将用户栈切换成内核栈,也就是权限切换时,CS 段寄存器的值会被改掉(不改掉就切换不了切换段描述符),还会将 SS 段选择子,切换为内核的段选择子,将 ESP 切换成内核的 ESP ,切换前, SS , ESP 的值都会被保存。在切换回来之后,才进行还原。
其实就是切换cs,ss,esp.
因为栈要进行切换,当函数被调用时,用户栈中的函数参数会被拷贝到内核栈因此需要在调用门描述符中指定参数的个数是多少,否则系统将无法为函数拷贝参数到内核栈中.
设置和使用
因为在Windows中没有使用调用门, 因此, 在GDT表中是没有调用门描述符的.
想要试验一个调用门,就需要自己使用 windbg开启双击调试,并自行在GDT中设置一个.
具体步骤看最后面编程。
3.1 任务门、中断门和陷阱门
除了GDT,IDT也存储了三种门描述符:
- 中断门-可屏蔽中断
- 陷阱门-异常
- 任务门-不可屏蔽中断
中断门和陷阱门的描述符其实和调用门一模一样.
产生中断或异常后:
- CPU会使用中断号找到 IDT 表中的中断描述符/陷阱描述符,
- 取出描述符后,得到门描述符中的段选择子.
- 通过此段选择子找到 GDT 表中的段描述符,
- 从GDT表中取出的段描述符中得到段基地址
- 使用段基地址 + 门描述符 中的函数偏移,得到函数地址.
- 调用该函数.
4. 其它寄存器
4.0. 调试寄存器
DR0-DR7
- DR-DR3,用于保存断点的线性地址.
- DR7,分别保存4个断点的中断条件.
- DR6,保存断点命中后的信息.
4.1. 控制寄存器
CR0-4,CR1保留,CR2专门用于保存缺页异常时的线性地址。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qRV4mSMx-1576737214295)(http://r.photo.store.qq.com/psb?/V11kR0B91ocBaQ/OiaJ*5kDaCmYCrQ5vJ9dqaSQNSI9bw90rfXkmDDvbFA!/r/dFMBAAAAAAAA)]
CR0
包含处理器的大量控制标志位。下面列出常用的三种:
- PE,是否启用保护模式,置1则启用
- PG,是否使用分页模式,置1则开启分页模式,此标志置1时,PE标志也必须置1,否则CPU报异常.
- WP,WP为1时,不能修改只读的内存页;WP为0 时,可以修改只读的内存页。
PE==1 && PG==1
则cpu功能工作在开启分页机制的保护模式下,也就是我们使用的系统。
不存在这两种情况:
PE==1 && PG==0
,不开启分页的保护模式;PE==0 && PG==1
,无保护的分页模式
CR3
专门用于保存一个进程的页目录地址(此寄存器存储的是物理地址)
此寄存器的低12位一般都设置为0,因为 CR3 寄存器所保存的地址是一个页目录的地址,该地址必须在一个以分页粒度对其的地址上。
- 在使用非 PAE 模式下的分页结构时,指向 PDT 基地址,线性地址是 10‐10‐12 格
式 - 使用 PAE 模式的分页结构时,指向
PDPT
基地址,线性地址是2‐9‐9‐12
格式
CR4
提供了一些扩展的控制功能.
- PAE==1,表示使用物理地址扩展模式;
- PAE==0,表示不使用物理地址扩展模式。
4.3. GDTR,IDTR寄存器
GDTR寄存器是48位的,高32位保存GDT表的内存地址,低16位保存的是GDT表的大
小.
LGDT:fword [地址] ; 将内存地址中存储的48位数据加载到GDTR寄存器总.高32位表示GDT表基地
SGDT:fword [地址] ; 将GDTR寄存器中的值存入到内存地址中.高32位表示GDT表基地址,低16位
4.4 MSR寄存器
全称:特殊模组寄存器组
这组寄存器它们没有名字,只有编号,它们保存了大量的信息,这些信息可以通过两条指令和一个编号来读写:
RDMSR
,读特殊寄存器的值,指令默认使用 ecx 寄存器来保存要读取的 MSR 寄存器的编号,使用edx:eax来保存读取结果。WRMSR
,写特殊寄存器,指令默认使用 ecx 寄存器来保存要写入的 MSR 寄存器的编号,使用edx:eax来保存要写入的值。
这两个指令R0才能执行。
常见 MSR 编号见Intel手册第三卷第35章.