4. 内存访问日志分析:-d mmu 参数详解、地址转换与页表遍历日志解读
内存访问出问题,是 RISC-V 系统里最让人头疼的故障之一。我见过不少开发者,程序跑飞了第一反应是查代码逻辑,折腾半天才发现是页表配置错了。其实 QEMU 的 -d mmu 日志,能直接把地址转换的每一步都摊开给你看。
这一节,我们就来彻底搞懂它。
4.1 -d mmu 到底输出了什么?
简单说,-d mmu 会打印每一次虚拟地址到物理地址的转换过程。包括页表遍历的每一级、页表项的权限检查、以及最终的映射结果。
启动 QEMU 时加上这个参数:
qemu-system-riscv64 -kernel my_os.elf -nographic -d mmu
你会看到类似这样的输出:
mmu: vaddr=0x80001200 paddr=0x80001200 size=4 prot=r-x
mmu: vaddr=0x80001204 paddr=0x80001204 size=4 prot=r-x
mmu: vaddr=0x80001208 paddr=0x80001208 size=4 prot=r-x
...
嗯,这里要注意:如果系统刚启动时还没开 MMU,日志里全是这种 恒等映射(vaddr == paddr)的记录。等软件执行了 satp 寄存器的写入,真正的页表遍历日志才会出现。
-d mmu 的日志,确认系统能正常启动到开启 MMU 的代码位置。然后再加上参数,否则日志量太大,反而淹没了关键信息。
4.2 地址转换的核心流程
RISC-V 的 Sv39 分页机制,说白了就是三级页表遍历。每次 CPU 访问一个虚拟地址,硬件都会自动完成以下步骤:
- 拆分虚拟地址:把 39 位虚拟地址拆成 3 个 9 位的索引(VPN[2]、VPN[1]、VPN[0])和 12 位的页内偏移。
- 读根页表:从
satp寄存器拿到根页表的物理地址,用 VPN[2] 作为索引,找到第二级页表的基址。 - 遍历中间级:用 VPN[1] 索引第二级页表,找到第三级页表的基址。
- 读叶子页表:用 VPN[0] 索引第三级页表,拿到最终的物理页号(PPN)。
- 拼出物理地址:PPN 左移 12 位,加上页内偏移,得到完整的物理地址。
下面这张图,把整个流程串起来了:
4.3 日志中的页表遍历细节
当 MMU 开启后,-d mmu 的日志会变得详细得多。我截取一段真实日志来分析:
mmu: translate: vaddr=0xffffffc000000000
mmu: 1st pte: paddr=0x80002000 vpn2=0x1ff pte=0x80002801
mmu: 2nd pte: paddr=0x80002800 vpn1=0x000 pte=0x80003001
mmu: 3rd pte: paddr=0x80003000 vpn0=0x000 pte=0x8000400f
mmu: result: paddr=0x80004000 prot=rw-
每一行都对应一次页表项的读取。我们来拆解一下:
| 日志字段 | 含义 | 我的解读 |
|---|---|---|
vaddr=0xffffffc000000000 |
要转换的虚拟地址 | 这是内核空间的高地址,通常映射到物理内存的起始位置 |
1st pte: paddr=0x80002000 vpn2=0x1ff |
根页表项位于物理地址 0x80002000,索引为 0x1ff | 0x1ff 是 9 位全 1,对应虚拟地址的最高 9 位 |
pte=0x80002801 |
页表项的值 | 低 12 位是标志位,0x001 表示有效且可读 |
result: paddr=0x80004000 prot=rw- |
最终物理地址和权限 | 映射成功,可读可写 |
0x01(V) — 有效位,必须置 10x0f(V|R|W|X) — 可读可写可执行0x11(V|U) — 用户态可访问
page fault。
4.4 常见故障模式与日志特征
我在项目中遇到过几次典型的页表问题,日志特征非常明显:
4.4.1 页表项无效(Page Not Present)
mmu: translate: vaddr=0x0000000080004000
mmu: 1st pte: paddr=0x80002000 vpn2=0x000 pte=0x80002801
mmu: 2nd pte: paddr=0x80002800 vpn1=0x000 pte=0x80003001
mmu: 3rd pte: paddr=0x80003000 vpn0=0x004 pte=0x00000000
mmu: page fault: vaddr=0x80004000 cause=12 (page not present)
看到 pte=0x00000000 了吗?这就是问题所在。第三级页表项全零,表示这个虚拟页面没有映射物理内存。
4.4.2 权限违规(Permission Fault)
mmu: translate: vaddr=0x0000000080004000
mmu: 1st pte: paddr=0x80002000 vpn2=0x000 pte=0x80002801
mmu: 2nd pte: paddr=0x80002800 vpn1=0x000 pte=0x80003001
mmu: 3rd pte: paddr=0x80003000 vpn0=0x004 pte=0x80004001
mmu: page fault: vaddr=0x80004000 cause=15 (store access fault)
这里第三级页表项是 0x80004001,只有 V 位(0x01),没有 W 位。但程序试图写这个地址,所以触发了 store access fault。
你想想看,如果页表项里没有 W 位,CPU 是不会允许写入的。这是硬件强制检查的,软件一点办法都没有。
4.4.3 大页(Huge Page)的日志特征
RISC-V 支持 2MB 和 1GB 的大页。如果中间级页表项设置了 R、W 或 X 位,遍历就会提前终止:
mmu: translate: vaddr=0xffffffc000000000
mmu: 1st pte: paddr=0x80002000 vpn2=0x1ff pte=0x80002801
mmu: 2nd pte: paddr=0x80002800 vpn1=0x000 pte=0x800000cf
mmu: result: paddr=0x80000000 prot=rw- (2MB huge page)
注意第二级页表项 0x800000cf,低 8 位是 0xcf,包含了 V、R、W、X 位。这意味着遍历在第二级就结束了,不需要再查第三级页表。日志里明确标注了 2MB huge page。
4.5 实战排查步骤
如果你遇到内存访问异常,我建议按这个顺序排查:
- 确认 MMU 是否已开启:检查日志中是否有
satp寄存器的写入记录。如果没有,所有地址都是恒等映射。 - 定位出错的虚拟地址:找到
page fault那一行,记下vaddr和cause。 - 逐级检查页表项:从
1st pte开始,确认每一级的页表项是否有效、权限是否正确。 - 验证物理地址:如果页表遍历成功,检查最终
paddr是否落在合法的物理内存范围内。 - 检查 TLB:如果页表看起来没问题,可能是 TLB 没有刷新。试试在修改页表后执行
sfence.vma指令。
-d mmu,cpu_reset,可以同时看到 MMU 日志和 CPU 状态。这样能更直观地看到异常发生时,CPU 的当前特权级和寄存器值。
4.6 总结
-d mmu 是排查内存映射问题的利器。它能让你看到地址转换的每一个细节,从虚拟地址拆分到页表项读取,再到最终的物理地址拼装。掌握了这些日志的解读方法,你就能快速定位页表配置错误、权限设置不当、物理地址越界等常见问题。
说白了,内存访问故障排查,就是一场和页表项的博弈。日志已经把答案摆在你面前了,剩下的就是你能不能读懂它。