背景
最近在阅读《操作系统设计与实现:基于LoongArch架构》这本书,研究 MaQueOS 教学操作系统。在调试 code3 示例时,遇到了一个让我困惑的问题,最终通过 GDB 调试和页表分析找到了答案。
问题描述
疑惑一:为什么映射模式下能跳转到低地址?
查看 code3 的 Makefile,发现链接参数是:
LDFLAGS = -z max-page-size=4096 -Ttext 0x9000000000200000
这意味着 _start
的虚拟地址应该是 0x9000000000200000
。
而从 loongarch_bios_0310_debug.bin
的行为来看:
- BIOS 将 MMU 置于映射地址翻译模式
- BIOS 将 kernel 复制到物理地址
0x200000
- BIOS 跳转到
0x200000
执行
问题来了: 如果 MMU 处于映射地址翻译模式,为什么还能直接跳转到地址 0x200000
运行?
疑惑二:看似无用的跳转指令
在 head.S
中有这样一段代码:
_start:
la $t0, go
go1:
jirl $r0, $t0, 0
go:
la $sp, kernel_init_stack
b main
乍一看: go1
标签处的 jirl
指令似乎是跳转到下一行代码 go
,这看起来完全是无用的操作。
实际上: 由于链接参数 -Ttext 0x9000000000200000
,编译后 la $t0, go
指令加载的是高地址空间的虚拟地址(0x9000000000200xxx
),而不是当前执行的低地址 0x200000
。
这几行汇编代码巧妙地实现了地址空间切换:从低地址空间 0x200000
跳转到高地址空间 0x9000000000200xxx
。
GDB 调试发现
通过 GDB 断点,我看到 BIOS 跳转时的汇编代码:
0x200000 pcaddu12i $t0, 3
0x200004 ld.d $t0, $t0, 496
0x200008 csrrd $t1, 0x180
0x20000c lu12i.w $t2, 513
0x200010 lu52i.d $t2, $t2, -1792
0x200014 st.d $t1, $t2, 0
...
确实是在地址 0x200000
执行!
关键线索
在 LoongArch 架构手册中找到了关键信息:
该寄存器(TLBRENTRY)用于配置 TLB 重填例外的入口地址。由于触发 TLB 重填例外之后,处理器核将进入直接地址翻译模式,所以此处所填入口地址应当是物理地址。
当 _start
执行时,我观察到发生了 TLB 重填例外:
0x1c042354 csrwr $t0, 0x8b # 保存 $t0
0x1c042358 csrrd $t0, 0x1b # 读取 BADV (触发异常的虚拟地址)
0x1c04235c lddir $t0, $t0, 0x3 # 查找4级页表
0x1c042360 lddir $t0, $t0, 0x2
0x1c042364 lddir $t0, $t0, 0x1
0x1c042368 ldpte $t0, 0x0 # 加载页表项
0x1c04236c ldpte $t0, 0x1
0x1c042370 tlbfill # 填充 TLB
0x1c042374 csrrd $t0, 0x8b
0x1c042378 ertn # 异常返回
这说明:0x200000
被当作虚拟地址,MMU 尝试查找 TLB,未命中后触发重填例外!
验证过程
添加调试代码
我在 head.S
中添加了代码来读取关键寄存器和页表信息:
# 读取DMW0寄存器的值
csrrd $t1, 0x180 # CSR_DMW0
# 将值存到固定内存位置
li.d $t2, 0x9000000000201000
st.d $t1, $t2, 0
# 读取DMW1寄存器的值
csrrd $t1, 0x181 # CSR_DMW1
st.d $t1, $t2, 8
# 读取DMW2寄存器的值
csrrd $t1, 0x182 # CSR_DMW2
st.d $t1, $t2, 16
# 读取DMW3寄存器的值
csrrd $t1, 0x183 # CSR_DMW3
st.d $t1, $t2, 24
# 读取CRMD寄存器的值
csrrd $t1, 0x0 # CSR_CRMD (寄存器号是0x0)
st.d $t1, $t2, 32 # 存到偏移32的位置
# 读取PGDL寄存器(页表基址) - 0x19
csrrd $t1, 0x19 # CSR_PGDL
st.d $t1, $t2, 40 # 存到偏移40的位置
# 读取PGDH寄存器(页表基址高位) - 0x1a
csrrd $t1, 0x1a # CSR_PGDH
st.d $t1, $t2, 48 # 存到偏移48的位置
...
完整的地址翻译流程
BIOS 设置
- CRMD.PG=1, CRMD.DA=0 (映射地址翻译模式)
- 配置 DMW0 =
0x9000000000000001
(直接映射窗口) - 配置页表,建立映射:
- 虚拟地址
0x200000
→ 物理地址0x200000
- 虚拟地址
0x9000000000200000
→ 通过 DMW0 映射到物理地址0x200000
- 虚拟地址
BIOS 跳转
- 跳转到虚拟地址
0x200000
(书中说的"物理地址"实际是虚拟地址!) - MMU 查找 TLB,未命中
- 触发 TLB 重填例外
- 跳转到虚拟地址
TLB 重填处理
- 处理器自动进入直接地址翻译模式 (DA=1)
- 执行异常处理程序(物理地址)
- 填充 TLB
- 异常返回,恢复映射模式
继续执行
- 重新执行
0x200000
的指令 - 这次 TLB 命中!
- 成功翻译:虚拟地址
0x200000
→ 物理地址0x200000
- 重新执行
地址空间切换
_start: la $t0, go # $t0 = 0x9000000000200xxx (高地址) go1: jirl $r0, $t0, 0 # 跳转到高地址空间 go: la $sp, kernel_init_stack b main
- 当前在低地址
0x200000
执行 la $t0, go
加载的是链接时的高地址0x9000000000200xxx
jirl
跳转到高地址空间- 之后所有代码都在高地址空间运行
- 当前在低地址
两种虚拟地址映射
BIOS 巧妙地配置了两种映射方式:
虚拟地址 | 映射方式 | 物理地址 | 用途 |
---|---|---|---|
0x200000 | 页表 | 0x200000 | BIOS 启动入口 |
0x9000000000200000 | DMW0 直接映射 | 0x200000 | 内核运行地址 |
吐槽
loongarch_bios_0310_debug.bin
不开源,虽然可以反汇编查看,但无法直接阅读源码,增加了调试难度。另外,书中描述 BIOS “跳转到物理地址 0x200000” 容易产生误解,实际上跳转的是虚拟地址 0x200000
,只是恰好映射到相同的物理地址。如果能开放 BIOS 源码并更正文档描述,对学习 LoongArch 架构会有更大帮助。
总结
这次调试让我深入理解了 LoongArch 的地址翻译机制和地址空间切换技巧:
- 映射模式 ≠ 不能访问低地址:低地址通过页表映射仍然可以访问
- “物理地址"的误解:BIOS 跳转的
0x200000
实际上是虚拟地址,通过映射翻译到物理地址0x200000
- TLB 重填的巧妙设计:异常时自动切换到直接地址模式,保证异常处理程序能正常运行
- DMW 的作用:为内核提供高地址空间的直接映射窗口
- 地址空间切换的艺术:通过
la
+jirl
实现从低地址到高地址的优雅过渡
说些什么吧!