1. ARM64 启动流程概览:从 ROM Code 到 head.S 的跳转

ARM64 系统的启动是一个多阶段、高度结构化的过程。从处理器复位到执行 Linux 内核的 head.S,中间经历了固件初始化、硬件自检、内存初始化以及异常等级切换等关键步骤。理解这一流程,是掌握页表构建与 MMU 使能的前提——因为页表的初始化和 MMU 的开启时机,与启动阶段紧密绑定。

1.1 启动阶段划分与核心任务

典型的 ARM64 启动流程可分为以下四个主要阶段。每个阶段都有其特定的运行环境、异常等级(Exception Level)和页表状态。

阶段 执行主体 异常等级 MMU 状态 页表状态
ROM Code 片上 BootROM EL3 关闭 无页表,物理地址直接访问
Bootloader U-Boot / TF-A EL3 → EL2 通常关闭(或临时开启) 可能建立临时页表(如 Flat Mapping)
Kernel 入口 head.S EL2 / EL1 关闭 尚未建立,依赖 Bootloader 传递的 FDT
早期 MMU 使能 head.S / early_fixmap EL1 开启 恒等映射 + 内核虚拟地址映射
关键认知: 在 head.S 执行之前,系统处于“裸物理地址”模式。任何对内存的访问都是直接通过物理地址完成的。页表的构建和 MMU 的使能,是 head.S 中最早、最核心的任务之一。

1.2 从 ROM Code 到 Bootloader 的跳转

系统上电或复位后,CPU 从 ROM Code 开始执行。ROM Code 是固化在芯片内部的只读代码,负责最基本的硬件初始化:

  • 配置系统时钟和电源管理单元(PMU)。
  • 初始化片内 SRAM 或 L2 Cache 作为临时堆栈。
  • 检测启动介质(eMMC、NAND Flash、UART 等)并加载第一级 Bootloader(如 U-Boot SPL)。
  • 跳转到 Bootloader 的入口地址,通常位于 EL3 或 EL2。

Bootloader(如 U-Boot 或 TF-A)接管后,会完成更复杂的初始化:

  • 初始化 DDR 控制器,使能主内存。
  • 加载设备树(FDT)到内存中。
  • 加载 Linux 内核镜像到指定物理地址。
  • 准备启动参数,并通过 wfe / sev 或直接跳转指令将控制权交给内核。
实践提示: 在调试启动早期故障时,经常需要检查 Bootloader 传递给内核的 device tree 地址(x0 寄存器)和内核入口地址(x1 寄存器)。如果这两个值不正确,head.S 将无法正确解析内存布局。

1.3 内核入口:head.S 的初始环境

当 Bootloader 通过 b 或 br 指令跳转到内核入口时,CPU 处于以下状态:

  • 异常等级: EL2(如果内核支持虚拟化)或 EL1(非虚拟化)。
  • MMU: 关闭。所有地址都是物理地址。
  • Cache: 可能处于未知状态,通常需要 invalidate。
  • 堆栈: 未设置。head.S 的前几条指令必须立即配置临时堆栈。
  • 寄存器:
    • x0:设备树(FDT)的物理地址。
    • x1:内核入口的物理地址(通常与链接地址不同)。
    • x2:Bootloader 的机器类型(已废弃,通常为 0)。

head.S 的第一个动作是执行 __primary_switch 或类似的入口函数,其核心逻辑包括:

  1. 设置 SP_ELx 堆栈指针。
  2. 保存 FDT 地址到 __fdt_pointer。
  3. 检查当前异常等级,并决定是否需要降级(EL2 → EL1)。
  4. 调用 __create_page_tables 构建初始页表。
  5. 调用 __enable_mmu 使能 MMU。

1.4 启动阶段与页表的关系

页表的构建时机与启动阶段紧密相关。下图展示了从 ROM Code 到 head.S 页表构建的完整流程与依赖关系:

ARM64 启动阶段与页表构建流程 ARM64 启动阶段与页表构建流程 ROM Code EL3, MMU 关闭 加载 Bootloader Bootloader EL3 → EL2 DDR 初始化, 加载内核 head.S 入口 EL2 / EL1 MMU 关闭, 物理寻址 跳转 跳转 (x0=FDT) head.S 内部:页表构建与 MMU 使能子流程 设置 SP_ELx 临时堆栈 创建页表 恒等映射 + 内核映射 使能 MMU SCTLR_EL1.M=1 跳转 虚拟地址 说明: 1. 恒等映射(Identity Mapping)确保 MMU 开启瞬间指令地址不失效。 2. 内核映射将 .text/.data 段映射到链接地址(通常为 0xffff0000xxxxxxxx)。

1.5 页表构建的时机选择

为什么页表必须在 head.S 中构建,而不是在 Bootloader 中完成?原因如下:

考量因素 Bootloader 构建页表 head.S 构建页表
灵活性 受限于 Bootloader 的页表实现,难以适配不同内核配置 内核完全控制页表布局,支持 CONFIG_ARM64_VA_BITS 等编译选项
安全性 Bootloader 运行在 EL3/EL2,页表权限可能过于宽松 内核在 EL1 构建,可以精确控制用户态/内核态隔离
可移植性 不同厂商的 Bootloader 实现差异大,页表行为不统一 内核自带的页表构建代码是标准化的,跨平台一致
调试便利性 Bootloader 页表故障难以与内核调试工具联动 内核的 early_printk 和 debug_ll 可以直接用于页表调试
常见陷阱: 如果 Bootloader 错误地开启了 MMU 但未正确配置页表,或者跳转到内核时未关闭 MMU,head.S 将立即触发 Page Fault 或 TLB 不一致 异常。在调试时,务必确认进入 head.S 时 SCTLR_EL1.M 位为 0。

1.6 启动流程中的关键寄存器状态

在 head.S 执行页表构建之前,以下系统寄存器的状态至关重要:

  • SCTLR_EL1(系统控制寄存器):M 位(MMU 使能)必须为 0;C 位(Cache 使能)和 I 位(指令 Cache)通常也为 0。
  • TCR_EL1(翻译控制寄存器):控制页表粒度、地址空间大小、缓存策略等。head.S 会在创建页表前将其配置为内核期望的值。
  • MAIR_EL1(内存属性间接寄存器):定义内存区域属性(如 Normal、Device、Cacheable 等)。页表描述符中的 AttrIndx 字段指向 MAIR 中的条目。
  • TTBR0_EL1 / TTBR1_EL1(翻译表基址寄存器):分别指向用户空间和内核空间的页表基址。head.S 在创建页表后会将 TTBR1_EL1 设置为内核页表的物理地址。
调试技巧: 在 QEMU 或真实硬件上调试启动早期故障时,可以通过 JTAG 或硬件断点检查上述寄存器的值。例如,使用 info registers(QEMU monitor)或 md.l 命令查看 SCTLR_EL1 的十六进制值,确认 MMU 是否意外开启。

1.7 本章小结

ARM64 的启动流程从 ROM Code 开始,经过 Bootloader 的硬件初始化,最终到达内核的 head.S。页表的构建和 MMU 的使能发生在 head.S 的早期阶段,这是从“物理地址世界”切换到“虚拟地址世界”的关键转折点。理解每个阶段的异常等级、MMU 状态和页表状态,是后续深入分析页表构建代码和排查 MMU 故障的基础。