本文介绍 x64 架构 Windows 系统中虚拟地址转换为物理地址的核心机制。
Table of contents
Open Table of contents
简介
简单说一下 x64 架构的 Windows 系统虚拟地址转换为物理地址的逻辑。
基础概念
关键术语
| 术语 | 说明 |
|---|---|
| CR3 | 存储当前 PML4 地址信息的寄存器 |
| PML4 | Page Map Level 4,第四级页映射表 |
| PDPT | Page Directory Pointer Table,页目录指针表 |
| PD | Page Directory,页目录 |
| PT | Page Table,页表 |
四级分页结构
64 位的 Windows 系统使用了长模式内存寻址方案,也就是 4 级分页:
PML4 → PDPT → PD → PT → Physical Address
每个页表包含 512 个条目(Entry),从 PT 开始计算:
| 层级 | 管理范围 |
|---|---|
| PT | 4 KB |
| PD | 512 × 4 KB = 2 MB |
| PDPT | 512 × 2 MB = 1 GB |
| PML4 | 512 × 1 GB = 256 TB |
虚拟地址结构
虚拟地址 64 位中有 48 位用于寻址,高位没有实际使用:

数据结构定义:
typedef union _virt_addr_t
{
void* value;
struct
{
std::uint64_t offset : 12; // 页内偏移
std::uint64_t pt_index : 9; // PT 索引
std::uint64_t pd_index : 9; // PD 索引
std::uint64_t pdpt_index : 9; // PDPT 索引
std::uint64_t pml4_index : 9; // PML4 索引
std::uint64_t reserved : 16; // 保留位
};
} virt_addr_t, *pvirt_addr_t;
地址翻译流程
整个寻址逻辑是:先从 CR3 中拿到当前页表的基地址,根据索引找到表中的具体条目,从这个条目里获取下一级页表的基地址。这个过程像接力一样重复 4 次,最终找到物理内存地址。
PML4(Page Map Level 4)
CR3 寄存器
CR3 是一切寻址的起点。通过 _readcr3() 获取 CR3 的值,该值存储了当前进程 PML4 的物理基地址。
typedef union _cr3
{
std::uint64_t flags;
struct
{
std::uint64_t reserved1 : 3;
std::uint64_t page_level_write_through : 1;
std::uint64_t page_level_cache_disable : 1;
std::uint64_t reserved2 : 7;
std::uint64_t dirbase : 36; // PML4 物理基地址(PFN)
std::uint64_t reserved3 : 16;
};
} cr3;
为什么只有 36 位? 因为物理内存页面以 4KB 对齐,低 12 位永远为 0,不需要存储。使用时将 PFN 左移 12 位即可得到真实物理地址。
PML4E 结构
PML4 表是一个占用 4096 字节(4KB)的物理内存页,由 512 个 PML4E 组成,每个 PML4E 占用 8 字节(64 位)。
typedef union _pml4e
{
std::uint64_t value;
struct
{
std::uint64_t present : 1; // 必须为 1,否则区域无效
std::uint64_t rw : 1; // 0 = 只读,1 = 可写
std::uint64_t user_supervisor : 1; // 0 = 仅内核访问
std::uint64_t page_write_through : 1;
std::uint64_t page_cache : 1;
std::uint64_t accessed : 1; // 是否被访问过
std::uint64_t Ignored1 : 1;
std::uint64_t page_size : 1; // PML4E 必须为 0
std::uint64_t Ignored2 : 4;
std::uint64_t pfn : 36; // PDPT 的页帧号
std::uint64_t reserved : 4;
std::uint64_t Ignored3 : 11;
std::uint64_t nx : 1; // 1 = 禁止执行
};
} pml4e, *ppml4e;
计算公式
目标 PML4E 地址 = PML4 基地址 + (PML4 索引 × 8)
获取 pfn 后,左移 12 位得到 PDPT 基地址。
PDPT(Page Directory Pointer Table)
PDPTE 结构
PDPT 和 PML4 一样,由 512 个 PDPTE 组成:
typedef union _pdpte
{
std::uint64_t value;
struct
{
std::uint64_t present : 1;
std::uint64_t rw : 1;
std::uint64_t user_supervisor : 1;
std::uint64_t page_write : 1;
std::uint64_t page_cache : 1;
std::uint64_t accessed : 1;
std::uint64_t Ignored1 : 1;
std::uint64_t page_size : 1; // 1 = 映射 1GB 大页
std::uint64_t Ignored2 : 4;
std::uint64_t pfn : 36; // PD 的页帧号
std::uint64_t reserved : 4;
std::uint64_t Ignored3 : 11;
std::uint64_t nx : 1;
};
} pdpte, *ppdpte;
计算公式
目标 PDPTE 地址 = PDPT 基地址 + (PDPT 索引 × 8)
PD(Page Directory)
PDE 结构
typedef union _pde
{
std::uint64_t value;
struct
{
std::uint64_t present : 1;
std::uint64_t rw : 1;
std::uint64_t user_supervisor : 1;
std::uint64_t page_write : 1;
std::uint64_t page_cache : 1;
std::uint64_t accessed : 1;
std::uint64_t Ignored1 : 1;
std::uint64_t page_size : 1; // 1 = 映射 2MB 大页
std::uint64_t Ignored2 : 4;
std::uint64_t pfn : 36; // PT 的页帧号
std::uint64_t reserved : 4;
std::uint64_t Ignored3 : 11;
std::uint64_t nx : 1;
};
} pde, *ppde;
计算公式
目标 PDE 地址 = PD 基地址 + (PD 索引 × 8)
PT(Page Table)
到 PT 已经是最后一级页表了。
PTE 结构
typedef union _pte
{
std::uint64_t value;
struct
{
std::uint64_t present : 1;
std::uint64_t rw : 1;
std::uint64_t user_supervisor : 1;
std::uint64_t page_write : 1;
std::uint64_t page_cache : 1;
std::uint64_t accessed : 1;
std::uint64_t dirty : 1; // 是否被写入过
std::uint64_t page_access_type : 1;
std::uint64_t global : 1; // 全局页
std::uint64_t ignored2 : 3;
std::uint64_t pfn : 36; // 物理页帧号
std::uint64_t reserved : 4;
std::uint64_t ignored3 : 7;
std::uint64_t protect_key : 4; // 保护密钥
std::uint64_t nx : 1;
};
} pte, *ppte;
计算公式
目标 PTE 地址 = PT 基地址 + (PT 索引 × 8)
最终物理地址
从 PTE 获取的 pfn 左移 12 位后,加上虚拟地址的低 12 位偏移,得到最终物理地址:
物理地址 = (PTE.pfn << 12) + 虚拟地址.offset
完整翻译流程总结
1. 从 CR3 获取 PML4 基地址
2. PML4 基地址 + (PML4索引 × 8) → PML4E → 获取 PDPT 基地址
3. PDPT 基地址 + (PDPT索引 × 8) → PDPTE → 获取 PD 基地址
4. PD 基地址 + (PD索引 × 8) → PDE → 获取 PT 基地址
5. PT 基地址 + (PT索引 × 8) → PTE → 获取物理页帧号
6. (PFN << 12) + offset → 最终物理地址
演示代码
TODO