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 或类似的入口函数,其核心逻辑包括:
- 设置
SP_ELx堆栈指针。 - 保存 FDT 地址到
__fdt_pointer。 - 检查当前异常等级,并决定是否需要降级(EL2 → EL1)。
- 调用
__create_page_tables构建初始页表。 - 调用
__enable_mmu使能 MMU。
1.4 启动阶段与页表的关系
页表的构建时机与启动阶段紧密相关。下图展示了从 ROM Code 到 head.S 页表构建的完整流程与依赖关系:
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 故障的基础。