本文将详细探讨 Windows x64 系统的分页机制,测试环境如下:
操作系统 | Windows 10 x64 22H2(2024 年 11 月更新) |
调试软件 | WinDbg、VMware Workstation Pro |
分页模式概述
分页模式是现代 x86-64 架构处理器的核心机制,用于将虚拟地址映射到物理地址。目前大多数基于 x86-64 架构的处理器支持的物理地址位宽通常为 46 位(64 TB 寻址空间),而理论上最大支持 48 位(256 TB)。
虚拟地址空间则通过分页机制进行管理,目前主流实现使用 48 位虚拟地址空间(9-9-9-9-12 分页结构),并支持在 5 层分页表(Page Map Level 5)的硬件和操作系统上扩展到 57 位(128 PB 寻址空间)。
目前 Windows x64 系统最大支持 48 位虚拟地址空间(基于 PML4)和最多 46 位物理地址空间(64 TB)。
分页结构表
虚拟地址结构
在 64 位模式下,段寄存器的基地址通常为 0,因此程序中的虚拟地址与线性地址几乎相同。
页表名称 | 解析位数 |
---|---|
PML5(Page Map Level 5) | 9(56 位 ~ 48 位) |
PML4(Page Map Level 4) | 9(47 位 ~ 39 位) |
PDPT(Page Directory Pointer Table) | 9(38 位 ~ 30 位) |
PD(Page Directory) | 9(29 位 ~ 21 位) |
PT(Page Table) | 9(20 位 ~ 12 位) |
Offset(页内偏移) | 12(11 位 ~ 0 位) |
CR3 寄存器通常指向最高层页表的物理地址,其具体结构如下:
- CR3 指向 PML5 页表的物理基地址,采用 9-9-9-9-9-12 分页结构。
- CR3 指向 PML4 页表的物理基地址,采用 9-9-9-9-12 分页结构。
由于物理页的大小为 4 KB,我们可以计算得到以下结论:
- PML5:每个页表包含
= 512 个项,每项占用 4 KB / 512 = 8 字节。 - PML4:每个页表包含
= 512 个项,每项占用 4 KB / 512 = 8 字节。 - PDPT:每个页表包含
= 512 个项,每项占用 4 KB / 512 = 8 字节。 - PD:每个页表包含
= 512 个项,每项占用 4 KB / 512 = 8 字节。 - PT:每个页表包含
= 512 个项,每项占用 4 KB / 512 = 8 字节。 - Offset: 页内偏移部分为
= 4 KB,可以完整索引单个物理页的范围。
线性地址的索引和偏移计算
-
提取线性地址的各级索引和页内偏移
// 56 bit ~ 48 bit PML5E_index = linear_address >> 48 & 0x1FF; // 47 bit ~ 39 bit PML4E_index = linear_address >> 39 & 0x1FF; // 38 bit ~ 30 bit PDPTE_index = linear_address >> 30 & 0x1FF; // 29 bit ~ 21 bit PDE_index = linear_address >> 21 & 0x1FF; // 20 bit ~ 12 bit PTE_index = linear_address >> 12 & 0x1FF; // 11 bit ~ 0 bit Offset(页内偏移) = linear_address & 0xFFF;
-
索引与页表项的大小相乘计算出表项偏移
PML5E_offset = PML5E_index x 8; PML4E_offset = PML4E_index x 8; PDPTE_offset = PDPTE_index x 8; PDE_offset = PDE_index x 8; PTE_offset = PTE_index x 8;
Windows 支持的页面大小
-
4 KB 小页:最常见的页面大小。
-
2 MB 中页:页目录表(PD)的 PS 位为 1 时,跳过页表(PT),直接指向 2 MB 物理页。
将页目录表项(PDE)的低 21 位清零,再加上线性地址的低 21 位偏移量,得到物理地址:
// 高位 (PDE & 0xFFFFFFFE00000) 表示 2 MB 物理页的基地址 // 低位 (线性地址 & 0x1FFFFF) 用于页内偏移 // 寻址范围: 2^21 = 2 MB 物理地址 = (PDE & 0xFFFFFFE00000 + 线性地址 & 0x1FFFFF)
-
1 GB 大页:页目录指针表(PDPT)的 PS 位为 1 时,跳过页目录表(PD)和页表(PT),直接指向 1 GB 物理页。
将页目录指针表项(PDPTE)的低 30 位清零,再加上线性地址的低 30 位偏移量,得到物理地址:
// 高位 (PDPTE & 0xFFFFC0000000) 表示 1 GB 物理页的基地址 // 低位 (线性地址 & 0x3FFFFFFF) 用于页内偏移 // 寻址范围: 2^30 = 1 GB 物理地址 = (PDPTE & 0xFFFFC0000000 + 线性地址 & 0x3FFFFFFF)
从虚拟地址到物理地址
我们首先编写一个简单的 C 语言程序,用于打印一个虚拟地址。代码如下:
// TestProject.exe
#include <stdio.h>
#include <Windows.h>
int main(int argc, char* argv[])
{
int value = 0x12345678;
printf("Virtual Address: %p\n", &value);
system("pause");
return 0;
}
运行程序后,输出示例如下:
Virtual Address: 0x000000E9700FFBE4
解析上述虚拟地址对应的线性地址
PML4E_index = 0xE9700FFBE4 >> 39 & 0x1FF = 0x1
PDPTE_index = 0xE9700FFBE4 >> 30 & 0x1FF = 0x1A5
PDE_index = 0xE9700FFBE4 >> 21 & 0x1FF = 0x180
PTE_index = 0xE9700FFBE4 >> 12 & 0x1FF = 0xFF
Offset = 0xE9700FFBE4 & 0xFFF = 0xBE4
索引与每级页表项的大小相乘计算出偏移
PML4E_offset = 0x1 x 8 = 0x8
PDPTE_offset = 0x1A5 x 8 = 0xD28
PDE_offset = 0x180 x 8 = 0xC00
PTE_offset = 0xFF x 8 = 0x7F8
我们利用 WinDbg 调试工具,获取当前程序的 CR3 寄存器。
kd> !process 0 0
PROCESS ffffa8839c77f080
SessionId: 1 Cid: 0d48 Peb: e96ff2e000 ParentCid: 1170
DirBase: 12e6bc000 ObjectTable: ffffcd8108554980 HandleCount: 53
Image: TestProject.exe
DirBase 指向 4 层分页表(PML4)的物理地址,DirBase 类似于 CR3 但不完全相同。
kd> !dq 12e6bc000
#12e6bc000 0a000000`33ae4867 0a000001`1dad1867
#12e6bc010 00000000`00000000 00000000`00000000
#12e6bc020 0a000000`057d7867 00000000`00000000
#12e6bc030 00000000`00000000 00000000`00000000
PML4 加上表项偏移得到 PML4E,获取页目录指针表(PDPT)
// PML4E = 0x12e6bc000 + 0x8 = 0x12e6bc008
kd> !dq 0x12e6bc008 L1
#12e6bc008 0a000001`1dad1867 // 页目录指针表(PDPT)
PDPT 加上表项偏移得到 PDPTE,获取页目录表(PD)
// PDPTE = 0x0a000001`1dad1867 & 0xfffffffff000 + 0xd28 = 0x11dad1d28
kd> !dq 0x11dad1d28 L1
#11dad1d28 0a000000`a16d2867 // 页目录表(PD)
PD 加上表项偏移得到 PDE,获取页表(PT)
// PDE = 0x0a000000`a16d2867 & 0xfffffffff000 + 0xc00 = 0xa16d2c00
kd> !dq 0xa16d2c00 L1
#a16d2c00 0a000001`22fdd867 // 页表(PT)
PT 加上表项偏移得到 PTE,获取物理页(4 KB Page)
// PTE = 0x0a000001`22fdd867 & 0xfffffffff000 + 0x7f8 = 0x122fdd7f8
kd> !dq 0x122fdd7f8 L1
#122fdd7f8 81000000`313e2847 // 物理页(4 KB Page)
物理页加上 Offset(页内偏移)得到物理地址
// 物理地址 = 0x81000000`313e2847 & 0xfffffffff000 + 0xbe4 = 0x313e2be4
// int value = 0x12345678;
kd> !db 0x313e2be4
#313e2be4 78 56 34 12 cc cc cc cc-cc cc cc cc cc cc cc cc xV4.............
#313e2bf4 cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc ................