bochs 中文文档 下载 App

页表与内存管理调试——walk page table 看 CR3 翻译每一级

x86 分页的几种模式

分页模式由 CR0.PG + CR4.PAE + EFER.LMA 组合决定:

PGPAELMA模式页表层级页大小
0--不分页无-
100普通 32 位PD → PT(2 级)4KB / 4MB
110PAE 32 位PDPT → PD → PT(3 级)4KB / 2MB
111x86-64 Long ModePML4 → PDPT → PD → PT(4 级)4KB / 2MB / 1GB
111x86-64 5-level(可选)PML5 → PML4 → PDPT → PD → PT4KB

OS 开发最常遇到的是 4 级 x86-64 和 3 级 PAE。

地址拆分:每级表用哪些位

地址翻译的核心思想:虚拟地址被拆成几段,每段用来查不同层级的页表。

4 级 x86-64(Long Mode)

48 位虚拟地址拆成:

[47:39]  PML4 index   (9 位)
[38:30]  PDPT index   (9 位)
[29:21]  PD index     (9 位)
[20:12]  PT index     (9 位)
[11:0]   Page offset  (12 位)

每级页表 4KB 大小、512 个 8 字节条目。CR3 指向 PML4 表的物理基址。

3 级 PAE(32 位 + PAE)

32 位虚拟地址拆成:

[31:30]  PDPT index   (2 位)
[29:21]  PD index     (9 位)
[20:12]  PT index     (9 位)
[11:0]   Page offset  (12 位)

PAE 的页表条目是 8 字节(比普通 32 位的 4 字节大)。

Bochs 里手动 walk page table

示例场景:验证 0xC0100000 的翻译(PAE 32 位模式)

bochs:1> info cr
cr3=0x12345000                 # PDPT 物理基址

# 第 1 级:PDPT(2 个条目,用虚拟地址 [31:30] 位)
# 虚拟地址 0xC0100000 → 二进制 1100 0000 0001 0000 0000 0000 0000 0000
# [31:30] = 11 = 3(二进制)→ PDPT 索引 3
bochs:2> xp /8gx 0x12345000 + 3*8   # 读 PDPT 第 3 项
0x12345018: 0x0000000000567001       # 条目值,bit 0=1(Present),bit 1=1(RW)
# 提取页表基址:(0x0000000000567001 >> 12) << 12 = 0x0000000000567000

# 第 2 级:PD(512 个条目,用虚拟地址 [29:21] 位)
# 虚拟地址 0xC0100000 → [29:21] = 010000000 = 256(二进制)
bochs:3> xp /8gx 0x567000 + 256*8    # 读 PD 第 256 项
0x5678000: 0x0000000000890001       # Present+RW

# 第 3 级:PT(512 个条目,用虚拟地址 [20:12] 位)
# 虚拟地址 0xC0100000 → [20:12] = 000000000 = 0
bochs:4> xp /8gx 0x890000 + 0*8      # 读 PT 第 0 项
0x890000: 0x0000000000012001         # Present+RW,页框号 0x12

# 最终物理地址 = 页框号 << 12 + offset
# 物理地址 = 0x12000 + (0xC0100000 & 0xFFF) = 0x12000
bochs:5> xp /16lx 0x12000            # 用 xp 直接看物理
bochs:6> x /16lx 0xC0100000          # 用 x 看虚拟(自动翻译)
# 两者应该显示相同内容!

示例场景:4 级 x86-64 walk

bochs:1> info cr
cr3=0x200000                    # PML4 基址

# 虚拟地址 0xffffffff80100000
# [47:39] = 111111111 = 511(符号扩展的上半部分)
# [38:30] = 100000000 = 256
# [29:21] = 000000000 = 0
# [20:12] = 000000000 = 0

bochs:2> xp /8gx 0x200000 + 511*8    # PML4 index 511
bochs:3> xp /8gx <PML4 条目 base> + 256*8   # PDPT index 256
bochs:4> xp /8gx <PDPT 条目 base> + 0*8     # PD index 0
bochs:5> xp /8gx <PD 条目 base> + 0*8       # PT index 0
# PT 条目里拿到最终页框号

页表条目格式

每级页表条目都是 8 字节(PAE/x86-64)或 4 字节(普通 32 位)。格式:

8 字节条目(x86-64)

位含义
0Present(P)= 1 表示该条目有效
1Read/Write(RW)= 1 可写
2User/Supervisor(US)= 1 用户态可访问
3Page-level Write Through
4Page-level Cache Disable
5Accessed(A)= CPU 自动置位
6Dirty(D)= CPU 自动置位(叶子表项)
7Page Size(PS)= 1 表示大页(2MB/1GB)
8Global(G)
51PAT(Page Attribute Table)
39:12页框号(Physical Address)
63:52保留

4 字节条目(普通 32 位 PT)

位含义
0-4同上
5Accessed
6Dirty(仅 PT 叶子)
7Page Size(PD 上 = 4MB 大页)
31:12页框号

walk page table 时的错误检查

某级表项 Present = 0

0x12345000: 0x0000000000000000    # P=0,条目无效

→ 页表里根本没写这个映射。内核初始化漏了、或者 CR3 指错了表、或者地址越界。

Present = 1 但 RW / US 权限不对

0x12345000: 0x0000000000567001    # RW=1, US=1 → 用户态可读写
0x12345000: 0x0000000000567005    # RW=0, US=1 → 用户态只读

→ page fault handler 里 error code 的 bit 1/2 应该能反映(写只读页 / 用户态访问内核页)。

页框号明显不合理

页框号应该是对齐到 4KB / 2MB / 1GB 的。如果页框号是个奇怪的值(比如 0x1),检查是不是用了 PAE 模式但按普通 32 位 walk(条目大小从 4 字节变 8 字节,索引错位)。

TLB 和页表修改

x86 有 TLB(Translation Lookaside Buffer)缓存页表翻译结果。如果你在 Bochs 里手动修改了物理内存里的页表条目,TLB 缓存不会自动失效——翻译可能还是用旧值。

在 Bochs 调试器里改完页表后需要让 CPU 刷新 TLB:

bochs:1> set $cr0 = ($cr0 | 0x80000000) - 0x80000000    # 关 PG 再开
# 或者更简单:
bochs:2> set $cr3 = 0x12345000                          # 重新写入 CR3(强制 TLB 刷新)

这会强制 CPU 重新加载页表、刷新 TLB。OS 内核代码里 mov cr3, eax 天然会刷新 TLB,但你在调试器里手动改内存不走这条路径。

内核内存管理验证

你的内核写好了 kmalloc / kfree(内核堆分配)后,想验证它是不是正确映射了物理页?

  1. 触发 kmalloc 分配一小块内存
  2. 在 Bochs 里对 kmalloc 返回的虚拟地址设断点
  3. 分配完成后,用 x 看虚拟地址、用 xp 看对应的物理地址(手动 walk)
  4. 验证物理页真的是从你的 free list 里拿出来的(页框号符合你的分配策略)
  5. kfree 后检查这个页框是不是回到了 free list

这比只跑程序看结果更可靠——你能直接看到页表里写了什么。