4. 早期页表存放位置:为何页表必须放在可寻址的物理内存中,以及链接脚本中的页表对齐
在 ARM64 内核启动的早期阶段,MMU 尚未使能,CPU 处于物理地址空间直接寻址模式。此时,页表(Translation Tables)必须存放在 可被 CPU 直接访问的物理内存 中。任何存放在非易失性存储介质(如 Flash、ROM)或外设寄存器空间的页表,在 MMU 使能前都无法被 CPU 读取,从而导致系统崩溃。
4.1 早期页表的物理可寻址约束
当 CPU 从 Reset 向量执行第一条指令时,MMU 默认关闭。此时,CPU 发出的所有地址都被视为物理地址(PA),直接发送到内存控制器。页表基址寄存器(TTBR0_EL1/TTBR1_EL1)在写入时,其值必须是一个有效的物理地址,指向一段已初始化且可读的物理内存区域。
下图展示了 MMU 关闭状态下,CPU 如何通过物理地址直接访问页表:
4.2 链接脚本中的页表对齐要求
ARM64 架构要求页表基址必须按照页表大小进行对齐。对于 4KB 粒度的页表,每个级别(Level 0/1/2/3)的页表占用一个 4KB 页面。因此,页表的起始地址必须 4KB 对齐(即地址的低 12 位为 0)。对于 16KB 或 64KB 粒度,对齐要求相应增大。
在链接脚本(如 arch/arm64/kernel/vmlinux.lds.S)中,通过 . = ALIGN(SZ_4K); 或 . = ALIGN(PAGE_SIZE); 来保证页表段的起始地址满足对齐要求。
MSR TTBR0_EL1, X0 指令时,硬件会忽略低位的对齐位(对于 4KB 粒度,忽略低 12 位),导致实际使用的页表地址与预期不符,从而引发页表 walk 错误,最终触发 Data Abort 或 Translation Fault。
下表总结了不同粒度下的页表对齐要求:
| 翻译粒度 | 页表大小 | 对齐要求 | TTBRx 忽略的低位 |
|---|---|---|---|
| 4KB | 4KB | 4KB 对齐(低 12 位为 0) | Bits[11:0] |
| 16KB | 16KB | 16KB 对齐(低 14 位为 0) | Bits[13:0] |
| 64KB | 64KB | 64KB 对齐(低 16 位为 0) | Bits[15:0] |
4.3 链接脚本中的页表段定义示例
在 ARM64 Linux 内核的链接脚本中,早期页表通常被放置在 .head.text 段之后,紧挨着内核映像的起始部分。以下是一个典型的链接脚本片段:
SECTIONS
{
. = KIMAGE_VADDR;
.head.text : {
_text = .;
HEAD_TEXT
}
. = ALIGN(PAGE_SIZE); /* 页表对齐 */
.init_pg_dir : {
__init_pg_dir = .; /* 页表起始地址 */
. += INIT_PG_DIR_SIZE; /* 预留页表空间 */
__init_pg_end = .;
}
.text : {
TEXT_TEXT
}
/* ... 其他段 ... */
}
关键点说明:
. = ALIGN(PAGE_SIZE);:强制将当前位置对齐到页面边界,确保.init_pg_dir段的起始地址满足对齐要求。__init_pg_dir:符号标记页表的起始物理地址,后续在head.S中通过adrp指令获取该地址并写入 TTBR0_EL1。INIT_PG_DIR_SIZE:预分配的页表空间大小,通常为 4 个页面(Level 0/1/2/3 各一个 4KB 页面),共 16KB。
4.4 页表存放位置与链接脚本的协同关系
下图展示了页表在链接脚本中的位置、对齐要求以及最终在物理内存中的布局:
4.5 常见故障与排查方法
可能原因: 页表基址未对齐,导致 TTBR0_EL1 指向了错误的物理地址。
排查方法: 在
MSR TTBR0_EL1, X0 指令前,检查 X0 寄存器的低 12 位是否全为 0。若不为 0,则链接脚本中的对齐设置失效。
可能原因: 页表所在的物理内存区域尚未被初始化(如 DRAM 控制器未配置),CPU 读取到全 0 或随机数据。
排查方法: 在写入 TTBR0_EL1 前,通过读取页表所在地址的内容,确认其值是否为预期的页表描述符。
4.6 页表对齐的代码级验证
在 head.S 中,通常会在写入 TTBR0_EL1 之前插入对齐检查代码,以确保页表基址满足要求:
/*
* 检查页表基址是否 4KB 对齐
* X0 包含页表基址
*/
tst x0, #0xFFF // 检查低 12 位
b.eq 1f // 如果为 0,则跳转
/* 对齐错误处理 */
mov x0, #0xDEAD0000 // 错误码
b . // 死循环
1:
msr ttbr0_el1, x0 // 写入页表基址
isb
这段代码在调试阶段非常有用,可以快速定位因链接脚本对齐错误导致的页表故障。在生产版本中,通常通过链接脚本的 ASSERT 语句在编译时进行检查:
ASSERT((__init_pg_dir & ~PAGE_MASK) == 0,
"init_pg_dir not page aligned")
通过编译时断言和运行时检查的双重保障,可以确保页表存放位置和对齐要求得到满足,为后续的 MMU 使能提供可靠的基础。