x86-64 地址翻译:从虚拟地址到物理地址的四级页表遍历
用一个完整例子追踪虚拟地址经过 TLB、CR3 和四级页表到达物理地址。
建议先了解
- 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 4 和 AMD64 Architecture Programmer’s Manual Volume 2: System Programming 的架构定义整理。
2. 前置知识
需要先掌握:
- x86-64 页表项:64 位 Entry、物理地址位宽与标志位
- 十六进制、二进制和 bit 编号;
- 物理地址与虚拟地址的区别;
- 4 KiB =
2^12; - 数组下标寻址:
base + index × element_size。
待补充:如果要理解真实 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 页大小:
所以虚拟地址最低 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 │
└──────────────┴───────────┴───────────┴───────────┴───────────┴────────────┘
原因是:
其中:
- 4 组 9 bit:每级选择 512 个 entry 中的一个;
- 12 bit:4 KiB 页内偏移。
5.2 每一级查表的核心公式
假设当前页表的物理基址是:
table_base
当前级 index 是:
index
一个 entry 为 8 byte,则 entry 自己的物理地址为:
CPU 读取该 8-byte entry 后,再从其中提取下一张页表的物理基址:
不断重复,直到最终 PTE。
5.3 完整概念流程
- CPU 形成一个待访问的虚拟/线性地址。
- 检查地址是否满足 canonical 要求。
- 查询 TLB。
- 若 TLB hit,直接使用缓存 translation。
- 若 TLB miss,hardware page walker 从 CR3 指向的顶级页表开始。
- 用 bits
47:39选中 PML4E。 - PML4E 指向下一层 PDPT。
- 用 bits
38:30选中 PDPTE。 - 若该 PDPTE 是非叶子项,继续到 PD;若其 page-size 语义表示 1 GiB huge page,则提前终止。
- 用 bits
29:21选中 PDE。 - 若 PDE 是非叶子项,继续到 PT;若其 page-size 语义表示 2 MiB huge page,则提前终止。
- 用 bits
20:12选中 PTE。 - PTE 给出最终 4 KiB physical page frame。
- 将原虚拟地址的 bits
11:0offset 拼到 physical page base 上。 - 得到最终 physical address。
- 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
得到:
| 字段 | 二进制 | 十进制 | 十六进制 |
|---|---|---|---|
| L4 | 011111110 | 254 | 0xFE |
| L3 | 001001000 | 72 | 0x48 |
| L2 | 110100010 | 418 | 0x1A2 |
| L1 | 101100111 | 359 | 0x167 |
| offset | 100010011010 | 2202 | 0x89A |
因此:
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,所以:
因此 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:
所以 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:
所以 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:
所以 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 地址
统一公式:
7.3 最终物理地址
4 KiB page 下:
或者从 bit 角度:
PA = [ physical-page-number ][ original offset ]
8. 容易混淆的概念
| 概念 A | 概念 B | 核心区别 |
|---|---|---|
| 虚拟地址中的 page-table index | page offset | 前者用于选择页表项;offset 不参加 4 KiB 页表查找 |
| CR3 | PML4E | CR3 给出顶级页表上下文/基址;PML4E 是 PML4 中的一个 8-byte entry |
| entry 自己的物理地址 | entry 内保存的下一层地址 | 例如 0x1007F0 是 PML4E 存放位置,而 0x200000 是它指向的 PDPT 基址 |
| PTE 的物理地址 | PTE 指向的 physical page | 0x400B38 是 PTE 本身的位置,0x12345000 是映射的数据页 |
| TLB lookup | page-table walk | TLB hit 可绕过完整 walk;TLB miss 才需要硬件查页表 |
| 4-level paging | 5-level paging | 前者使用 4 组 9-bit index;后者再增加一个 9-bit PML5 index |
| page walk | DRAM access | page 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 的定义。
误区:0x1007F0 和 0x200000 都是“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、物理地址位宽与标志位
上游概念:
- 虚拟内存、物理页面与多级页表
- 进程地址空间 — 待建链接
直接关联:
下游概念:
- 缺页异常与 Page Fault — 待建链接
- 上下文切换与 CR3/PCID — 待建链接
从操作系统角度看,不同进程通常拥有不同的地址翻译上下文/顶级页表根;操作系统可以共享或复用部分内核映射结构。具体组织方式受到 OS 设计与安全机制影响,不能简单理解成“全系统只有一张页表”或“所有页表页都绝对独立”。
11. 可以亲手完成的验证
实验目标
用纯用户态程序验证一个虚拟地址的 L4/L3/L2/L1/offset bit 切分,并验证“同一 4 KiB 虚拟页中的地址共享相同四级索引”。
所需环境
- Python 3;
- 无需 root 权限。
操作步骤
-
运行:
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)) -
比较两个结果。
-
再把
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. 尚未解决的问题
- 5-level paging(LA57)启用后,57-bit canonical virtual address 如何分解,需要单独整理。
- CR3 的 PCID 语义及其如何减少 context switch 后的 TLB flush 尚未展开。
- TLB 的层级、容量、ITLB/DTLB/STLB 区别与微架构实现尚未展开。
- Paging-structure cache 与普通 data cache 在 page walk 中如何配合,需要结合具体 Intel/AMD 微架构资料。
- Page fault 的 error code、权限检查次序以及 Reserved-bit violation 尚未展开。
- Linux 如何建立和修改多级页表、如何共享内核映射,需要进入操作系统实现层分析。
- Huge page 的 leaf entry 地址位布局与普通 4 KiB PTE 不完全相同,后续应单独整理。
13. 自测问题
- 为什么 4 KiB page 需要 12-bit offset?
- 为什么每一级 page-table index 恰好是 9 bit?
- 在 4-level paging 中,VA 的 bits
29:21用于哪一级? table_base + index × 8中为什么乘以 8?- CR3、PML4E 和 PTE 分别在整条翻译链中扮演什么角色?
- 为什么
entry @ 0x1007F0与next table @ 0x200000不能混为一谈? - 为什么 TLB hit 时通常不需要重新走四级页表?
VA=0x00007F123456789A在本文示例中最终翻译成什么 PA?
参考答案
- 4 KiB =
2^12byte,因此页内任意 byte 需要 12 bit 编号。 - 每张页表 4 KiB,每个 entry 8 byte,所以有
4096/8=512=2^9个 entry。 - L2,也就是 Page Directory index。
- 因为每个 paging-structure entry 固定为 8 byte。
- CR3 提供顶级页表基址;PML4E 选择并指向下一层 PDPT;普通 4 KiB PTE 最终指向 physical page frame。
- 前者是某个 8-byte entry 自己存放的位置,后者是该 entry 的地址字段所指向的对象。
- TLB 已缓存了虚拟页到物理页的 translation,可以直接使用缓存结果。
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 页内偏移。