
页表的物理地址从哪里来:内核如何访问页表
页表本身也放在内存中。如果内核想修改一个页表项,首先要回答两个不同的问题:这张页表位于哪个物理页框?内核应该通过哪个虚拟地址访问它?前一个问题决定硬件如何找到页表,后一个问题决定内核代码如何读写页表。本文以 x86-64 四级页表、4 KiB 页面为例,沿这两个问题理解直接映射和递归映射。
页表的物理地址如何确定
页表并没有一组预先约定好的物理地址。启动时,引导程序或内核从可用物理内存中选出页框,放入页表内容;运行中需要新的下级页表时,内核也可以再分配页框。各张页表不必在物理内存中连续。x86-64 约定的是如何找到它们:当前地址空间的顶级页表物理基址记录在 CR3 中;顶级页表的一个有效表项记录下一级页表的物理基址,后续层级同理。这里讨论的是指向下级页表的表项;大页映射会提前结束这条查表路径。
例如,内核选择物理地址 0x1000 放顶级页表,并选择 0x5000 放某张下级页表,就可以把顶级页表的相应表项设为指向 0x5000,再让 CR3 指向 0x1000。这些具体数字是软件的选择;页框对齐、表项格式以及处理器从 CR3 开始逐级查找,则是硬件规则。内核在建立新页表时,须先有办法访问新分配的页框,才能填好内容并把它接入层级;启动阶段已有映射或临时映射可承担这一步。
为什么内核不能直接使用物理地址
处理器在地址翻译时读取的是页表项里记录的物理地址。但内核代码在启用分页后执行普通内存读写时,给出的地址会被当成虚拟地址。因此,读出表项里的 0x5000,并不意味着内核可以直接解引用地址 0x5000 来修改那张页表:虚拟地址 0x5000 可能映射到别处,也可能根本没有映射。
内核必须让某个虚拟页映射到物理页框 0x5000。这个虚拟页可以有多种安排方式;物理页框的位置不因此改变。下面的两种方案,解决的都是“内核用哪个虚拟地址访问已知物理页框”这个问题。
直接映射
一种做法是在内核虚拟地址空间中保留一个区域,使物理地址 p 对应的虚拟地址总是 base + p。前提是内核已经建立了这片映射;加法只是使用映射的规则,并不会自行创建映射。于是,若页表页位于物理地址 p,内核通过 base + p 访问整张页表;第 i 个 8 字节表项位于 base + p + 8i。
沿目标虚拟地址的四级索引 A、B、C、D 查找一级页表项,可以写成下面的示意代码。FRAME_MASK 用来取出 CR3 或页表项中的页框基址;代码省略了有效位和大页检查。
unsigned walk[3] = {A, B, C};
uint64_t table_pa = read_cr3() & FRAME_MASK;
for (int level = 0; level < 3; ++level) {
uint64_t *table = (uint64_t *)(DIRECT_BASE + table_pa);
table_pa = table[walk[level]] & FRAME_MASK;
}
uint64_t *pt = (uint64_t *)(DIRECT_BASE + table_pa);
uint64_t *pte = &pt[D];每轮从表项取得的是下级页表的物理地址;下一轮加上 DIRECT_BASE 后,内核才得到可以解引用的虚拟地址。
递归映射
另一种做法是在顶级页表(PML4)选择索引 R,让 PML4[R] 指向 PML4 自己的物理页框。为了看出效果,把普通虚拟地址的四级索引记作 A、B、C、D,页内偏移记作 EEEE。普通地址沿着以下路径到达数据页:
CR3 → PML4[A] → 三级表[B] → 二级表[C] → 一级表[D] → 数据页现在构造索引为 R、A、B、C、页内偏移为零的虚拟地址。处理器仍从 CR3 出发,但第一步读到 PML4[R] 后又回到了 PML4;余下三个索引继续沿原路径走,最后被当作“数据页”访问的,恰好是原地址所用的一级页表页:
CR3 → PML4[R] → PML4[A] → 三级表[B] → 二级表[C] → 一级表页这就是递归映射中最重要的一次转变:页表页依旧位于原来的物理页框,只是通过自指表项获得了一个可计算的虚拟地址。把最前面的 R 再重复一次,就能让翻译路径再少下降一级;依次重复可访问更高层的页表页。
沿用原笔记的完整八进制记法,AAA、BBB、CCC、DDD、RRR 各表示 9 位索引(三位八进制数),EEEE 表示 12 位页内偏移(四位八进制数)。索引和偏移合起来是 48 位;最前面的 SSSSSS 表示补成 x86-64 规范虚拟地址时的高 16 位,其值由该行第一个索引的最高位决定。
| 要访问的对象 | 虚拟地址结构(八进制) |
|---|---|
| 普通页内的地址 | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE |
| 一级页表项 | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD |
| 二级页表项 | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC |
| 三级页表项 | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB |
| 四级页表项 | 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA |
第一行表项地址中的 RRR_AAA_BBB_CCC_0000 定位一级页表页;最后的 DDDD 是索引 DDD 乘以 8 字节后得到的页内偏移。其他行同理:每重复一次 RRR,最终被访问的页表就上升一级。
原笔记的图中,红色路径对应三级页表项,蓝色路径对应四级页表项:

下面的示意代码把同一个一级页表项地址按位移写出来:
uintptr_t pte_va = canonical48(
((uintptr_t)R << 39) | ((uintptr_t)A << 30) |
((uintptr_t)B << 21) | ((uintptr_t)C << 12) |
((uintptr_t)D << 3));
uint64_t *pte = (uint64_t *)pte_va;canonical48 对应表中的 SSSSSS;前四组索引对应 RRR_AAA_BBB_CCC,D << 3 对应 DDDD。更高层表项只需相应地重复 R,并把该层索引放到页内偏移。
这样,物理地址与虚拟地址各司其职:CR3 和表项中的物理地址让硬件沿页表层级翻译;直接映射或递归映射则让内核能通过虚拟地址修改这些页表。递归映射额外提供了一个性质:给定要映射的虚拟地址,其查表路径上的各级页表项地址可以直接由索引算出。
参考文献
- Intel, Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Chapter 4:
CR3、四级页表结构及表项地址规则。 - Philipp Oppermann, Paging Implementation:页表页的物理地址与内核访问它们的多种映射方式。
- Philipp Oppermann, Advanced Paging:递归映射的地址推导;该文已标注由前一篇更新替代,本文只参考其推导示例。