保护模式介绍
什么是保护模式?
x86 CPU的3个模式:实模式、保护模式和虚拟8086模式。
为什么不直接讲x64的?
x86是由Intel推出的一种复杂指令集,能够生产支持这种指令集CPU公司主要是Intel和AMD.
AMD在1999年的时候,拓展了这套指令集,称为x86-64,后改名为AMD64,Intel也兼容了这个产品,称为Intel 64.但AMD64和Intel64几乎是一样的,所以在很多资料中统称为x64.这套指令集是对x86的拓展,向下兼容的.
保护模式有什么特点?
段的机制
页的机制
学习保护模式有什么用?
真正理解内核是如何运作的
段寄存器结构
1、什么是段寄存器?
当我们用汇编读写某一个地址时:
1 | mov dword ptr ds:[0x123456] |
eax我们真正读写的地址是:
1 | ds.base + 0x123456 |
ES CS SS DS FS GS LDTR TR 共8个
2、段寄存器的结构
3、段寄存器的读写
3.1 读段寄存器
1 | 比如:MOV AX,ES 只能读16位的可见部分 |
3.2 写段寄存器
1 | 比如:MOV DS,AX 写时是写96位 |
段寄存器属性探测
1、段寄存器成员简介
段寄存器 | Selector | Attribute | Base | Limit |
---|---|---|---|---|
ES | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
CS | 001B | 可读、可执行 | 0 | 0xFFFFFFFF |
SS | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
DS | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
FS | 003B | 可读、可写 | 0x7FFDE000 | 0xFFF |
GS | - | - | - | - |
2、探测Attribute
1 | int var = 0; |
3、探测Base
1 | int var = 1; |
4、探测Limit
1 | int var = 1; |
段描述符与段选择子
段描述符分类
GDT(全局描述符表)
LDT(局部描述符表)
当我们执行类似MOV DS,AX指令时,CPU会查表,根据AX的值来决定查找GDT还是LDT,查找表的什么位置,查出多少数据.
段描述符
段选择子
段选择子是一个16位的段描述符,该描述符指向了定义该段的段描述符.
RPL:请求特权级别
TI:
1 | TI=0 查GDT表 |
Index:处理器将索引值乘以8在加上GDT或者LDT的Base地址,就是要加载的段描述符
加载段描述符至段寄存器
除了MOV指令,我们还可以使用LES、LSS、LDS、LFS、LGS指令修改寄存器
CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要改CS,必须要保证CS与EIP一起改,后面会讲
1 | char buffer[6]; |
[!NOTE]
注意:RPL<=DPL(在数值上)
段描述符属性
P位和G位
1、P位
1 | P = 1 段描述符有效 |
2、G位(当前寻址上限)
G = 0 Limit单位是bytes
1 | Limit = 000 FFFFF 前面加000 |
G = 1 Limit单位是以4KB
1 | Limit = FFFFF FFF 后面加FFF |
S位和TYPE域
1、S位
S = 1 代码段或者数据段描述符
S = 0 系统段描述符
2、TYPE域
分析举例
数据段(DS)说明
Access 访问位,表示该位最后一次被操作系统清零后,该段是否被访问过.每当处理器将该段选择符置入某个段寄存器时,就将该位置1.
Write 是否可写
E 扩展方向 (向上拓展红色部分图一,向下拓展红色部分图二)
代码段(CS)说明
A 访问位
R 可读位
C 一致位
1 | C = 1 一致代码段 |
系统段(SS)描述符
当S=0时,该段描述符为系统描述符.系统描述符有分为以下类型
DB位
DB位:当前实际地址最多大小
1、CS段
1 | D = 1 采用32位寻址方式 |
整个地址空间是4KB
1 | D = 0 采用16位寻址方式 |
整个地址空间是64K
2、SS段
1 | D = 1 隐式堆栈访问指令(如:PUSH POP CALL) 使用32位堆栈指针寄存器ESP |
3、向下拓展的数据段
1 | D = 1 段上线为4GB |
1 | D = 0 段上线为64KB |
总结
- 段描述符中的DB位控制操作数的默认大小,DB=0时默认操作数为16位,DB=1时默认操作数为32位。
- 寄存器宽度的改变通过改变DB位实现,不影响寄存器本身的宽度。
- CS段描述符的DB位控制代码段的默认操作数大小,影响push指令的默认操作数。
- SS段描述符的DB位改变堆栈寄存器的寻址方式,从32位变为16位
段权限检查
CPU分级
如何查看程序处于几环?
CPL(Current Privilege Level) :当前特权级
CS和SS中存储的段选择子后2位
例如:23 == 0010 0011 2b == 0010 1011 11==3 那么在三环
DPL(Descriptor Privilege Level) 描述符特权级别
DPL存储在段描述符中,规定了访问该段所需要的特权级别是什么.
通俗的理解:如果你想访问我,那么你应该具备什么特权.
举例说明:
1 | mov DS,AX 如果AX指向的段DPL = 0 |
RPL(Request Privilege Level) 请求特权级别
RPL是针对段选择子而言的,每个段的选择子都有自己的RPL
举例说明:
1 | Mov ax,0008 与 Mov ax,000B //段选择子 |
数据段的权限检查
1 | 比如当前程序处于0环,也就是说CPL=0 |
数据段的权限检查:
1 | CPL <= DPL 并且 RPL <= DPL (数值上的比较) |
注意:代码段和系统段描述符中的检查方式并不一样
总结
1 | CPL CPU当前的权限级别 |
代码跨段执行流程
段寄存器:ES,CS,SS,DS,FS,GS,LDTR,TR
段寄存器读写:除CS外,其他的段寄存器都可以通过MOV,LES,LSS,LDS,LFS,LGS
指令进行修改
1、代码间的跳转(段间跳转 非调用门之类的)
段间跳转,有2种情况,即要跳转的段是一致代码段还是非一致代码段,也就是看C等于0还是1
同时修改CS与EIP的指令
JMP FAR / CALL FAR / RETF / INT /IRETED
[!NOTE]
注意:只改变EIP的指令 JMP / CALL / JCC / RET
2、执行流程
CPU如何执行这行代码? JMP 0x20:0x004183D7
(1) 段选择子拆分
1 | 0x20 对应二进制形式 0000 0000 0010 0000 |
(2) 查表得到段描述符
1 | TI = 0 所以查GDT表 |
(3) 权限检查
1 | 如果是非一致代码段,要求:CPL == DPL 并且 RPL <= DPL |
(4) 加载段描述符
1 | 通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中. |
(5) 代码执行
1 | CPU将 CS.Base + Offset 的值写入EIP 然后执行CS:EIP处的代码,段间跳转结束. |
3、总结
对于一致代码段:也就是共享的段
- 特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据
- 特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态还是用户态
对于普通代码段:也就是非一致代码段
- 只允许同级访问
- 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态.
直接对代码段进行JMP 或者 CALL的操作,无论目标是一致代码段还是非一致代码段,CPL都不会发生改变.如果要提升CPL的权限,只能通过调用门.
3、实验
3.1 构造段描述符
找一个非一致代码段描述符,复制一份,写入到GDT表中
1 | kd> r gdtr |
将 00cffb00`0000ffff 数据写如某个P位为0的位置
1 | kd> eq 8003f048 00cffb00`0000ffff(用内核工具查看新增是否成功) |
3.2 OD中进行测试
在OD中,执行跨段跳转 JMP FAR 004B:0041840D
成功的话,CS段寄存器就会修改成4B
3.3 修改段描述符的权限级别,并再次在OD中进行测试
将00cffb00`0000ffff 改为00cf9b00`0000ffff在OD中,执行跨段跳转 JMP FAR 004B:0041840D
3.4 将该段描述符的属性更改为一致代码段.
将00cffb00`0000ffff 改为00cf9f00`0000ffff在OD中,执行跨段跳转 JMP FAR 004B:0041840D
如果是一致代码段,要求:CPL >= DPL (当前同样可以访问)
CPL:3
DPL:0
4、总结
1、为了对数据进行保护,普通代码段是禁止不同级别进行访问的。用户态的代码不能访问内核的数据,同样,内核态的代码也不能访问用户态的数据.
2、如果想提供一些通用的功能,而且这些功能并不会破坏内核数据,那么可以选择一致代码段,这样低级别的程序可以在不提升CPL权限等级的情况下即可以访问.
3、如果想访问普通代码段,只有通过“调用门”等提示CPL权限,才能访问。
长调用与短调用
我们通过JMP FAR可以实现段间的跳转,如果要实现跨段的调用就必须要学习CALL FAR,也就是长调用.
CALL FAR比JMP FAR要复杂,JMP并不影响堆栈,但CALL指令会影响.
1、短调用
指令格式:CALL 立即数/寄存器/内存
发生改变的寄存器:ESP EIP
2、长调用(跨段不提权)
指令格式:CALL CS:EIP(EIP是废弃的)
发生改变的寄存器:ESP EIP CS
3、长调用(跨段并提权)
指令格式:CALL CS:EIP(EIP是废弃的)
发生改变的寄存器:ESP EIP CS SS
4、总结
- 跨段调用时,一旦有权限切换,就会切换堆栈.
- CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样.
- JMP FAR 只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL的权限.
调用门
1、调用门执行流程
指令格式:CALL CS:EIP(EIP是废弃的)
执行步骤:
- 根据CS的值 查GDT表,找到对应的段描述符 这个描述符是一个调用门.
- 在调用门描述符中存储另一个代码段段的选择子.
- 选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址.
2、门描述符
(16-31位)offset+(0-15位)offset = 跳转到函数执行的位置
3、构造一个调用门(无参 提权)
1 |
|
3.1 代码测试
步骤一:代码测试,并观察堆栈与寄存器的变化记录执行前的寄存器值:SS ESP CS
执行前:
执行后:
1 | eax=00000048 ebx=7ffd9000 ecx=00000000 |
变化:
1 | CS:1b->08 |
堆栈:
步骤二:在测试代码中加入特权指令并读取高2G内存
测试代码:
1 | BYTE GDT[6] = {0}; |
4、构造一个调用门(有参 提权)
调用门描述符:
0040EC03`00081030 (三个参数)
1 | kd>eq 8003f048 0040EC03`00081030 |
测试代码:
1 | // demo03.cpp : 定义控制台应用程序的入口点。 |
成功获取到3个传入的参数
5、总结
- 当通过门,权限不变的时候,只会PUSH两个值:CS和返回地址新的CS的值由调用门决定
- 当通过门,权限改变的时候,会PUSH四个值:SS、ESP、CS和返回地址 新的CS的值由调用门决定 新的SS和ESP由TSS提供
- 通过门调用时,要执行哪行代码有调用门决定,但使用RETF返回时,由堆栈中压入的值决定,这就是说,进门时只能按指定路线走,出门时可以翻墙(只要改变堆栈里面的值就可以想去哪去哪)
- 可不可以再建个门出去呢?也就是用Call 当然可以了 前门进 后门出
中断门
使用场景
- 系统调用
1 | 大家在开发应用程序时都会使用到Windows提供的API,这些API在执行的过程中需要从3环一步一步进入到0环,这一过程就是系统调用,在这个过程中也用到了中断门(一些比较老的API使用的是中断门,但是新API中使用的都是快速调用) |
- 调试
1 | 大家使用OD调试程序时候会使用断点,断点本质上就是在你选中的这一行中写入一个字节0xCC,也就是INT 3指令,这个指令就是用来执行中断门的。中断门也有一张表,我们称之为IDT(中断描述符表),与GDT一样,IDT也是由一系列描述符组成的,每个描述符占8字节,需要注意的是IDT表中的第一个元素不是NULL(GDT是) |
1、IDT
IDT即中断描述符表,同GDT一样,IDT也是由一系列描述符组成的,每个描述符占8个字节。但要注意的是,IDT表中的第一个元素不是NULL。在windbg中查看IDT表的基址和长度
2、IDT表的构成
IDT表可以包含3种门描述符:
- 任务门描述符
- 中断门描述符
- 陷阱门描述符
3、中断门描述符结构
当8-12位的值是01110则是一个中断门
函数执行:401030
0040ee00`00081030
4、构造一个中断门
1 | //INT index 指令触发寻找中断门 |
5、中断执行
INTX
其中,X是索引,X*8+IDT的基址就是具体的中断门描述符
6、中断返回
INT N指令:
在没有权限切换时,会向堆栈PUSH 3个值,分别是:CS EFLAG EIP(返回地址)
在有权限切换时,会向堆栈PUSH 5个值,分别是:SS ESP EFLAG CS EIP(返回地址)
在中断门中,不能通过RETF返回,而应该通过IRET/IRETD指令返回
陷阱门
1、陷阱门描述符结构
当8-12位的值是01111则是一个陷阱门
2、构造一个陷阱门
构造一个陷阱门(0040EF00 00081030)
0000ef00 00080000
0000 0000 0000 0000 1110 1111 0000 0000
写入到IDT表中
1 | eq>8003f500 0040EF00`0008103 |
执行陷阱门
1 | __asm{ |
3、陷阱门与中断门的区别
中断门执行时,将IF位(中断标志)清零,但陷阱门不会。
任务段
在调用门、中断门与陷阱门中,一旦出现权限切换,那么就会有堆栈的切换。而且,由于CS的CPL发生改变,也导致了SS也必须要切换。
切换时,会有新的ESP和SS(CS是由中断门或者调用门指定)这2个值从哪里来的呢?TSS (Task-state segment ),任务状态段.
1、TSS的结构
大小104字节
2、TSS的作用
不要把TSS与“任务切换”联系到一起
TSS的意义就在于可以同时换掉”一堆”寄存器
3、TR段寄存器取值
CPU如何找到TSS呢? 通过TR段寄存器,base找到TSS内存 Limit指的是TSS内存大小,TR段寄存器值来自于GDT表,GDT表里的TSS段描述符
4、TSS段描述符(TSS Descriptor)
5、TR寄存器读写
- 将TSS段描述符加载到TR寄存器
指令:LTR
说明:
用LTR指令去装载的话仅仅是改变TR寄存器的值(96位) 并没有真正改变TSS
LTR指令只能在系统层使用
加载后TSS段描述符会状态位会发生改变
6、修改TR寄存器
- 在Ring0 我们可以通过LTR指令去修改TR寄存器
- 在Ring3 我们可以通过CALL FAR 或者 JMP FAR指令来修改
用JMP去访问一个代码段的时候,改变的是CS和EIP :
1 | JMP 0x48:0x123456 如果0x48是代码段 |
用JMP去访问一个任务段的时候:
如果0x48是TSS段描述符,先修改TR寄存器,在用TR.Base指向的TSS中的值修改当前的寄存器
注意:
如果你用JMP的话Previous Task Link的值不变和NT位不变,没有人给你填充初始值多少现在就多少,如果用Call那就会写成原来那个TSS段的选择子和NT位置1
NT = 0 IRET(堆栈取 中断返回)
NT = 1 IRET (TSS:PTL 不是中断返回)
7、实现任务切换
- 构造完整的TSS
- 构造TSS段描述符
- 使用CALL FAR/JMP FAR指令修改TR寄存器
1 |
|
根据TSS段描述符的结构构造描述符,首先获取TSS的地址,在代码中下断点然后查看地址即可:其地址为0x0012fd70
我们就得出对应TSS段描述符中Base的值,接着Limit就是TSS的大小0x68,DPL为3(3环程序访问)
Type域为0x9即表示当前描述符没有被加载过。
在Windbg中断点输入
1 | kd> !process 0 0 |
指令找到当前的程序获取Cr3的值填入到程序中
最终得出TSS段描述符为:
构造TSS段描述符:XX00e9XX`XXXX0068
1 | tss:0x0012fc2c |
接着我们使用eq指令在Windbg中向GDT表中写入我们构造好的TSS段描述符:
通过windbg寄存器我们可以看到EIP,ESP,CS,SS变成了我们设置的值
任务门
1、任务门描述符
1 | 构造任务门:0000 e500 00c3 0000 |
2、任务门执行过程:
INT N
查IDT表,找到中断门描述符
通过中断门描述符,查GDT表,找到任务段描述符
使用TSS段中的值修改寄存器
IRETD返回
3、代码
段选择子一定要计算正确
1 | idt 0x20 构造中断门 |
1 | // demo07.cpp : Defines the entry point for the console application. |
分页
1、4GB空间
2、物理地址
地址分为:线性地址、有效地址、物理地址
指令:
1 | MOV eax,dword ptr ds:[0x12345678] |
0x12345678是属于有效地址
ds.base+0x12345678 = 线性地址
3、物理在哪
每个进程都有一个CR3,(准确的说是都一个CR3的值,CR3本身是个寄存器,一个核,只有一套寄存器)
CR3指向一个物理页,一共4096字节
4、设置分页方式
将noexecute 改成 execute
5、如何寻找物理地址(10-10-12为例)
10-10-12 一共32位,把一段地址拆分成三段10位 10位 12位
用notepad为例
第一步先在notepad中写入一个hello world字符串,然后使用CE进行搜索拿到字符串在进程中的地址
第二步拆分地址
1 | 000AA750 hello world字符串在进程中地址 |
第三步windbg进行查看找到cr3的值
1 | DirBase:314b7000 |
PDE/PTE
PDE和PTE介绍
1、PDE与PTE
2、指向相同物理页
- PTE可以没有物理页,且只能对应一个物理页
- 多个PTE也可以指向同一个物理页
3、实验
查分0地址,观察PTE是否有物理页
向0地址读写数据
没有挂物理页之前向0地址写入不了数据
手动挂物理页
1 | x的线性地址:0x1245052 |
PDE/PTE 属性
1、物理页的属性
物理页的属性 = PDE属性 & PTE属性
2、P位
PDE和PTE的P位如果为0,则当前的物理页是无效的,反之
3、R/W位
R/W = 0 只读
R/W = 1 可读可写
1 |
|
已知上面代码在正常情况下str是只读的,后续是无法对它进行任何的修改操作的。会报错内存访问异常
现在我们对R/W位进行查询
1 | 0x420f94 |
可以看到7c7e9025
对应的二进制是
1 | 0111 1100 0111 1110 1001 0000 0010 0101 而第二位R/W:0 说明是只读权限 |
现在我们修改7c7e9025
->7c7e9027
1 | 0111 1100 0111 1110 1001 0000 0010 0111 |
1 | kd> !dd 7f7ba000 + 80 |
现在看看效果
已经成功!!!!
4、U/S 位
U/S = 0 特权用户
U/S = 1 普通用户
5、P/S位
只对PDE有意义,PS == PageSize的意思
当PS==1的时候 PDE直接指向物理页 无PTE,低22位是页内偏移。也就是说少一级偏移。
6、A位
是否被访问(读或者写)过
A=1 访问过
[!NOTE]
即使只访问一个字节也会导致PDE或者PTE置1
7、D位
脏位:是否被写过
0:没有被写过
1:被写过
页目录表基址
1、拆分C0300000
当前我们用DbgView程序为例
1 | DirBase: 238ef000 |
结论:C0300000存储的值就是PDT
如果我们要访问第N个PDE,那么有如下公式:0xC0300000 + N*4
2、页目录表基址(XP系统 10-10-12分页模式)
3、总结
- 通过0xC0300000找到的物理页就是页目录表
- 这个物理页即是页目录表本身也是页表
- 页目录表是一张特殊的页表,每一项PTE指向的不是普通的物理页,而是指向其他的页表
- 如果我们要访问第N个PDE,那么有如下公式:0xC0300000 + N*4
页表基址
1、拆分C0000000 / C0001000
1 | DirBase: 238ef000 |
线性地址0xc0000000映射到第一个PTT表,0xC0000000到0xC0001000刚好相差0x1000相当于一个页,后续地址增量为4KB,对应不同的PTT表。 0xc0300000线性地址用于访问页目录表本身。掌握0xc0000000和0xc0300000两个地址等于掌握了进程内存的读写权限
总结:
- 页表被映射到了从0xC0000000到0xC03FFFFF的4M地址空间
- 在这1024个表中有一张特殊的表:页目录表
- 页目录被映射到了0xC0300000开始处的4K地址空间
2、总结
PDI与PTI:10-10-12 (I就是Index)
访问页目录表的公式:0xC0300000 + PDI*4
访问页表的公式:0xC0000000 + PDI*4096 + PTI*4
2-9-9-12分页
随着硬件的发展,这时候10-10-12分页的物理地址显然不够用,因此2-9-9-12分页随之产生。
1、10-10-12分页划分
- 先确定了页的大小 4K 所以后面的12位的功能就确定了。(4096 = 2^12)
- 当初的物理内存比较小,所以4个字节的PTE就够了,加上页的尺寸是4K 所以一个页能存储1024个 PTE 也就是2的10次方 第二个10也就确定了。(4096/4 = 1024 = 2^10)
- 剩下的10为PDI 10+10+12刚好32位
2、2-9-9-12分页划分
- 页的大小是确定的,4KB不能随便改,所以12确定了
- 如果想增大物理内存的访问范围,就需要增大PTE,考虑对齐的因素,增加到8个字节
- 同理PDI也是2的9次方32,2-9-9-12 还差2位 所以就再做一级 叫PDPI
3、2-9-9-12分页结构
PDPTE:Page-Directory-Point-Table Entry页目录指针表项 每项占8个字节
第四项PDPTE指向的是第四张PDT表时,第四张PDT表的前四项又分别指向PDPTT表的PDPTE
1 | 总结如下: |
4、如何寻找物理地址(2-9-9-12为例)
2-9-9-12 一共32位,把一段地址拆分成三段2位 9位 9位 12位
用notepad为例
第一步先在notepad中写入一个hello world字符串,然后使用CE进行搜索拿到字符串在进程中的地址
第二步拆分地址
1 | 000AA750 hello world字符串在进程中地址 |
第三步windbg进行查看找到cr3的值
1 | DirBase:123402a0 |
开启2-9-9-12:将C:\boot.ini文件中的execute改为noexecute 重启
5、PDPTE结构
- PDPTE共有四项(4 = 2^2)
- 35-12 存储的是页目录表的基址,低12位补0,共36位,即页目录基址。
6、PDE结构
- 当PS=1时是大页,35-21位是大页的物理地址,这样36位的物理地址的低21位为0,这就意味着页的大小为2MB,且都是2MB对齐。
1 | 2^21 = 2MB |
- 当PS=0时,35-12位是页表基址,低12位补0,共36位
1 | 2^12 = 4KB |
在所有的表项中(除了指向2MB页的页目录项),基地址都被视为36位物理地址的高24位,这就迫使页表和页都是4KB对齐的(这样,36位物理地址的低12位都为0)。当页目录项指向一个2MB的页时,基地址被视为36位物理地址的高15位,这就迫使2MB的页都是2MB对齐的(这样,36位物理地址的低21位为0)
7、PTE结构
PTE中35-12是物理页基址,24位,低12位补0
物理页基址+12位的页内偏移指向具体数据
8、XD标志位
AMD中称为NX,即No Excetion
PDE/PTE结构:
段的属性:有可读、可写和可执行
页的属性:有可读、可写
当RET执行返回的时候,如果我修改堆栈里面的数据指向一个我提前准备好的数据(把数据当作代码来执行)所以,Intel就做了硬件保护,做了一个不可执行位,XD=1时。那么你的软件溢出了也没有关系,即使你的EIP蹦到了危险的“数据区”,也是不可以执行的!在PAE分页模式下,PDE与PTE的最高位为XD/NX位
9、页目录表,页表基地址
PAE分页模式下
页目录表基址:0xC0600000
页表基址:0xC0000000
PDPTE、PDI与PTI
1 | 2-9-9-12 (I就是Index) |
访问页目录表项(PDE)的公式:
1 | 0xc0600000 + PDPTI * 0x200 * 8 + PDI * 8 |
访问页表项(PTE)的公式
1 | 0xc0000000 + PDPTI * 0x200 * 0x200 * 8 + PDI * 0x200 * 8 + PTI * 8 |
TLB
1、地址解析
- 通过一个线性地址访问一个物理页。比如:一个DWORD,其实未必真正读的是4个字节,我们先读的PDE再读PTE,最后才读的4个字节的页。
- 在2-9-9-12会读24个字节,如果跨页可能更多。为了提高效率,只能做记录。CPU内部做了一个表,来记录这些东西,这个表格是CPU内部的,和寄存器一样快,这个表格:TLB
2、TLB结构
- ATTR(属性):属性是PDPE PDE PTE三个属性AND起来的. 如果是10-10-12 就是PDE and PTE
- 不同的CPU 这个表的大小不一样
- 只要Cr3变了,TLB立马刷新,一核一套TLB.
操作系统的高2G映射基本不变,如果Cr3改了,TLB刷新 重建高2G以上很浪费。所以PDE和PTE中有个G标志位,如果G位为1刷新TLB时将不会刷新 PDE/PTE的G位为1的页,当TLB满了,根据统计信息将不常用的地址废弃,最近最常用的保留
3、TLB种类
TLB在X86体系的CPU里的实际应用最早是从Intel的486CPU开始的,在X86体系的CPU里边,一般都设有如下4组TLB:
第一组:缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB)
第二组:缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB)
第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB)
第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)
中断与异常
1、什么是中断
- 中断通常是由CPU外部的输入输出设备(硬件)所触发的,供外部设备通知CPU“有事情需要处理”,因此又叫中断请求(Interrupt Request)
- 中断请求的目的是希望CPU暂时停止执行当前正在执行的程序,转去执行中断请求所对应的中断处理例程(中断处理程序在哪有IDT表决定)
- 80x86有两条中断请求线
- 非屏蔽中断线,称为NMI(NonMaskable Interrupt)
- 可屏蔽中断线,称为INTR(Interrupt Require)
2、非可屏蔽中断如何处理
当非可屏蔽中断产生时,CPU在执行完当前指令后会里面进入中断处理程序非可屏蔽中断不受EFLAG寄存器中IF位的影响,一旦发生,CPU必须处理非可屏蔽中断处理程序位于IDT表中的2号位置
3、可屏蔽中断
在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断控制器.它负责分配中断资源和管理各个中断源发出的中断请求.为了便于标识各个中断请求,中断管理器通常用IRQ(Interrupt Request)后面加上数字来表示不同的中断
比如:在Windows中 时钟中断的IRQ编号为0 也就是:IRQ0
4、时钟中断
大多数操作系统时钟中断在10-100MS之间,Windows系列为10-20MS
5、可屏蔽中断如何处理
(IDT表)中断号 | IRQ | 说明 |
---|---|---|
0x30 | IRQ0 | 时钟中断 |
0x31-0x3F | IRQ1-IRQ15 | 其他硬件设备的中断 |
如果自己的程序执行时不希望CPU去处理这些中断
- 用CLI指令清空EFLAG寄存器中的IF位
- 用STI指令设置EFLAG寄存器中的IF位
硬件中断与IDT表中的对应关系并非固定不变的,参见:APIC(高级可编程中断控制器)
6、异常
异常通常是CPU在执行指令时检测到的某些错误,比如除0、访问无效页面等
中断与异常的区别:
1、中断来自于外部设备,是中断源(比如键盘)发起的,CPU是被动的.
2、异常来自于CPU本身,是CPU主动产生的
3、INT N虽然被称为“软件中断”,但其本质是异常。EFLAG的IF位对INT N无效。
7、异常处理
无论是由硬件设备触发的中断请求还是由CPU产生的异常,处理程序都在IDT表。
常见的异常处理程序:
错误类型 | (IDT表)中断号 |
---|---|
页错误 | 0xE |
段错误 | 0xD |
除零错误 | 0x0 |
双重错误 | 0x8 |
8、缺页异常
缺页异常的产生:
1、当PDE/PTE的P=0时。
2、当PDE/PTE的属性为只读,但程序试图写入的时一旦发生缺页异常,CPU会执行IDT表中的0xE号中断处理程序,由操作系统来接管。
控制寄存器
1、控制寄存器
控制寄存器用于控制和确定CPU的操作模式
控制寄存器类型:Cr0、Cr1、Cr2、Cr3、Cr4、Cr8
Cr0:包含系统控制标志,用于控制处理器的工作模式和状态
Cr1:保留
Cr2:包含页面错误线性地址(导致页面错误的线性地址)
Cr3:页目录表基址
Cr4:包含一组标志位,用于启用一些体系结构扩展功能,并指示操作系统或执行程序对特定处理器功能的支持情况。
Cr8:仅64位下使用
2、Cr0
PE位:CR0的位0是启用保护(Protection Enable)标志。
PE=1保护模式
PE=0 实地址模式
1 | 这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE和PG标志都要置位。 |
PG位:当设置该位时即开启了分页机制。在开启这个标志之前必须已经或者同时开启PE标志。
1 | PG=0且PE=0 处理器工作在实地址模式下 |
WP位:对于Intel 80486或以上的CPU,CR0的位16是写保护(Write Proctect)标志当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作;
1 | 当CPL<3的时候: |
3、Cr2
当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在CR2中。
4、Cr4
PAE:1 是2-9-9-12分页 ,0 是10-10-12分页
PSE:
PWT/PCD
1、CPU缓存
- CPU缓存是位于CPU与物理内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。
- CPU缓存可以做的很大,有几K、几十K、几百K 甚至上M的也有。
CPU缓存与TLB的区别:
TLB:线性地址 <—–> 物理地址
CPU缓存:物理地址 <—–> 内容
所以当拿到一个程序的线性地址查询时,先会使用TLB,然后在使用CPU缓存
2、关于PWT/PCD属性
PWT:Page Write Through
1 | PWT = 1 时 写Cache的时候也要将数据写入内存中。 |
PCD:Page Cache Disable
1 | PCD = 1时,禁止某个页写入缓存,直接写内存。 比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了 |
具体细节参考Intel白皮书
调试踩坑
1、32位的系统就得用老版本的32位windbg,不要用windbg perview 暂时不知道怎么解决
2、windbg老版本遇到寄存器内容无法显示
WingDbg.dll插件放windbg相同目录下
1 | !WingDbg.regfix |