Cr3
描述:
在所有的寄存器中,只有Cr3存储的是物理地址,其它寄存器存的都是线性地址
Cr3所存储的物理地址指向了一个页目录表(PDT)
在Windows中,一个页的大小通常为4KB,即一个页可以存储1024个页目录表项(PDE)
下面是手册中的描述在第3卷2.5 CONTROL REGISTERS
物理页结构图:
注意:但其实上面这种结构方式是错误的,这个以后再解释,先这样理解
PDE(页目录表项)
描述:
页目录表(PDT)的每一项元素称为页目录表项(PDE)
每个页目录表项指向一个页表(PTT)
每个页表的大小为4KB,即一个页表可以存储1024个页表项(PTE)
PTE(页表项)
描述:
页表(PTT)的每一个元素称为页表项(PTE)
页表项(PTE)所指向的才是真正的物理页
特点:
- PTE可以指向一个物理页,也可以不指向物理页
- 多个PTE可以指向同一个物理页
- 一个PTE只能指向一个物理页
物理页的属性
物理页的属性
=PDE属性
& PTE属性
下面两张图看得更直观一些
PDE(页目录表项)
PTE(页表项)
P 位
– 存在位,表示当前条目是否在物理内存中
R/W 位
– 读写权限位,为 0 表示只读,为 1 表示可读写
U/S 位
– 也称为权限位,页或一组页的特权级,为 0 表示系统级,对应 CPL 0、1、2,为 1 表示用户级,对应 CPL 3。
PWT
– 页表缓冲写入机制,为 0 表示 write-back 模式,更新页表缓冲区时,只标记为已更新,不同步写内存,只有被新进入的数据取代时才更新到内存,为 1 表示 write-through 模式,更新页表缓冲区时,同步写内存,保证缓冲区与内存一致
PCD
– 是否拒绝被缓冲,为 0 表示可以被缓冲,为 1 表示不可以被缓冲
A 位
– 是否被访问,CPU 会在访问到页面时将该位置 1,但不会清除,只有软件可以将 A 位复位
D 位
– 是否被写入,CPU 会在写入页面时将该位置 1,但不会清除,只有软件可以将 D 位复位
PS
– PDE特有,页大小位,为 0 表示页大小为 4KB,且 PDE 指向页表,为 1 表示页大小为 4MB,且 PDE 指向 4MB 的整块内存
PAT
– 奔腾3以后的 CPU 引入的页属性表标识位,为 1 开启页属性表后,通过一系列专用寄存器(MBR)为每个页提供了详细的属性设置
G 位
– 全局位,也称为脏位,如果该位与 CR4 寄存器的 PGE 位同时被置为 1,则该页或页目录项将不会在 TLB 中被逐出
20bits 基地址
– PDE 与 PTE 的高 20bits 都是下级页基址,无论是页目录表还是页表还是在内存中的页,他们都是 4KB 对齐的,也就是说他们的首地址低12位均为0,这样,只需要通过 20bits 的基地址 * 12 就可以得到计算后的 32 位物理地址了
10-10-12分页的补充
为什么要按10-10-12分页:
- 一个物理页的大小为4096字节,即2的12次方,若要遍历整个物理页,则需要12个比特位
- 一个页表有1024个页表项,1024等于2的十次方,即需要10个比特位
- 页目录表项同理,也需要10个比特位
实验
证明PTE特征
PTE可以指向一个物理页,也可以不指向物理页
可以发现有许多页表项都为0,没有指向任何物理页
实验2
通过修改页表使C语言能在0地址处读写
编译如下代码
#include <iostream>
#include <windows.h>
int main(int argc, char* argv[])
{
int x = 1;
printf("x的地址:%x\n", &x);
getchar();
// 向0地址写入数据
*(int*)0 = 123;
// 从0地址读出数据
printf("0地址的数据:%x\n", *(int*)0);
getchar();
return 0;
}
运行
得到地址后中断进入windbg
使用WinDbg将虚拟机中断,将变量x所在的物理页挂载到线性地址0的PTE
拆分
0012ff3c
0000 0000 0001 0010 1111 1111 0011 1100
0000 0000 00 //0
01 0010 1111 //0x12F
1111 0011 1100 //0xF3C
修改
返回继续运行
成功对0地址进行了读写
由此可见,0地址是否能读写和其他无关只与PDE 和 PTE有关
实验3
通过修改物理页属性使字符串常量可修改
编译如下代码
#include <iostream>
#include <windows.h>
int main(int argc, char* argv[])
{
char *str = (char*)"Hello World";
printf("线性地址:%x\n", str);
getchar(); // 让程序执行到这里
//修改只读变量
str[0] = 'M';
printf("修改后的值:%s\n", str);
getchar();
}
打开x32dbg,我们可以看到
去内存布局里寻找字符串位置,发现字符串再.rdata段,而rdata段只有可读属性,并没有可写属性
如果继续执行到修改的时候,就会产生一个0xc0000005异常
我们再次运行程序
得到str地址,进入windbg中断,得到Cr3
拆分地址
00413160
0000 0000 0100 0001 0011 0001 0110 0000
0000 0000 01 //1
00 0001 0011 //0x13
0001 0110 0000 //0x160
拆分
0x2277E005
0010 0010 0111 0111 1110 0000 0000 0101
从上面的图片可知道PTE的第1位是R/W 位
,所以修改后为
0x2277E007
0010 0010 0111 0111 1110 0000 0000 0111
然后继续运行程序
成功修改str,所以可以看出内存的读写只与PDE 和 PTE有关于其他无关,这里可以看出一个有意思的东西,就是PTE虽然有R/W位
但是没有标记内存是否可以执行,其实对于CPU来说任何它能识别的code都是可以执行的,但是为什么再程序里却有些内存有执行属性有些内存却没有,这里有兴趣的可以自行拓展一下
实验4
通过修改物理页属性使普通用户读取高2G内存
编译如下代码
#include <iostream>
#include <windows.h>
int main(int argc, char* argv[])
{
PDWORD p = (PDWORD)0x84279a7a;
getchar(); // 让程序运行到这里
printf("读取高2G内存:%x \n", *p);
getchar();
return 0;
}
再次启动程序,运行,中断进入windbg,查看cr3
拆分
0x84279a7a
1000 0100 0010 0111 1001 1010 0111 1010
1000 0100 00 //0x210
10 0111 1001 //0x279
1010 0111 1010 //0xA7A
然后修改PDE与PTE的U/S位
通过上图我们可以知道U/S位
在第二位
拆分
0x001C3063
000111000011000001100011
0x4279125
0100001001111001000100100101
修改后
0x001C3067
000111000011000001100111
0x4279125
0100001001111001000100100101
返回继续执行程序
成功读取高2G内存,由此可见读取的权限只与PDE 和 PTE有关,与其他无关