内存管理的基础内容可以看另一篇博客:内存管理篇
VMSA
MMU
VMSA 提供了 MMU 硬件单元,MMU 中还包含了 TLB,保留的是 MMU 页表翻译后的转换结果。
MMU 硬件单元用来实现 VA 到 PA 的地址转换(转换由硬件自动转换):
- 硬件遍历页表:table walk
- TTBR0/1:页表基地址寄存器,指向当前使用的页表(一级页表)的物理地址
其中,TTBR0 通常用于用户空间地址转换,TTBR1 用于内核空间地址转换。为了实现用户态和内核态进程的隔离,保证内核空间不被用户空间访问,因此可以看到我们总线有 64 bit,但实际上用户空间和内核空间的地址范围都被限制在 48 bit 内,原因是这样就可以通过 TTBR0 和 TTBR1 分别指向不同的页表来实现用户空间和内核空间的地址转换。
- 内核空间:0xFFFF000000000000 ~ 0xFFFFFFFFFFFFFFFF,占用高位虚拟地址空间
- 用户空间:0x0000000000000000 ~ 0x0000FFFFFFFFFFFF,占用低位虚拟地址空间

二、初始化 MMU
要注意,页表修改后,需要把相关的 TLB entry 刷新掉,否则可能会访问到过期的 TLB entry 导致访问错误。
初始化 MMU 的时候需要做的事:
- 初始化 PGD 页表目录:清空 PGD 页,准备好页表基地址(idmap_pg_dir)
- 创建代码段(.text)映射:建立恒等映射(VA=PA),确保启用 MMU 后代码能继续执行
- 创建测试内存映射:建立 512MB 内存的页表映射,用于测试页表功能
- 创建 MMIO 映射:为设备寄存器区域建立映射,使用设备内存属性
- 配置 CPU MMU 寄存器:设置内存属性(MAIR)、虚拟/物理地址范围(TCR)、浮点支持等
- 启用 MMU:设置页表基地址(TTBR0),使能 MMU(SCTLR_EL1.M),刷新 TLB
1 | void paging_init(void) |
三、访问测试
该测试中,需要创建完 text 内存映射后,再创建 512MB 的内存映射:
1 | static void create_identical_mapping(void) |
代码中只设置了代码段~512MB的内存,因此先测试创建的内存以内的地址访问会触发页错误异常(TOTAL_MEMORY - 4096),然后测试没有内存映射的地址访问会触发页错误异常:
1 | static int test_access_map_address(void) |
最后的运行结果如下:
1 | akira@akira:~/BenOS/BenOS_ARM/benos$ make run |
实验二:dump 页表
我们 debug 内存相关的内容的时候,经常需要 dump 出页表的虚拟地址、页表项属性等信息。
同时,这也可以看出我们的页表映射是否正确。
实现方法是软件上遍历页表,遍历到页表的叶子节点时,打印出虚拟地址、页表项属性等信息。
一、定义打印内容-数据结构
需要打印的内容: - 页表项属性对应的属性值(转成文字描述) - 叶子节点的层级:pg_level[].name = PGD/PUD/PMD/PTE - 每个叶子节点 entry 代表的页面大小 - 普通内存页是 4K,叶子节点的层级是PTE - 大页有可能是是 2MB,叶子节点的层级是PMD
1 | struct prot_bits { |
下面在初始化 MMU 后,就可以初始化一下各个层级的各个属性的mask值,把所有属性位全部置为1:
1 | static void pg_level_init() |
运行结果如下:
1 | (gdb) p/x pg_level[1].mask |
二、遍历页表并打印
遍历页表的逻辑跟创建页表的类似,也是一层一层遍历下去,这里就不赘述了。唯一不一样的是遇到PUD_TYPE_SECT/PMD_TYPE_SECT/PTE_TYPE_SECT属性时,表示是叶子节点,此时停止遍历,调用print_pgtabledump
打印节点信息。
1 | walk_pgd |
1 | static void walk_pud(pgd_t *pgdp, unsigned long start, unsigned long end) |
运行结果如下:
create_identical_mapping函数创建出来的.text 段不是 2MB 对齐的,虽然设置了BLOCK_MAPPINGS但是非 2MB 对齐,因此也是创建 PTE 节点:1
2
3
4
5
6
7
8---[ Identical mapping ]---
0x0000000000080000-0x0000000000081000 4K PTE ro x SHD AF UXN MEM/NORMAL
0x0000000000081000-0x0000000000082000 4K PTE ro x SHD AF UXN MEM/NORMAL
0x0000000000082000-0x0000000000083000 4K PTE ro x SHD AF UXN MEM/NORMAL
...
0x00000000001fd000-0x00000000001fe000 4K PTE RW NX SHD AF UXN MEM/NORMAL
0x00000000001fe000-0x00000000001ff000 4K PTE RW NX SHD AF UXN MEM/NORMAL
0x00000000001ff000-0x0000000000200000 4K PTE RW NX SHD AF UXN MEM/NORMALcreate_identical_mapping函数创建出来的 512MB 块内存区域:1
2
3
4
5
6
70x0000000000200000-0x0000000000400000 2M PMD RW NX SHD AF BLK UXN MEM/NORMAL
0x0000000000400000-0x0000000000600000 2M PMD RW NX SHD AF BLK UXN MEM/NORMAL
0x0000000000600000-0x0000000000800000 2M PMD RW NX SHD AF BLK UXN MEM/NORMAL
...
0x000000001fa00000-0x000000001fc00000 2M PMD RW NX SHD AF BLK UXN MEM/NORMAL
0x000000001fc00000-0x000000001fe00000 2M PMD RW NX SHD AF BLK UXN MEM/NORMAL
0x000000001fe00000-0x0000000020000000 2M PMD RW NX SHD AF BLK UXN MEM/NORMAL
非 text
段的普通内存页范围为0x86000~0x20000000,所以也有一部分未对齐的普通内存页被写成
4KB 内存了。原因是:
1 | _etext = 0x850d0 |
0x86000 不是 2MB 对齐的,因此会创建 4KB 页节点
1 | python3 -c "print(hex(0x86000 % 0x200000))" |
所以从 0x86000 开始:
- 先用 4KB 页填充到 0x90000(下一个 2MB 边界)
- 从 0x200000 开始才是 2MB 对齐,才用 2MB 块
1 | 0x86000 ──── 4KB 页 ───→ 0x200000 ──── 2MB 块 ───→ 0x20000000 |
不同内存区域的对齐方式导致 dump_pgtable 会同时看到 4KB 和 2MB 叶子节点:
| 内存区域 | 起始/结束对齐 | 结果 |
|---|---|---|
| .text 代码段 | 通常不是严格 2MB 对齐 | 4KB 页(PTE 叶子) |
| 512MB 内存 | 通常是 2MB 对齐 | 2MB 块(PMD 叶子) |