保护模式介绍
什么是保护模式?
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我们真正读写的地址是:
ES CS SS DS FS GS LDTR TR 共8个
2、段寄存器的结构

3、段寄存器的读写
3.1 读段寄存器
1 2 3 4 5
| 比如:MOV AX,ES 只能读16位的可见部分
读写LDTR的指令为:SLDT/LLDT
读写TR的指令为:STR/LTR
|
3.2 写段寄存器
段寄存器属性探测
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 2 3 4 5 6 7
| int var = 0; __asm { mov ax,ss //cs不行 cs是可读 可执行 但不可写 mov ds,ax mov dword ptr ds:[var],eax }
|
3、探测Base
1 2 3 4 5 6 7 8 9 10
| int var = 1; __asm { mov ax,fs mov gs,ax mov eax,gs:[0] //不要用DS 否则编译不过去 mov dword ptr ds:[var],eax //mov edx,dword ptr ds:[0x7FFDF000] }
|
4、探测Limit
1 2 3 4 5 6 7 8 9 10
| int var = 1; __asm { mov ax,fs mov gs,ax mov eax,gs:[0] //不要用DS 否则编译不过去 mov dword ptr ds:[var],eax //mov edx,dword ptr ds:[0x7FFDF000] }
|
段描述符与段选择子
段描述符分类
GDT(全局描述符表)
LDT(局部描述符表)
当我们执行类似MOV DS,AX指令时,CPU会查表,根据AX的值来决定查找GDT还是LDT,查找表的什么位置,查出多少数据.

段描述符

段选择子
段选择子是一个16位的段描述符,该描述符指向了定义该段的段描述符.

RPL:请求特权级别
TI:
Index:处理器将索引值乘以8在加上GDT或者LDT的Base地址,就是要加载的段描述符

加载段描述符至段寄存器
除了MOV指令,我们还可以使用LES、LSS、LDS、LFS、LGS指令修改寄存器
CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要改CS,必须要保证CS与EIP一起改,后面会讲
1 2 3 4 5
| char buffer[6]; __asm { les ecx,fword ptr ds:[buffer] //高2个字节给es,低四个字节给ecx }
|
[!NOTE]
注意:RPL<=DPL(在数值上)
段描述符属性
P位和G位
1、P位
1 2 3
| P = 1 段描述符有效
P = 0 段描述符无效
|
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 2 3
| C = 1 一致代码段
C = 0 非一致代码段
|
系统段(SS)描述符
当S=0时,该段描述符为系统描述符.系统描述符有分为以下类型

DB位
DB位:当前实际地址最多大小
1、CS段

整个地址空间是4KB
1 2
| D = 0 采用16位寻址方式 前缀67 改变寻址方式
|

整个地址空间是64K
2、SS段
1 2 3
| D = 1 隐式堆栈访问指令(如:PUSH POP CALL) 使用32位堆栈指针寄存器ESP D = 0 隐式堆栈访问指令(如:PUSH POP CALL) 使用16位堆栈指针寄存器SP
|
3、向下拓展的数据段

总结
- 段描述符中的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 2 3
| mov DS,AX 如果AX指向的段DPL = 0
但当前程序的CPL = 3 这行指令是不会成功的
|
RPL(Request Privilege Level) 请求特权级别
RPL是针对段选择子而言的,每个段的选择子都有自己的RPL

举例说明:
1 2 3
| Mov ax,0008 与 Mov ax,000B //段选择子
Mov ds,ax Mov ds,ax //将段描述指向的是同一个段描述符,但RPL是不一样的.
|
数据段的权限检查
1 2 3 4 5
| 比如当前程序处于0环,也就是说CPL=0
Mov ax,000B //1011 RPL = 3
Mov ds,ax //ax指向的段描述符的DPL = 0
|
数据段的权限检查:
1
| CPL <= DPL 并且 RPL <= DPL (数值上的比较)
|
注意:代码段和系统段描述符中的检查方式并不一样
总结
1 2 3 4 5
| CPL CPU当前的权限级别
DPL 如果你想访问我,你应该具备什么样的权限
RPL 用什么权限去访问一个段
|
代码跨段执行流程
段寄存器: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 2 3 4
| 0x20 对应二进制形式 0000 0000 0010 0000 RPL = 00 TI = 0 Index = 4
|
(2) 查表得到段描述符
1 2 3 4 5
| TI = 0 所以查GDT表 Index = 4 找到对应的段描述符
四种情况可以跳转:代码段、调用门、TSS任务段、任务门
|
(3) 权限检查
1 2 3
| 如果是非一致代码段,要求:CPL == DPL 并且 RPL <= DPL 如果是一致代码段,要求:CPL >= DPL
|
(4) 加载段描述符
1
| 通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中.
|
(5) 代码执行
1
| CPU将 CS.Base + Offset 的值写入EIP 然后执行CS:EIP处的代码,段间跳转结束.
|
3、总结
对于一致代码段:也就是共享的段
- 特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据
- 特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态还是用户态
对于普通代码段:也就是非一致代码段
- 只允许同级访问
- 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态.
直接对代码段进行JMP 或者 CALL的操作,无论目标是一致代码段还是非一致代码段,CPL都不会发生改变.如果要提升CPL的权限,只能通过调用门.
3、实验
3.1 构造段描述符
找一个非一致代码段描述符,复制一份,写入到GDT表中
1 2 3 4 5 6 7 8 9 10 11
| kd> r gdtr gdtr=8003f000 kd> dq 8003f000 8003f000 00000000`00000000 00cf9b00`0000ffff 8003f010 00cf9300`0000ffff 00cffb00`0000ffff 8003f020 00cff300`0000ffff 80008b04`200020ab 8003f030 ffc093df`f0000001 0040f300`00000fff 8003f040 0000f200`0400ffff 00000000`00000000 8003f050 80008954`b1000068 80008954`b1680068 8003f060 00009302`2f40ffff 0000920b`80003fff 8003f070 ff0092ff`700003ff 80009a40`0000ffff
|
将 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 2 3 4 5 6 7 8 9 10 11
| 第一步:构造大概的值 DPL:3环 == 11 0000 0000 0000 0000 1110 1100(调用门固定值) 0000 0000 == 0000ec00 0000 0000 0000 1000 0000 0000 0000 0000 == 00080000 0000ec00`00080000 第二步:拿到方法地址 401000 0040ec00`00081000 第三步:eq 80b99048 0040ec00`00081000 第四步:运行程序
|
3.1 代码测试
步骤一:代码测试,并观察堆栈与寄存器的变化记录执行前的寄存器值:SS ESP CS
执行前:

执行后:
1 2 3 4 5 6 7
| eax=00000048 ebx=7ffd9000 ecx=00000000 edx=00000001 esi=00000000 edi=0012fe5c eip=00401000 esp=a69f5ca0 ebp=0012fe5c iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000206
|
变化:
1 2 3 4 5
| CS:1b->08
ESP:12fd7c->a682bca0(高地址)
SS:23->10
|
堆栈:

步骤二:在测试代码中加入特权指令并读取高2G内存
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| BYTE GDT[6] = {0}; DWORD dwH2Gvalue; void _declspec(naked) getRegister(){ __asm{ pushad pushfd mov eax,0x80b9900c mov ebx,[eax] mov dwH2Gvalue,ebx sgdt GDT
popfd popad
retf }
} void printfRegister(){ DWORD GDT_ADDR = *(PDWORD)(&GDT[2]); WORD GDT_LIMIT = *(PWORD)(&GDT[0]); printf("%x %x %x\n",dwH2Gvalue,GDT_ADDR,GDT_LIMIT); }
int testfun1(){ __asm{ mov ebx,ebx mov ebx,ebx }
char buf[6];
*(DWORD*)&buf[0] = 0x12345678; *(WORD*)&buf[4] = 0x48;
__asm{ call fword ptr[buf] } printfRegister(); getchar(); return 0; }
int _tmain(int argc, _TCHAR* argv[]) { testfun1(); return 0; }
|

4、构造一个调用门(有参 提权)
调用门描述符:
0040EC03`00081030 (三个参数)
1
| kd>eq 8003f048 0040EC03`00081030
|
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
|
#include "stdafx.h" #include <windows.h>
DWORD x,y,z;
void _declspec(naked) getRegister(){ __asm{ pushad pushfd mov eax,[esp+0x24+0x8+0x8] mov dword ptr ds:[x],eax
mov eax,[esp+0x24+0x8+0x4] mov dword ptr ds:[y],eax mov eax,[esp+0x24+0x8+0x0] mov dword ptr ds:[z],eax popfd popad
retf 0xc }
} void printfRegister(){ printf("%x %x %x\n",x,y,z); }
int testfun1(){ char buf[6];
*(DWORD*)&buf[0] = 0x12345678; *(WORD*)&buf[4] = 0x68;
__asm{ push 1 push 2 push 3 call fword ptr[buf] } printfRegister(); getchar(); return 0; }
int _tmain(int argc, _TCHAR* argv[]) { testfun1(); return 0; }
|
成功获取到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 2 3 4 5 6 7
|
__asm{ push fs INT 0x22 pop fs }
|
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
|
执行陷阱门
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
说明:
6、修改TR寄存器
- 在Ring0 我们可以通过LTR指令去修改TR寄存器
- 在Ring3 我们可以通过CALL FAR 或者 JMP FAR指令来修改
用JMP去访问一个代码段的时候,改变的是CS和EIP :
1 2
| JMP 0x48:0x123456 如果0x48是代码段 执行后:CS-->0x48 EIP-->0x123456
|
用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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| #include <stdio.h> #include <windows.h> int o_eax, n_esp; short n_cs, n_ss; void __declspec(naked) GetValue() { _asm { mov o_eax, eax mov n_esp, esp mov ax, cs mov n_cs, ax mov ax, ss mov n_ss, ax mov eax, o_eax iretd } } void main() { char stack[100] = {0}; char buffer[6] = {0x0, 0x0, 0x0, 0x0, 0x4B, 0x0}; int iCr3 = 0; printf("Input: "); scanf("%x", &iCr3); getchar(); DWORD tss[0x68] = { 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, (DWORD)iCr3, 0x00401020, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, (DWORD)stack, 0x00000000, 0x00000000, 0x00000000, 0x00000023, 0x00000008, 0x00000010, 0x00000023, 0x00000030, 0x00000000, 0x00000000, 0x20ac0000 }; _asm { push fs call far fword ptr[buffer] pop fs } printf("ESP: %x, CS: %x, SS: %x \n", n_esp, n_cs, n_ss); getchar(); }
|
根据TSS段描述符的结构构造描述符,首先获取TSS的地址,在代码中下断点然后查看地址即可:其地址为0x0012fd70
我们就得出对应TSS段描述符中Base的值,接着Limit就是TSS的大小0x68,DPL为3(3环程序访问)
Type域为0x9即表示当前描述符没有被加载过。
在Windbg中断点输入
指令找到当前的程序获取Cr3的值填入到程序中
最终得出TSS段描述符为:
构造TSS段描述符:XX00e9XX`XXXX0068
1 2 3 4
| tss:0x0012fc2c eq 8003f0c0 0000e912`fc2c0068
cr3:bf3915e0 getValue 4113d0
|
接着我们使用eq指令在Windbg中向GDT表中写入我们构造好的TSS段描述符:

通过windbg寄存器我们可以看到EIP,ESP,CS,SS变成了我们设置的值

任务门
1、任务门描述符

1 2
| 构造任务门:0000 e500 00c3 0000 >eq 8003f500 0000e500`00c30000
|
2、任务门执行过程:
INT N
查IDT表,找到中断门描述符
通过中断门描述符,查GDT表,找到任务段描述符
使用TSS段中的值修改寄存器
IRETD返回
3、代码
段选择子一定要计算正确
1 2 3 4
| idt 0x20 构造中断门 eq 8003f500 0000e500`004b0000 gdt 0x4b 构造任务门 0100 1011 RPL:3 eq 8003f048 0000e912`fd700068
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
|
#include "stdafx.h" #include <stdio.h> #include <windows.h> DWORD dwOK,dwESP,dwCS; void __declspec(naked) GetValue() { dwOK = 1; _asm { mov eax,esp mov dwESP,eax mov ax,cs mov word ptr[dwCS],ax
iretd } }
int main() { char stack[100] = {0}; char buffer[6] = {0x0, 0x0, 0x0, 0x0, 0xC0, 0x0}; int iCr3 = 0; printf("Input: "); scanf("%x", &iCr3); getchar();
DWORD tss[0x68] = { 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, (DWORD)iCr3, (ULONG)GetValue, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, (DWORD)stack, 0x00000000, 0x00000000, 0x00000000, 0x00000023, 0x00000008, 0x00000010, 0x00000023, 0x00000030, 0x00000000, 0x00000000, 0x20ac0000 }; printf("TSS:%x\n",&tss); getchar(); _asm { PUSH fs INT 0X20 POP fs } printf("ESP: %x, CS: %x, dwOK: %x \n", dwESP, dwCS, dwOK); getchar(); return 0; }
|

分页
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 2 3 4
| 000AA750 hello world字符串在进程中地址 第一级:0000 0000 00 0 第二级:00 1010 1010 AA * 4 = 2A8 第三级:750
|
第三步windbg进行查看找到cr3的值
1 2 3 4 5
| DirBase:314b7000 第一级:!dd cr3+0 = 314b7000+0 第二级:!dd 3192d067+2A8 后面三位是属性使用时改成000 第三级:!dd 319ed000+750 !db 319ed000+750 可验证
|

PDE/PTE
PDE和PTE介绍
1、PDE与PTE

2、指向相同物理页
- PTE可以没有物理页,且只能对应一个物理页
- 多个PTE也可以指向同一个物理页
3、实验
查分0地址,观察PTE是否有物理页

向0地址读写数据
没有挂物理页之前向0地址写入不了数据

手动挂物理页
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| x的线性地址:0x1245052 先拆分 0000 0001 00 10 10 0100 0101 245 * 4 = 914 052
DirBase: 553aa000 第一级: kd> !dd 553aa000 #553aa000 54ad6067 54a95067 00000000 00000000 #553aa010 00000000 00000000 00000000 00000000 #553aa020 00000000 00000000 00000000 00000000 #553aa030 00000000 00000000 00000000 00000000 #553aa040 00000000 00000000 00000000 00000000 #553aa050 00000000 00000000 00000000 00000000 #553aa060 00000000 00000000 00000000 00000000 #553aa070 00000000 00000000 00000000 00000000
第二级: kd> !dd 54ad6000 + 914 #54ad6914 54df5067 00000000 00000000 00000000 #54ad6924 00000000 00000000 00000000 00000000 #54ad6934 00000000 00000000 00000000 5576d067 #54ad6944 5556f067 553b0067 00000000 00000000 #54ad6954 00000000 00000000 00000000 00000000 #54ad6964 00000000 00000000 00000000 00000000 #54ad6974 00000000 00000000 00000000 0bb31025 #54ad6984 0bb32025 00000000 00000000 00000000
查询下0地址 kd> !dd 54ad6000 #54ad6000 00000000 00000000 00000000 00000000 #54ad6010 00000000 00000000 00000000 00000000 #54ad6020 00000000 00000000 00000000 00000000 #54ad6030 00000000 00000000 00000000 00000000 #54ad6040 54e97067 00000000 00000000 00000000 #54ad6050 00000000 00000000 00000000 00000000 #54ad6060 00000000 00000000 00000000 00000000 #54ad6070 00000000 00000000 00000000 00000000
把当前x变量的物理页挂到0地址上 kd> !ed 54ad6000 54df5067
|

PDE/PTE 属性
1、物理页的属性
物理页的属性 = PDE属性 & PTE属性


2、P位
PDE和PTE的P位如果为0,则当前的物理页是无效的,反之
3、R/W位
R/W = 0 只读
R/W = 1 可读可写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include "stdafx.h" #include <windows.h>
int main(int argc, char* argv[]) { char* str = "hello world"; printf("%x\n",str); getchar();
DWORD dwAddr = (DWORD)str;
*(char*)dwAddr = 'M';
printf("%s\n",str); return 0; }
|
已知上面代码在正常情况下str是只读的,后续是无法对它进行任何的修改操作的。会报错内存访问异常

现在我们对R/W位进行查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 0x420f94 Dirbase:7f4fd000 0000 0000 01 00 0010 0000 20*4=80 f94 第一级: kd> !dd 7f4fd000 #7f4fd000 7f57b067 7f7ba067 00000000 00000000 #7f4fd010 00000000 00000000 00000000 00000000 #7f4fd020 00000000 00000000 00000000 00000000 #7f4fd030 00000000 00000000 00000000 00000000 #7f4fd040 00000000 00000000 00000000 00000000 #7f4fd050 00000000 00000000 00000000 00000000 #7f4fd060 00000000 00000000 00000000 00000000 #7f4fd070 00000000 00000000 00000000 00000000 第二级: kd> !dd 7f7ba000 + 80 #7f7ba080 7c7e9025 00000000 00de4067 7f895067 #7f7ba090 7fe65067 7eb52027 00000000 00000000 #7f7ba0a0 00000000 00000000 00000000 00000000 #7f7ba0b0 00000000 00000000 00000000 00000000 #7f7ba0c0 7f8dc067 7f7dd067 7f5de067 7f8df067 #7f7ba0d0 7f960067 7fe61067 00de2067 7fbe3067 #7f7ba0e0 00000000 00000000 00000000 00000000 #7f7ba0f0 00000000 00000000 00000000 00000000
|
可以看到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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| kd> !dd 7f7ba000 + 80 #7f7ba080 7c7e9025 00000000 00de4067 7f895067 #7f7ba090 7fe65067 7eb52027 00000000 00000000 #7f7ba0a0 00000000 00000000 00000000 00000000 #7f7ba0b0 00000000 00000000 00000000 00000000 #7f7ba0c0 7f8dc067 7f7dd067 7f5de067 7f8df067 #7f7ba0d0 7f960067 7fe61067 00de2067 7fbe3067 #7f7ba0e0 00000000 00000000 00000000 00000000 #7f7ba0f0 00000000 00000000 00000000 00000000 kd> !ed 7f7ba080 7c7e9027 kd> !dd 7f7ba000 + 80 #7f7ba080 7c7e9027 00000000 00de4067 7f895067 #7f7ba090 7fe65067 7eb52027 00000000 00000000 #7f7ba0a0 00000000 00000000 00000000 00000000 #7f7ba0b0 00000000 00000000 00000000 00000000 #7f7ba0c0 7f8dc067 7f7dd067 7f5de067 7f8df067 #7f7ba0d0 7f960067 7fe61067 00de2067 7fbe3067 #7f7ba0e0 00000000 00000000 00000000 00000000 #7f7ba0f0 00000000 00000000 00000000 00000000
|
现在看看效果

已经成功!!!!
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| DirBase: 238ef000
1100 0000 00 00 300 * 4 = C00 11 0000 0000 300 * 4 = C00 000
kd> !dd 238ef000 #238ef000 237e3067 2365a067 23a2e067 23a1c067 #238ef010 00000000 00000000 00000000 00000000 #238ef020 00000000 00000000 00000000 00000000 #238ef030 00000000 00000000 00000000 00000000 #238ef040 00000000 00000000 00000000 00000000 #238ef050 00000000 00000000 00000000 00000000 #238ef060 00000000 00000000 00000000 00000000 #238ef070 00000000 00000000 00000000 00000000
kd> !dd 238ef000 + C00 PDT #238efc00 238ef063 236f0063 00000000 09f55163 //PDE #238efc10 09f56163 09f57163 09f58163 09f59163 #238efc20 09f5a163 09f5b163 09f5c163 09f5d163 #238efc30 09f5e163 09f5f163 09f60163 09f61163 #238efc40 09f62163 09f63163 09f64163 09f65163 #238efc50 09f66163 09f67163 09f68163 09f69163 #238efc60 09f6a163 09f6b163 09f2c163 09f2d163 #238efc70 09f2e163 09f2f163 09f30163 09f31163
kd> !dd 238ef000 + C00 PTT #238efc00 238ef063 236f0063 00000000 09f55163 //PTE #238efc10 09f56163 09f57163 09f58163 09f59163 #238efc20 09f5a163 09f5b163 09f5c163 09f5d163 #238efc30 09f5e163 09f5f163 09f60163 09f61163 #238efc40 09f62163 09f63163 09f64163 09f65163 #238efc50 09f66163 09f67163 09f68163 09f69163 #238efc60 09f6a163 09f6b163 09f2c163 09f2d163 #238efc70 09f2e163 09f2f163 09f30163 09f31163
kd> !dd 238ef000 #238ef000 237e3067 2365a067 23a2e067 23a1c067 #238ef010 00000000 00000000 00000000 00000000 #238ef020 00000000 00000000 00000000 00000000 #238ef030 00000000 00000000 00000000 00000000 #238ef040 00000000 00000000 00000000 00000000 #238ef050 00000000 00000000 00000000 00000000 #238ef060 00000000 00000000 00000000 00000000 #238ef070 00000000 00000000 00000000 00000000
|
结论:C0300000存储的值就是PDT
如果我们要访问第N个PDE,那么有如下公式:0xC0300000 + N*4
2、页目录表基址(XP系统 10-10-12分页模式)

3、总结
- 通过0xC0300000找到的物理页就是页目录表
- 这个物理页即是页目录表本身也是页表
- 页目录表是一张特殊的页表,每一项PTE指向的不是普通的物理页,而是指向其他的页表
- 如果我们要访问第N个PDE,那么有如下公式:0xC0300000 + N*4
页表基址
1、拆分C0000000 / C0001000

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| DirBase: 238ef000 第一个PTT kd> !dd 237e3000 #237e3000 00000000 00000000 00000000 00000000 #237e3010 00000000 00000000 00000000 00000000 #237e3020 00000000 00000000 00000000 00000000 #237e3030 00000000 00000000 00000000 00000000 #237e3040 23764067 00000000 00000000 00000000 #237e3050 00000000 00000000 00000000 00000000 #237e3060 00000000 00000000 00000000 00000000 #237e3070 00000000 00000000 00000000 00000000
第二个PTT kd> !dd 2365a000 #2365a000 238ee025 23951025 23b08025 239c9025 #2365a010 23b0a025 23b8b025 23b4c025 2388d025 #2365a020 2398e025 238cf025 00000000 238ad025 #2365a030 00000000 239ef025 23830025 23931025 #2365a040 00000000 23833025 23734025 237f5025 #2365a050 236f6025 23677025 00000000 00000000 #2365a060 00000000 00000000 00000000 00000000 #2365a070 00000000 235a5025 23666025 00000000
C0000000 11 0000 0000 300 * 4 = C00 00 0000 0000 0 0
kd> !dd 238ef000 + C00 kd> !dd 238ef000 + 0 kd> !dd 237e3000 #237e3000 00000000 00000000 00000000 00000000 #237e3010 00000000 00000000 00000000 00000000 #237e3020 00000000 00000000 00000000 00000000 #237e3030 00000000 00000000 00000000 00000000 #237e3040 23764067 00000000 00000000 00000000 #237e3050 00000000 00000000 00000000 00000000 #237e3060 00000000 00000000 00000000 00000000 #237e3070 00000000 00000000 00000000 00000000 用0xC0000000查出来的是第一张PTT表
C0001000 11 0000 0000 300 * 4 = C00 00 0000 0001 1 * 4 = 0 0
kd> !dd 238ef000 + C00 kd> !dd 238ef000 + 4 kd> !dd 2365a000 #2365a000 238ee025 23951025 23b08025 239c9025 #2365a010 23b0a025 23b8b025 23b4c025 2388d025 #2365a020 2398e025 238cf025 00000000 238ad025 #2365a030 00000000 239ef025 23830025 23931025 #2365a040 00000000 23833025 23734025 237f5025 #2365a050 236f6025 23677025 00000000 00000000 #2365a060 00000000 00000000 00000000 00000000 #2365a070 00000000 235a5025 23666025 00000000 用0xC0001000查出来的是第二张PTT表
|
线性地址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)

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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 总结如下: 1.第三个PDPTE指向了一个PDT表,此表的前四项 指向了PDPTE的每一个元素 2.根据计算 C0600000 的最大索引,不会超过 C0604000 mov edi, edi .text:0043C92A 55 push ebp .text:0043C92B 8B EC mov ebp, esp .text:0043C92D 51 push ecx .text:0043C92E 51 push ecx .text:0043C92F 8B 4D 08 mov ecx, [ebp+VirtualAddress] .text:0043C932 56 push esi .text:0043C933 8B C1 mov eax, ecx .text:0043C935 C1 E8 12 shr eax, 12h .text:0043C938 BE F8 3F 00 00 mov esi, 3FF8h .text:0043C93D 23 C6 and eax, esi .text:0043C93F 2D 00 00 A0 3F sub eax, 3FA00000h 线性地址 >>18 +C0600000 也就是说。得出的索引是 2^14,最大值4000H 实际应用从0开始,也就是 4000H-(1*8byte) 那么得到最后一个PDE 也就是 C06003FFE8,由此可推出 C0600000是第一个PDT表的首地址 C0601000是第二个PDT表的首地址 C0602000是第三个PDT表的首地址 C0603000是第四个PDT表的首地址
|
4、如何寻找物理地址(2-9-9-12为例)
2-9-9-12 一共32位,把一段地址拆分成三段2位 9位 9位 12位
用notepad为例
第一步先在notepad中写入一个hello world字符串,然后使用CE进行搜索拿到字符串在进程中的地址

第二步拆分地址
1 2 3 4 5
| 000AA750 hello world字符串在进程中地址 第一部分:00 0*8 第一部分:00 0000 000 0*8 第一部分:0 1010 1010 AA * 8 = 550 第四部分:750
|
第三步windbg进行查看找到cr3的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| DirBase:123402a0 第一级:!dd 123402a0 kd> !dd 123402a0 #123402a0 288a8001 00000000 28729001 00000000 #123402b0 2886a001 00000000 287a7001 00000000 #123402c0 1d083001 00000000 1cf44001 00000000 #123402d0 1cec5001 00000000 1d142001 00000000 #123402e0 1d402001 00000000 1d383001 00000000 #123402f0 1d1c4001 00000000 1d301001 00000000 #12340300 1cc20001 00000000 1cce1001 00000000 #12340310 1cc62001 00000000 1cb5f001 00000000
第二级:!dd 288a8000+0 后面三位是属性使用时改成000 kd> !dd 288a8000 #288a8000 285e4067 00000000 28b43067 00000000 #288a8010 28696067 00000000 00000000 00000000 #288a8020 285e5067 00000000 2885e067 00000000 #288a8030 28ca1067 00000000 29088067 00000000 #288a8040 28757067 00000000 00000000 00000000 #288a8050 00000000 00000000 00000000 00000000 #288a8060 00000000 00000000 00000000 00000000 #288a8070 00000000 00000000 00000000 00000000
第三级:!dd 285e4000+550 kd> !dd 285e4000+550 #285e4550 2892b067 80000000 28821067 80000000 #285e4560 28922067 80000000 286e4067 80000000 #285e4570 28625067 80000000 28866067 80000000 #285e4580 288e7067 80000000 28a68067 80000000 #285e4590 288e9067 80000000 00000200 00000000 #285e45a0 00000200 00000000 00000200 00000000 #285e45b0 29142067 80000000 28c8d067 80000000 #285e45c0 28ed1067 80000000 28d94067 80000000
第四级:!dd 2892b000 + 750 kd> !dd 2892b000 + 750 #2892b750 00650068 006c006c 0020006f 006f0077 #2892b760 006c0072 00000064 00000000 00000000 #2892b770 00000000 00000000 00000000 00000000 #2892b780 00000000 00000000 00000000 00000000 #2892b790 00000000 0088000c 000a0005 0008014a #2892b7a0 00000000 000350ac 000aaec8 000aa3e0 #2892b7b0 00000000 00000000 00000000 00000000 #2892b7c0 0005002e 00102341 000800cc 00000000
!db 2892b000 + 750 可验证
|

开启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对齐。
- 当PS=0时,35-12位是页表基址,低12位补0,共36位
在所有的表项中(除了指向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
访问页目录表项(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 2 3 4 5
| PG=0且PE=0 处理器工作在实地址模式下 PG=0且PE=1 处理器工作在没有开启分页机制的保护模式下
PG=1且PE=0 在PE没有开启的情况下 无法开启PG PG=1且PE=1 处理器工作在开启了分页机制的保护模式下
|
WP位:对于Intel 80486或以上的CPU,CR0的位16是写保护(Write Proctect)标志当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作;
1 2 3 4
| 当CPL<3的时候:
如果 WP=0 可以读写任意用户级物理页,只要线性地址有效. 如果 WP=1 可以读取任意用户级物理页,但对于只读的物理页,则不能写.
|
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相同目录下
下载地址:https://github.com/mbikovitsky/WingDbg