Skip to content
She11code Blog
Go back

初探 Windows 下的现代内存寻址

Edit page

本文介绍 x64 架构 Windows 系统中虚拟地址转换为物理地址的核心机制。

Table of contents

Open Table of contents

简介

简单说一下 x64 架构的 Windows 系统虚拟地址转换为物理地址的逻辑。


基础概念

关键术语

术语说明
CR3存储当前 PML4 地址信息的寄存器
PML4Page Map Level 4,第四级页映射表
PDPTPage Directory Pointer Table,页目录指针表
PDPage Directory,页目录
PTPage Table,页表

四级分页结构

64 位的 Windows 系统使用了长模式内存寻址方案,也就是 4 级分页:

PML4 → PDPT → PD → PT → Physical Address

每个页表包含 512 个条目(Entry),从 PT 开始计算:

层级管理范围
PT4 KB
PD512 × 4 KB = 2 MB
PDPT512 × 2 MB = 1 GB
PML4512 × 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 结构

PDPTPML4 一样,由 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


参考链接


Edit page
Share this post on:

Next Post
银狐木马样本分析