保护模式(六)| 页表基址与 2-9-9-12 分页
# 前言
上一篇我们学习了 10-10-12 分页机制和 PDE/PTE。本篇将深入讲解页目录表基址、页表基址,以及 2-9-9-12(PAE)分页机制。
# 页目录表基址与页表基址
如果想填充 PDE 与 PTE,必须能够访问 PDT 与 PT。这就引出两个问题:
- 一定存在某个线性地址能够访问 PDT 与 PT(并且已经挂好了 PDE 与 PTE),如何找到这个地址?
- 这个为我们挂好 PDE 与 PTE 的 "人" 是谁?
注意:Cr3 中存储的是物理地址,不能在程序中直接读取。如果想读取,也要把 Cr3 的值挂到 PDT 和 PT 中才能访问。
# 高地址内核空间的设计
在 x86 的虚拟地址空间(4GB)中:
- 用户空间:
0x00000000~0x7FFFFFFF - 内核空间:
0x80000000~0xFFFFFFFF
Windows 为页表和页目录表保留了线性地址 0xC0000000 到 0xC03FFFFF 的 4MB 空间。
最关键的设计:页目录表的第 0x300 项指向页目录表本身。
因此:
- 访问线性地址
0xC0000000→ 可以访问到自己的页表 - 访问线性地址
0xC0300000→ 可以访问到自己的页目录表
# 页目录表基址
每个页表管理的大小为 4MB:
故得:
1 | 页表 1 映射的线性地址范围:0x00000000 ~ 0x003FFFFF |
因此,页表在 Cr3 中的索引: 0xC0000000 / 4MB = 768 = 0x300 。
每个页表可以映射 4MB 的虚拟地址空间(1024 个 PTE × 4KB / 页 = 4MB),要映射 0xC0000000 (3GB)的地址空间,需要 0xC0000000 / 0x400000 = 0x300 个页表。
将 0xC0300000 拆分为 10-10-12 分组:
1 | 0xC0300000 |
- PDI =
0x300:Cr3 的第 0x300 项指向页表区,但保存的是页目录表的基地址 - PTI =
0x300:页表的第 0x300 项(其实还是页目录表的第 0x300 项)又指向了页目录表本身 - Offset =
0x0:加上偏移量,就可以访问页目录表项
因此,页目录表的线性地址范围为 0xC0300000 ~ 0xC0300FFF 。
# 实验 1:理解页目录表基址
一、选择任意进程,获得它的 Cr3。
1 | !process 0 0 |
二、查看进程的页目录表(PDT)。
三、查看 0xC0300000 对应的物理页。
拆分线性地址:
1 | 0xC0300000 |
查看页目录表项:
查看页表项:
查看物理页:
# 页表基址
将 0xC0000000 拆分为 10-10-12 分组:
1 | 0xC0000000 |
- PDI =
0x300:Cr3 的第 0x300 项指向页表区 - PTI =
0x0:页表的第 0 项(即页目录表的第 0 项)指向第 1 张页表 - Offset =
0x0:偏移量
将 0xC0001000 拆分:
1 | 0xC0001000 |
PTI = 0x1 → 页目录表的第 1 项指向第 2 张页表。
以此类推,页表的线性地址范围为 0xC0000000 ~ 0xC02FFFFF 。
# 实验 2:理解页表基址
一、选择任意进程,获得它的 Cr3。
1 | !process 0 0 |
二、查看进程的页目录表(PDT)。
三、查看 0xC0000000 对应的物理页。
拆分线性地址:
1 | 0xC0000000 |
查看页目录表项:
查看页表项:
查看物理页:
# 总结
-
有了
0xC0300000和0xC0000000,就掌握了一个进程所有的物理内存读写权限。 -
公式总结:
- 访问页目录表:
0xC0300000 + PDI × 4 - 访问页表:
0xC0000000 + PDI × 4096 + PTI × 4 - 通过线性地址定位所在页表:
0xC0000000 + (addr >> 10)
- 访问页目录表:
# 2-9-9-12 分页
# 10-10-12 分页的设计思路回顾
-
Intel 认为一张页的大小为 4KB 是比较合理的,所以页大小确定为 4KB(2^12),最后 12 位确定为页内偏移。
-
当初的物理内存比较小,4 个字节的页表项(PTE)就够了,加上页的大小是 4KB,所以一个页能存储 1024 个 PTE(2^10),第二个 10 也就确定了。
-
同理,PDT 也需要 10 个比特位,10 + 10 + 12 刚好 32 位。
在这种分页方式下,物理地址最多可达 4GB。但随着硬件的发展,4GB 已经无法满足需求。Intel 在 1996 年设计了新的分页方式 ——2-9-9-12 分页,又称 PAE(Physical Address Extension,物理地址扩展)分页。
# 2-9-9-12 分页的设计思路
-
页大小确定为 4KB 不变,所以 12 位页内偏移不变。
-
为了增大物理内存的访问范围,需要增大 PTE。考虑对齐因素,PTE 增加到 8 个字节。由于页表大小不变(4KB),每张页表能容纳的 PTE 由 1024 个减少到 512 个(2^9),因此 PTI = 9。
-
同理 PDI = 9。
-
还剩前 2 位未分配。与 10-10-12 不同,Cr3 不直接指向 PDT 表,而是指向一张新的表 ——PDPT(Page-Directory-Pointer Table,页目录指针表)。PDPT 表中的每个成员叫做 PDPTE,每项 8 个字节。PDPT 表只有 4 个成员(2 位只能表示 4 种情况:
00 01 10 11)。
# PDPTE(页目录指针表项)
| 属性位 | 含义 |
|---|---|
| 第 0 位 | P 位,有效位 |
| 第 9-11 位 | 供操作系统软件使用,CPU 不使用 |
| Address of page table | 指向 PDT 表地址,共 24 位,后 12 位补 0 |
# PDE
| 属性位 | 含义 |
|---|---|
| PS | 页大小。 PS=1:页大小为 2MB(第 35~21 位表示大页物理地址,低 21 位为 0) PS=0:页大小为 4KB(第 35~12 位表示页表基址,低 12 位补 0) |
| PAT | 页属性表。仅当 PS=1 时有意义 |
# PTE
| 属性位 | 含义 |
|---|---|
| Address of 4KB page frame | 物理页基址,低 12 位补 0。 物理页基址 + 12 位页内偏移 → 具体数据 |
# XD/NX 标志位
- 在 PAE 分页模式下,PDE 与 PTE 的最高位为 XD/NX 位。
- Intel 中称为 XD,AMD 中称为 NX(No eXecute)。
- 段的属性包含可读、可写和可执行;页的属性包含可读、可写。
当 RET 执行返回时,如果把堆栈里的数据指向一段提前准备好的数据,然后通过栈溢出等方法使程序返回到堆栈中,就会产生任意代码执行的后果。Intel 为此设置了硬件保护 ——XD/NX 位。当 XD=1 时,即使 EIP 跳到了 "数据区",也不可以执行。
# 环境配置
将 C:\boot.ini 中的 execute 改为 noexecute ,重启即可切换到 2-9-9-12 分页模式。
# 实验:理解 2-9-9-12 分页
一、新建一个记事本,写入 “Hello World”。
二、使用 Cheat Engine 附加进程。
三、找到 “Hello World” 的线性地址。
确认是否为真实地址:
四、将最后一个字符 ‘d’ 改为 ‘m’,观察 CE 中的值是否变化。
线性地址最终确定为: 000AB2C0 。
五、将线性地址拆分为 2-9-9-12 四组。
1 | 0x000AB2C0 |
六、获得进程的 Cr3。
1 | !process 0 0 |
七、通过 Cr3 找到字符串的物理地址。
第一层:PDPT 表
第二层:PDT 表
第三层:PT(页表)
第四层:物理页
以字符形式查看数据:
上一篇:保护模式(五)| 10-10-12 分页与 PDE/PTE
下一篇:保护模式(七)| TLB、中断异常与控制寄存器