# 前言

上一篇我们学习了中断门、陷阱门和任务段。本篇将深入介绍 10-10-12 分页机制,以及 PDE 和 PTE 的结构与实验。

# 10-10-12 分页

10-10-12 分页是一种内存分页机制,将 32 位虚拟地址分为三个部分:10 位页目录索引(PDI)、10 位页表索引(PTI)、12 位页内偏移,通过两级页表(页目录表 + 页表)实现地址映射。

# 4GB 内存空间

每个程序运行时,操作系统都会为其分配一段 4GB 的内存空间。

但电脑的物理内存可能不够为每个进程都分配 4GB。实际上,进程被分配到的 "4GB 内存空间" 只是虚拟的内存空间,并不是真正的物理内存,虚拟内存与物理内存之间有一层转换关系。

# 有效地址 / 线性地址 / 物理地址

# 有效地址与线性地址

例如: mov eax, dword ptr ds:[0x12345678]

其中, 0x12345678有效地址ds.Base + 0x12345678线性地址

注意:当段寄存器的 Base 为 0 时,有效地址 = 线性地址(大多数时候都是如此)。但也有特殊情况,比如 FS 段寄存器的 Base 不为 0。

# 物理地址

系统 DLL(动态链接库)存在于物理地址中。当程序想要调用某个 DLL 时,DLL 便会映射一份线性地址给程序,程序通过线性地址找到 DLL 的物理地址。

# Cr3(控制寄存器)

每个进程都有一个 Cr3 的值(Cr3 本身是一个寄存器,一个核只有一套寄存器)。

# 实验:通过线性地址找到物理地址

一、将 XP 虚拟机设置为 10-10-12 分页模式。

右键 → 我的电脑 → 属性 → 高级。

/noexecute 改为 /execute ,保存,重启。

二、新建一个记事本,写入 “Hello World”。

三、使用 Cheat Engine 附加进程。

四、找到 “Hello World” 的线性地址。

修改记事本中的字符来确认真正的线性地址:

线性地址最终确定为: 000AB2C0

五、将线性地址拆分为 10-10-12 三组。

1
2
3
4
0x000AB2C0
= 0000 0000 0000 1010 1011 0010 1100 0000
= 0000000000 0010101011 001011000000
PDI = 0x0 PTI = 0xAB Offset = 0x2C0

六、在 WinDbg 获取进程的 Cr3。

命令: !process 0 0

DirBase 就是 Cr3。

七、通过 Cr3 找到字符串的物理地址。

注意:

  1. 找第一层和第二层时要将索引乘以 4(每个地址占 4 个字节)
  2. 每找到一层都要将地址的低 12 位(属性位) 清零再继续找下一层

第一层(PDE):

第二层(PTE):

第三层(物理页):

以字节形式查看:

# PDE 与 PTE

# Cr3

  1. 在所有寄存器中,只有 Cr3 存储的是物理地址,其他寄存器存的都是线性地址
  2. Cr3 所存储的物理地址指向了一个 PDT(Page Directory Table,页目录表)
  3. 在 10-10-12 分页模式中,一个页的大小为 4KB,即一个页可以存储 1024 个页目录表项(PDE)。

物理页结构图:

# PDE(Page Directory Entry,页目录表项)

  1. 页目录表(PDT) 的每一项元素称为页目录表项(PDE)
  2. 每个 PDE 指向一个页表(PT,Page Table)
  3. 每个页表的大小为 4KB,即一个页表可以存储 1024 个页表项(PTE)

# PTE(Page Table Entry,页表项)

  1. 页表的每一个元素称为页表项(PTE)
  2. PTE 指向的内存才是真正的物理页

特征:

  1. PTE 可以指向一个物理页,也可以不指向物理页。
  2. 多个 PTE 可以指向同一个物理页

# 物理页的属性

物理页的属性 = PDE 属性 & PTE 属性

属性位 含义
P(第 0 位) 有效位。P=1:有效;P=0:无效
R/W 读写位。R/W=0:只读;R/W=1:可读可写
U/S 权限位。U/S=0:特权用户;U/S=1:普通用户
PS 页大小。PDE 特有,位于第 7 位。
PS=0:PDE 指向页表
PS=1:PDE 直接指向物理页(低 22 位 = 页内偏移,最大 4MB,俗称 "大页")
A 访问位。A=1:已被访问过;A=0:未被访问过
D 脏位。D=1:已被写过;D=0:未被写过

其他属性位需等学完控制寄存器TLB 才能讲解。

# 补充:为什么按 10-10-12 分页

  1. 一个物理页的大小为 4096 字节(2^12),遍历整个物理页需要 12 个比特位。
  2. 一个页表有 1024 个页表项(2^10),需要 10 个比特位。
  3. 页目录表项同理,也需要 10 个比特位。

# 实验 1:证明 PTE 可以不指向物理页

一、选择任意进程的 Cr3 作为目标,例如 notepad.exe

二、查看部分页表项,可以发现有许多页表项都为 0,没有指向任何物理页。

# 实验 2:通过修改页表使 C 语言能在 0 地址处读写

一、编译并运行以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#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;
}

运行后,首先输出 x 的地址:

二、使用 WinDbg 将虚拟机中断,将变量 x 的地址挂载到 PTE 为 0 的物理页上。

1
2
3
4
0x12ff7c
= 0000 0000 0001 0010 1111 1111 0111 1100
= 0000000000 0100101111 111101111100
PDI = 0x0 PTI = 0x12F Offset = 0xF7C

三、继续运行程序,可以看到成功对 0 地址进行了读写。

# 实验 3:通过修改物理页属性使字符串常量可修改

一、编译并运行以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[])
{
char *str = "Hello World";

printf("线性地址:%x", str);

getchar(); // 让程序执行到这里

// 修改只读变量
str[0] = 'M';

printf("修改后的值:%s\n", str);

getchar();

return 0;
}

运行后得到 str 的地址:

二、修改 str 所在 PTE 的属性,将 R/W 位设置为 1。

1
2
3
4
0x40e11c
= 0000 0000 0100 0000 1110 0001 0001 1100
= 0000000001 0000001110 000100011100
PDI = 0x1 PTI = 0xE Offset = 0x11C

三、继续运行程序,成功修改了字符串常量。

# 实验 4:通过修改物理页属性使普通用户能够读取高 2G 内存

一、编译并运行以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[])
{
PDWORD p = (PDWORD)0x8003F00C;

getchar(); // 让程序运行到这里

printf("读取高2G内存:%x \n", *p);

getchar();

return 0;
}

二、修改指针 p 保存的线性地址所指向的 PDE 和 PTE 的 U/S 位为 1。

1
2
3
4
0x8003f00c
= 1000 0000 0000 0011 1111 0000 0000 1100
= 1000000000 0000111111 000000001100
PDI = 0x200 PTI = 0x3F Offset = 0xC

修改 PDE 的 U/S 位:

修改 PTE 的 U/S 位:

三、继续运行程序,成功读取了高 2G 内存。


上一篇:保护模式(四)| 中断门、陷阱门与任务段
下一篇:保护模式(六)| 页表基址与 2-9-9-12 分页