1. ARM64 启动流程概述:从 Reset 到 C 语言环境的建立
ARM64 架构的启动过程是一个从硬件复位到高级语言环境建立的精密链条。在系统上电或复位后,CPU 从 EL3(异常级别 3)的复位向量开始执行,经过一系列初始化步骤,最终建立起可供 C 语言运行的栈、BSS 段清零、数据段重定位等环境。本章节聚焦于这一流程的整体脉络,并明确 head.S 在启动链中的核心位置与职责。
1.1 启动流程全景图
下图展示了从 Reset 向量到 C 语言入口 start_kernel 的完整启动阶段。每个阶段都有明确的硬件状态要求和软件职责。
关键概念: ARM64 启动必须经历“异常级别降级”过程——从最高权限的 EL3 逐步下降到目标 OS 运行的 EL1(内核态)或 EL0(用户态)。head.S 通常运行在 EL2(虚拟化扩展)或 EL1,负责最底层的硬件初始化。
1.2 启动阶段详细分解
ARM64 启动流程可以拆解为四个主要阶段,每个阶段都有明确的输入、输出和硬件依赖。下表总结了各阶段的关键特征:
| 阶段 | 异常级别 | 主要任务 | 关键寄存器/硬件 | 输出状态 |
|---|---|---|---|---|
| Reset | EL3 | 硬件复位、读取复位向量、最小系统配置 | RVBAR_EL3、SCR_EL3 | CPU 处于最高特权级,Cache 可能关闭 |
| Bootloader | EL3 / EL2 | DDR 初始化、时钟配置、加载内核镜像到内存 | DDR 控制器、GIC、UART | 内存可用,内核镜像已加载到物理地址 |
| head.S | EL2 / EL1 | 异常向量表配置、页表建立、MMU 使能、栈设置 | VBAR_ELx、TTBR0_ELx、SCTLR_ELx | MMU 开启,虚拟地址空间就绪,栈可用 |
| C 环境 | EL1 | BSS 清零、数据段重定位、调用 start_kernel | SP、BSS 起始/结束符号 | 纯 C 语言环境,可执行复杂逻辑 |
1.3 head.S 在启动链中的位置与作用
head.S 是 ARM64 Linux 内核启动的第一个汇编文件,通常位于 arch/arm64/kernel/head.S。它在启动链中扮演着“硬件抽象层”的角色——将 Bootloader 传递的原始硬件状态,转换为 C 语言环境所需的标准化状态。
调试视角: 在 head.S 阶段,由于 MMU 可能尚未开启或处于过渡状态,所有地址访问必须使用物理地址。调试器(如 JTAG)在此阶段可以直接观察物理内存和系统寄存器,是排查启动早期崩溃的关键窗口。
1.3.1 head.S 的核心职责清单
- 异常向量表安装: 将异常向量表的物理地址写入
VBAR_EL2或VBAR_EL1,确保中断、同步异常、SError 等能正确路由。 - 页表初始化: 建立恒等映射(identity mapping)和内核虚拟地址映射,配置
TTBR0_ELx和TTBR1_ELx。 - MMU 使能: 设置
SCTLR_ELx.M位,开启 MMU,同时配置 Cache 和分支预测。 - 栈指针设置: 为每个 CPU 核初始化独立的栈空间,写入
SP_ELx。 - 异常级别切换: 通过
eret指令从 EL2 降级到 EL1,或从 EL1 进入内核主入口。 - CPU 特性检测: 读取
MIDR_EL1、ID_AA64MMFR0_EL1等寄存器,识别 CPU 型号和特性。
1.3.2 head.S 与前后阶段的接口
head.S 与 Bootloader 和 C 语言环境之间通过约定的寄存器传递参数。下表列出了标准的参数传递约定:
| 寄存器 | 方向 | 含义 | 典型值 |
|---|---|---|---|
x0 |
Bootloader → head.S | 设备树 (DTB) 的物理地址 | 0x4000_0000 |
x1 |
Bootloader → head.S | 内核镜像的物理地址 | 0x8008_0000 |
x2 |
Bootloader → head.S | CPU 编号(主核为 0) | 0x0 |
x3 |
Bootloader → head.S | FDT 或 ACPI 表地址 | 0x4000_0000 |
x21 |
head.S → C 环境 | CPU 编号(经过验证) | 0x0 |
x22 |
head.S → C 环境 | 设备树地址(经过验证) | 0x4000_0000 |
1.4 典型启动代码片段分析
以下是从 head.S 中提取的关键代码片段,展示了异常向量表配置和栈初始化的典型实现:
/*
* 异常向量表基地址配置 (VBAR_EL2)
* 将异常向量表的物理地址写入 VBAR_EL2
*/
__setup_vector:
adrp x0, vectors // 获取 vectors 标签的页基地址
add x0, x0, #:lo12:vectors // 加上页内偏移,得到完整物理地址
msr vbar_el2, x0 // 写入 VBAR_EL2
isb // 指令同步屏障,确保生效
/*
* 栈指针初始化 (SP_EL2)
* 为当前 CPU 分配独立的栈空间
*/
__setup_stack:
mrs x0, mpidr_el1 // 读取多核处理器 ID
and x0, x0, #0xff // 取低 8 位作为 CPU 编号
ldr x1, =_stack_end // 栈结束地址(高地址)
mov x2, #0x1000 // 每个 CPU 栈大小 4KB
mul x3, x0, x2 // CPU 编号 × 栈大小
sub x4, x1, x3 // 栈顶 = 栈结束 - 偏移
mov sp, x4 // 设置 SP_EL2
msr sp_el1, x4 // 同时设置 SP_EL1(后续降级使用)
常见陷阱: 在 head.S 阶段,如果异常向量表尚未配置就发生了中断或同步异常(如未对齐访问),CPU 将陷入 “无向量异常” 状态,表现为 CPU 挂起或跳转到未知地址。调试时,应优先检查 VBAR_ELx 是否已正确写入,以及向量表首条指令是否为有效的跳转或处理逻辑。
1.5 启动流程中的关键检查点
在调试启动早期问题时,以下检查点可以帮助快速定位故障阶段:
- Reset 后第一条指令: 检查 CPU 是否从预期的物理地址开始执行(通常由硬件复位向量决定)。
- 异常向量表基地址: 在 head.S 执行
msr vbar_elx, x0后,通过 JTAG 读取VBAR_ELx确认地址正确。 - MMU 使能前后: 在
SCTLR_ELx.M置位前后,观察指令地址是否从物理地址切换到虚拟地址。 - 栈指针有效性: 在跳转到 C 函数前,确认
SP指向可读写的内存区域,且对齐到 16 字节。 - 异常级别切换: 执行
eret后,检查CurrentEL寄存器确认是否已降级到目标异常级别。
1.6 总结:head.S 的不可替代性
在 ARM64 启动链中,head.S 是唯一一个同时操作系统寄存器、页表、异常向量和栈的组件。它承担了从“硬件裸机状态”到“C 语言运行时”的桥梁作用。任何启动早期的崩溃、异常或挂起,都应首先检查 head.S 的执行路径是否正确,尤其是异常向量表配置和 MMU 使能这两个高风险步骤。
调试黄金法则: 在 head.S 阶段,优先使用物理地址调试。如果 MMU 已开启但配置错误,CPU 可能立即陷入缺页异常,而此时异常向量表可能尚未正确安装,导致“双重故障”死锁。建议在 MMU 使能前设置硬件断点,单步验证页表条目是否正确。