4. 早期页表存放位置:为何页表必须放在可寻址的物理内存中,以及链接脚本中的页表对齐

在 ARM64 内核启动的早期阶段,MMU 尚未使能,CPU 处于物理地址空间直接寻址模式。此时,页表(Translation Tables)必须存放在 可被 CPU 直接访问的物理内存 中。任何存放在非易失性存储介质(如 Flash、ROM)或外设寄存器空间的页表,在 MMU 使能前都无法被 CPU 读取,从而导致系统崩溃。

4.1 早期页表的物理可寻址约束

当 CPU 从 Reset 向量执行第一条指令时,MMU 默认关闭。此时,CPU 发出的所有地址都被视为物理地址(PA),直接发送到内存控制器。页表基址寄存器(TTBR0_EL1/TTBR1_EL1)在写入时,其值必须是一个有效的物理地址,指向一段已初始化且可读的物理内存区域。

关键约束: 在 MMU 使能前,页表必须位于 物理内存(DRAM/SRAM) 中,且该内存区域必须已经被初始化(如通过固件或启动代码的 memcpy 从 Flash 拷贝)。页表不能放在未映射的外设地址空间或只读存储器中。

下图展示了 MMU 关闭状态下,CPU 如何通过物理地址直接访问页表:

MMU 关闭时页表物理寻址路径 CPU 核心 (MMU 关闭) 物理地址总线 内存控制器 物理内存 (DRAM) 页表存放区域 0x4000_0000 ~ 0x4000_3FFF 为什么不能放在 Flash 或 ROM 中? • Flash/ROM 的读取延迟高,且通常需要特定的读命令序列 • CPU 在 MMU 关闭时无法执行页表 walk,必须直接读取物理内存

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); 来保证页表段的起始地址满足对齐要求。

对齐失败后果: 如果页表基址未对齐,当 CPU 执行 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 页表存放位置与链接脚本的协同关系

下图展示了页表在链接脚本中的位置、对齐要求以及最终在物理内存中的布局:

链接脚本页表布局与物理内存映射 链接脚本虚拟布局 .head.text (启动代码) . = ALIGN(PAGE_SIZE) .init_pg_dir (页表: 4KB 对齐) .text (内核代码) 加载到 物理内存布局 (DRAM) 0x4008_0000: .head.text 0x4008_1000: 对齐填充 0x4008_2000: .init_pg_dir (4KB 对齐, 低12位=0) 0x4008_3000: .text 页表基址写入 TTBR0_EL1 时,硬件自动忽略低 12 位,因此 0x4008_2000 实际被解释为 0x4008_2000

4.5 常见故障与排查方法

故障现象 1: 系统在 MMU 使能后立即触发 Translation Fault,且 ESR 寄存器指示 Level 0/1 转换错误。
可能原因: 页表基址未对齐,导致 TTBR0_EL1 指向了错误的物理地址。
排查方法: 在 MSR TTBR0_EL1, X0 指令前,检查 X0 寄存器的低 12 位是否全为 0。若不为 0,则链接脚本中的对齐设置失效。
故障现象 2: 系统在 MMU 使能后卡死,无任何异常输出。
可能原因: 页表所在的物理内存区域尚未被初始化(如 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 使能提供可靠的基础。