知行LEARNING HANDBOOK
目录 · N08 x86-64 地址翻译:从虚拟地址到物理地址的四级页表遍历
学习手册/计算机基础/虚拟内存与页表
N0815 分钟更新于 2026-08-20

x86-64 地址翻译:从虚拟地址到物理地址的四级页表遍历

用一个完整例子追踪虚拟地址经过 TLB、CR3 和四级页表到达物理地址。

x86-64virtual-addresspage-walkCR3TLBPML4PTE

建议先了解

  • x86-64 页表项基本结构
  • 二进制位切分
  • 4 KiB 页与地址对齐

1. 这篇笔记解决什么问题

这篇笔记完整追踪一次 x86-64 地址翻译:

Virtual Address
    ↓
TLB
    ↓ miss
CR3
    ↓
PML4
    ↓
PDPT
    ↓
PD
    ↓
PT
    ↓
Physical Page
    ↓ + page offset
Physical Address

重点不是只记住“四级页表”这个名词,而是能够真正回答:

  • 一个 64-bit 虚拟地址到底如何按 bit 被拆开?
  • 为什么每一级恰好取 9 bit?
  • CR3 中保存什么?
  • table_base + index × 8 为什么是页表遍历的核心公式?
  • 页表项自己的物理地址与页表项内部保存的物理地址有什么区别?
  • 最低 12 bit 为什么从头到尾不参加查表?
  • TLB 命中和 TLB miss 时路径有什么不同?
  • 4 KiB、2 MiB、1 GiB 页如何影响 page walk 的终止位置?

本文固定讨论:

  • x86-64 / IA-32e;
  • 4-level paging
  • 4 KiB page;
  • 每个 paging-structure entry 为 8 byte;
  • 不使用 huge page 的主示例。

核验说明:页表级次、地址位切分、CR3 顶级页表基址及 page-walk 规则按 Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Chapter 4AMD64 Architecture Programmer’s Manual Volume 2: System Programming 的架构定义整理。

2. 前置知识

需要先掌握:

待补充:如果要理解真实 CPU 为什么 page walk 没有“每次都访问四次 DRAM”那么慢,需要进一步学习 CPU cache、TLB 和 paging-structure cache。

3. 核心概念

3.1 虚拟地址与线性地址

本笔记沿用“虚拟地址(Virtual Address)”这一常用说法。

严格按 x86 文档术语,分页硬件翻译的是线性地址(Linear Address)。在 64-bit 模式的常见平坦地址模型下,两者在学习 page walk 时通常可以近似作为同一条地址来讨论;FS/GS base 等细节不在本篇展开。

3.2 Canonical Address

4-level paging 使用虚拟地址的低 48 bit。

地址并不是“高 16 bit 随便写”。

对于 48-bit canonical address:

bits 63:48

必须是:

bit 47

的符号扩展。

因此:

  • bit 47 = 0 → bits 63:48 必须全 0;
  • bit 47 = 1 → bits 63:48 必须全 1。

3.3 四级页表

4-level paging 的四级通常写作:

L4: PML4  (Page Map Level 4)
L3: PDPT  (Page Directory Pointer Table)
L2: PD    (Page Directory)
L1: PT    (Page Table)

对应 entry:

PML4E
PDPTE
PDE
PTE

3.4 CR3

CR3 是 x86 的控制寄存器之一。

在分页启用时,它提供当前地址翻译上下文的顶级页表物理基址信息。

在本文的简化 4-level 例子中:

CR3 → PML4 physical base

CR3 低位还可能与 PCID、PWT、PCD 等机制有关,具体取决于控制位配置。本例只取页表基址部分。

3.5 转换后备缓冲区(Translation Lookaside Buffer,TLB)

TLB 缓存近期的虚拟页到物理页翻译。

概念流程:

VA
 ↓
TLB lookup
 ├─ hit  → 直接得到 translation
 └─ miss → hardware page walk

因此四级页表遍历不是每次内存访问都必然发生。

3.6 Page Offset

4 KiB 页大小:

4 KiB=2124\text{ KiB}=2^{12}

所以虚拟地址最低 12 bit:

bits 11:0

表示页内偏移(page offset)。

这些 bit 不需要经过页表翻译,因为同一页内的偏移在虚拟页和目标物理页中保持不变。

4. 直观理解

四级页表可以看成一个 512 叉 radix tree

每一级页表:

4 KiB / 8 B = 512 entries = 2^9

所以每下降一级,就从虚拟地址中取 9 bit 作为下标。

virtual page number
       36 bits
          │
          ▼
   9 + 9 + 9 + 9
          │
          ▼
        L4
         ↓
        L3
         ↓
        L2
         ↓
        L1
         ↓
physical frame

这个类比的边界是:真实 CPU 还会进行权限检查、TLB/cache 查询、Accessed/Dirty 状态维护等,不只是单纯的树查找。

5. 工作原理

5.1 虚拟地址的 bit 切分

在 4-level、4 KiB page 模式下:

63           48 47       39 38       30 29       21 20       12 11        0
┌──────────────┬───────────┬───────────┬───────────┬───────────┬────────────┐
│ canonical    │ L4 index  │ L3 index  │ L2 index  │ L1 index  │   offset   │
│ extension    │   9 bit   │   9 bit   │   9 bit   │   9 bit   │   12 bit   │
└──────────────┴───────────┴───────────┴───────────┴───────────┴────────────┘

原因是:

48=9+9+9+9+1248=9+9+9+9+12

其中:

  • 4 组 9 bit:每级选择 512 个 entry 中的一个;
  • 12 bit:4 KiB 页内偏移。

5.2 每一级查表的核心公式

假设当前页表的物理基址是:

table_base

当前级 index 是:

index

一个 entry 为 8 byte,则 entry 自己的物理地址为:

entry_physical_address=table_base+index×8\text{entry\_physical\_address} = \text{table\_base} + \text{index}\times 8

CPU 读取该 8-byte entry 后,再从其中提取下一张页表的物理基址:

next_table_base=entry-address-field\text{next\_table\_base} = \text{entry-address-field}

不断重复,直到最终 PTE。

5.3 完整概念流程

  1. CPU 形成一个待访问的虚拟/线性地址。
  2. 检查地址是否满足 canonical 要求。
  3. 查询 TLB。
  4. 若 TLB hit,直接使用缓存 translation。
  5. 若 TLB miss,hardware page walker 从 CR3 指向的顶级页表开始。
  6. 用 bits 47:39 选中 PML4E。
  7. PML4E 指向下一层 PDPT。
  8. 用 bits 38:30 选中 PDPTE。
  9. 若该 PDPTE 是非叶子项,继续到 PD;若其 page-size 语义表示 1 GiB huge page,则提前终止。
  10. 用 bits 29:21 选中 PDE。
  11. 若 PDE 是非叶子项,继续到 PT;若其 page-size 语义表示 2 MiB huge page,则提前终止。
  12. 用 bits 20:12 选中 PTE。
  13. PTE 给出最终 4 KiB physical page frame。
  14. 将原虚拟地址的 bits 11:0 offset 拼到 physical page base 上。
  15. 得到最终 physical address。
  16. CPU 可将新的 translation 缓存进 TLB。

补充背景:真实 page walk 读取页表项时会利用处理器缓存体系及专门的 paging-structure caches,因此“走四级页表”不等于固定发生四次 DRAM 访问。

5.4 每一级都会检查 entry 状态

最基本的是:

P = 1

否则不能把该 entry 当作有效映射继续使用。

R/W、U/S、XD/NX 等权限还会参与最终访问权限判断。

补充背景:权限的有效结果不是只看最后一个 PTE;上层 paging-structure entries 也会约束访问。完整异常优先级和 page-fault error code 应单独学习。

6. 示例:完整走一遍 0x00007F123456789A

6.1 输入

设:

VA = 0x00007F123456789A

并假设:

CR3 page-table base = 0x0000000000100000

为了只突出地址计算,构造以下页表项:

PML4E = 0x0000000000200007
PDPTE = 0x0000000000300007
PDE   = 0x0000000000400007
PTE   = 0x0000000012345007

其中最低 0x7 表示这个简化例子里:

P  = 1
RW = 1
US = 1

这里故意没有在示例初值中置 A(Accessed)。真实硬件执行 page walk 时可能更新 Accessed 位,因此事后读取真实 entry 时不一定仍是这个数值。

6.2 Step 1:切分虚拟地址

0x00007F123456789A 可以按 4-level paging 分为:

bits 63:48   = canonical extension
bits 47:39   = L4 index
bits 38:30   = L3 index
bits 29:21   = L2 index
bits 20:12   = L1 index
bits 11:0    = offset

得到:

字段二进制十进制十六进制
L40111111102540xFE
L3001001000720x48
L21101000104180x1A2
L11011001113590x167
offset10001001101022020x89A

因此:

VA
=
[ L4=0xFE ]
[ L3=0x48 ]
[ L2=0x1A2 ]
[ L1=0x167 ]
[ offset=0x89A ]

6.3 Step 2:CR3 找到 L4 页表

假设:

PML4 physical base = 0x100000

L4 index:

0xFE = 254

一个 PML4E 为 8 byte,所以:

254×8=2032=0x7F0254\times8=2032=0x7F0

因此 PML4E 自己位于:

0x100000 + 0x7F0
= 0x1007F0

CPU 从物理地址:

0x1007F0

读取 8 byte,得到:

PML4E = 0x0000000000200007

去掉 flags 后:

next PDPT physical base = 0x200000

6.4 Step 3:L3 / PDPT

L3 index:

0x48 = 72

entry offset:

72×8=576=0x24072\times8=576=0x240

所以 PDPTE 自己位于:

0x200000 + 0x240
= 0x200240

读取:

PDPTE = 0x0000000000300007

本例不是 1 GiB huge page,因此继续向下。

得到:

PD physical base = 0x300000

6.5 Step 4:L2 / Page Directory

L2 index:

0x1A2 = 418

entry offset:

418×8=3344=0xD10418\times8=3344=0xD10

所以 PDE 自己位于:

0x300000 + 0xD10
= 0x300D10

读取:

PDE = 0x0000000000400007

本例不是 2 MiB huge page,因此继续向下。

得到:

PT physical base = 0x400000

6.6 Step 5:L1 / Page Table

L1 index:

0x167 = 359

entry offset:

359×8=2872=0xB38359\times8=2872=0xB38

所以 PTE 自己位于:

0x400000 + 0xB38
= 0x400B38

读取:

PTE = 0x0000000012345007

这一次地址字段不再指向下一张页表,而是:

physical page base = 0x12345000

6.7 Step 6:加回 page offset

原虚拟地址最低 12 bit:

offset = 0x89A

所以:

  0x12345000
+       0x89A
-------------
  0x1234589A

最终:

VA = 0x00007F123456789A
          ↓
PA = 0x000000001234589A

6.8 整条链

VA 0x00007F123456789A
│
├─ L4 index = 0xFE
│
│  CR3/PML4 base = 0x100000
│  entry @ 0x1007F0
│  PML4E = 0x200007
│
▼
PDPT base = 0x200000
│
├─ L3 index = 0x48
│  entry @ 0x200240
│  PDPTE = 0x300007
│
▼
PD base = 0x300000
│
├─ L2 index = 0x1A2
│  entry @ 0x300D10
│  PDE = 0x400007
│
▼
PT base = 0x400000
│
├─ L1 index = 0x167
│  entry @ 0x400B38
│  PTE = 0x12345007
│
▼
physical page base = 0x12345000
│
├─ offset = 0x89A
▼
PA = 0x1234589A

这个例子说明了整个 page walk 实际上在重复同一个动作:

table_base
    +
index × 8
    ↓
读取 entry
    ↓
取出 next table / physical page base

7. 代码、命令或公式

7.1 从虚拟地址提取四级索引

va = 0x00007F123456789A

l4 = (va >> 39) & 0x1FF
l3 = (va >> 30) & 0x1FF
l2 = (va >> 21) & 0x1FF
l1 = (va >> 12) & 0x1FF
offset = va & 0xFFF

print(hex(l4), hex(l3), hex(l2), hex(l1), hex(offset))

本次整理已验证,该输入得到:

0xfe 0x48 0x1a2 0x167 0x89a

位掩码:

0x1FF = 9 个二进制 1
0xFFF = 12 个二进制 1

7.2 每级 entry 地址

统一公式:

entry_pa=table_base+(index×8)\boxed{ \text{entry\_pa} = \text{table\_base} + (\text{index}\times8) }

7.3 最终物理地址

4 KiB page 下:

PA=physical_page_base+VA[11:0]\boxed{ \text{PA} = \text{physical\_page\_base} + \text{VA}[11:0] }

或者从 bit 角度:

PA = [ physical-page-number ][ original offset ]

8. 容易混淆的概念

概念 A概念 B核心区别
虚拟地址中的 page-table indexpage offset前者用于选择页表项;offset 不参加 4 KiB 页表查找
CR3PML4ECR3 给出顶级页表上下文/基址;PML4E 是 PML4 中的一个 8-byte entry
entry 自己的物理地址entry 内保存的下一层地址例如 0x1007F0 是 PML4E 存放位置,而 0x200000 是它指向的 PDPT 基址
PTE 的物理地址PTE 指向的 physical page0x400B38 是 PTE 本身的位置,0x12345000 是映射的数据页
TLB lookuppage-table walkTLB hit 可绕过完整 walk;TLB miss 才需要硬件查页表
4-level paging5-level paging前者使用 4 组 9-bit index;后者再增加一个 9-bit PML5 index
page walkDRAM accesspage walk 是逻辑上的页表访问链;真实页表项可能命中 CPU cache 或 paging-structure cache

9. 常见误区

误区:64-bit 虚拟地址的 64 个 bit 在四级页表里都会被拿来索引

错误原因:

把寄存器宽度与分页模式实际支持的 virtual-address width 混在一起。

正确理解:

典型 4-level IA-32e paging 使用低 48 bit 完成翻译,高位必须满足 canonical sign-extension 规则。

如何验证:

手工按 9 + 9 + 9 + 9 + 12 = 48 分解一个地址,并检查 bits 63:48 与 bit 47 的关系。

误区:最低 12 bit 也要进入某一级页表查找

错误原因:

没有区分页号与页内偏移。

正确理解:

4 KiB 页内 offset 在映射前后保持不变,所以 bits 11:0 直到最后一步才直接拼到 physical page base 后面。

如何验证:

将同一虚拟页内两个只差 offset 的地址分解,会发现四个 page-table index 完全相同。

误区:CR3 保存的是一个虚拟地址

错误原因:

把“进程的虚拟地址空间”与“CPU 查页表所需的页表物理位置”混淆。

正确理解:

hardware page walker 需要从一个不依赖当前虚拟映射的根开始,因此 CR3 提供顶级 paging structure 的物理基址信息。

如何验证:

查 x86 架构手册中 CR3 与 paging-structure base address 的定义。

误区:0x1007F00x200000 都是“PML4E 的地址”,只是写法不同

错误原因:

混淆了 entry 的存储位置与 entry 的内容。

正确理解:

0x1007F0

是 PML4E 这 8 byte 自己所在的位置;

0x200000

是 PML4E 的地址字段指向的下一层 PDPT 基址。

如何验证:

把页表看成“数组元素里存指针”:

&table[index] != table[index].next_base

误区:四级页表意味着每次 load/store 都要访问四次内存

错误原因:

忽略了 TLB 和缓存结构。

正确理解:

TLB hit 时无需完整 page walk;即使 TLB miss,页表项也可能命中处理器缓存或 paging-structure cache。

如何验证:

需要借助硬件性能计数器区分 TLB miss、page walk 和 cache 行为;单凭地址计算无法证明实际 DRAM 访问次数。

误区:每次 page walk 都一定走满 L4→L3→L2→L1

错误原因:

忽略 huge page。

正确理解:

1 GiB 页可在 PDPTE 处终止,2 MiB 页可在 PDE 处终止;只有普通 4 KiB 页才走到 PTE。

如何验证:

查看具体映射使用的 page size,并按对应 leaf-entry 格式解析。

10. 与其他知识的关系

页表遍历依赖于页表项本身的编码:

x86-64 页表项:64 位 Entry、物理地址位宽与标志位

上游概念:

直接关联:

下游概念:

从操作系统角度看,不同进程通常拥有不同的地址翻译上下文/顶级页表根;操作系统可以共享或复用部分内核映射结构。具体组织方式受到 OS 设计与安全机制影响,不能简单理解成“全系统只有一张页表”或“所有页表页都绝对独立”。

11. 可以亲手完成的验证

实验目标

用纯用户态程序验证一个虚拟地址的 L4/L3/L2/L1/offset bit 切分,并验证“同一 4 KiB 虚拟页中的地址共享相同四级索引”。

所需环境

  • Python 3;
  • 无需 root 权限。

操作步骤

  1. 运行:

    def split_va(va: int):
        return {
            "L4": (va >> 39) & 0x1FF,
            "L3": (va >> 30) & 0x1FF,
            "L2": (va >> 21) & 0x1FF,
            "L1": (va >> 12) & 0x1FF,
            "offset": va & 0xFFF,
        }
    
    a = 0x00007F123456789A
    b = 0x00007F1234567ABC
    
    print(split_va(a))
    print(split_va(b))
    
  2. 比较两个结果。

  3. 再把 b 改成跨过 4 KiB 边界的地址,观察 L1 index 何时变化。

预期现象

如果两个地址处于同一 4 KiB virtual page:

  • L4/L3/L2/L1 index 相同;
  • 只有 offset 不同。

跨越 4 KiB 页边界后,至少 L1 index 或更高层 index 会按进位关系发生变化。

如何判断结果

能够预测哪一段 bit 会变化,就说明已经真正理解:

[virtual page number][page offset]

的分工。

实验不能证明什么

这个纯计算实验不能证明:

  • 当前进程真实 PML4/PDPT/PD/PT 的物理地址;
  • 当前映射对应哪个 physical frame;
  • TLB 是否命中;
  • page walk 实际产生了多少 cache/DRAM 访问。

现代 Linux 对 /proc/*/pagemap 等物理页框信息存在权限与安全限制,因此不应把“无法直接读出 PFN”误认为页表机制不存在。

12. 尚未解决的问题

  1. 5-level paging(LA57)启用后,57-bit canonical virtual address 如何分解,需要单独整理。
  2. CR3 的 PCID 语义及其如何减少 context switch 后的 TLB flush 尚未展开。
  3. TLB 的层级、容量、ITLB/DTLB/STLB 区别与微架构实现尚未展开。
  4. Paging-structure cache 与普通 data cache 在 page walk 中如何配合,需要结合具体 Intel/AMD 微架构资料。
  5. Page fault 的 error code、权限检查次序以及 Reserved-bit violation 尚未展开。
  6. Linux 如何建立和修改多级页表、如何共享内核映射,需要进入操作系统实现层分析。
  7. Huge page 的 leaf entry 地址位布局与普通 4 KiB PTE 不完全相同,后续应单独整理。

13. 自测问题

  1. 为什么 4 KiB page 需要 12-bit offset?
  2. 为什么每一级 page-table index 恰好是 9 bit?
  3. 在 4-level paging 中,VA 的 bits 29:21 用于哪一级?
  4. table_base + index × 8 中为什么乘以 8?
  5. CR3、PML4E 和 PTE 分别在整条翻译链中扮演什么角色?
  6. 为什么 entry @ 0x1007F0next table @ 0x200000 不能混为一谈?
  7. 为什么 TLB hit 时通常不需要重新走四级页表?
  8. VA=0x00007F123456789A 在本文示例中最终翻译成什么 PA?
参考答案
  1. 4 KiB = 2^12 byte,因此页内任意 byte 需要 12 bit 编号。
  2. 每张页表 4 KiB,每个 entry 8 byte,所以有 4096/8=512=2^9 个 entry。
  3. L2,也就是 Page Directory index。
  4. 因为每个 paging-structure entry 固定为 8 byte。
  5. CR3 提供顶级页表基址;PML4E 选择并指向下一层 PDPT;普通 4 KiB PTE 最终指向 physical page frame。
  6. 前者是某个 8-byte entry 自己存放的位置,后者是该 entry 的地址字段所指向的对象。
  7. TLB 已缓存了虚拟页到物理页的 translation,可以直接使用缓存结果。
  8. 0x000000001234589A

14. 一句话总结

4-level x86-64 地址翻译的本质是:把 48-bit canonical virtual address 拆成 4 个 9-bit 索引 + 12-bit offset,从 CR3 开始反复执行 table_base + index × 8,最终找到 physical page,再原样加回最低 12-bit 页内偏移。