1. ARM64 启动流程概述:从 Reset 到 C 语言环境的建立
ARM64 架构的启动过程是从硬件复位到高级操作系统运行的桥梁。理解这一宏观阶段,是掌握设备树解析与内存映射调试的前提。本章将梳理从 CPU 上电复位到 C 语言环境建立的完整路径,明确各阶段的核心任务与数据流。
1.1 启动的宏观阶段划分
ARM64 启动通常可划分为以下四个宏观阶段,每个阶段都有明确的入口与出口条件:
| 阶段 | 入口条件 | 核心任务 | 出口标志 |
|---|---|---|---|
| Reset 阶段 | 硬件上电或复位信号 | 异常向量表定位、CPU 模式初始化、缓存/MMU 关闭 | 跳转到 Boot ROM 或固化引导代码 |
| Boot ROM / 一级引导 | Reset 向量执行完毕 | 加载启动设备(eMMC/NAND/UART)中的下一级镜像 | 跳转到 SPL 或 U-Boot 入口 |
| 引导加载器(U-Boot) | SPL 或 Boot ROM 完成加载 | 初始化 DRAM、解析设备树、加载内核镜像 | 跳转到内核入口(_text) |
| 内核早期启动 | 内核入口点 | 建立页表、开启 MMU、设置 C 语言栈 | 进入 start_kernel() |
调试要点: 在 Reset 阶段,所有调试器(JTAG/SWD)必须能捕获到 CPU 从 0x0000_0000 或 RVBAR 地址取指的动作。若无法停住,检查复位信号与电源时序。
1.2 Reset 阶段:CPU 的初始状态
ARM64 处理器在上电或复位后,硬件自动完成以下初始化动作:
- 异常等级(Exception Level):进入 EL3(安全最高权限),若 SoC 未实现 EL3,则进入 EL2。
- MMU 与缓存:MMU 关闭(SCTLR_ELx.M=0),I-cache 与 D-cache 关闭,所有地址视为物理地址。
- 栈指针:SP_EL3 或 SP_EL2 初始化为实现定义的值,通常为 0 或复位向量附近。
- 异常向量基址:VBAR_EL3 指向 Reset 向量表,通常位于 SoC 的 Boot ROM 区域。
关键寄存器状态(Reset 后):
- PC = RVBAR_EL3(实现定义,通常为 0x0000_0000 或 0xFFFF_0000)
- SCTLR_EL3 = 0x0000_0018(仅保留 M 和 C 位清零,其余位复位)
- SCR_EL3 = 0x0000_0000(所有安全相关控制关闭)
- PC = RVBAR_EL3(实现定义,通常为 0x0000_0000 或 0xFFFF_0000)
- SCTLR_EL3 = 0x0000_0018(仅保留 M 和 C 位清零,其余位复位)
- SCR_EL3 = 0x0000_0000(所有安全相关控制关闭)
1.3 从 Reset 到 Boot ROM 的流程
下图展示了从 CPU 复位到 Boot ROM 执行完毕的典型流程,包含关键决策点:
1.4 引导加载器阶段:设备树的首次登场
当 Boot ROM 将 SPL 或 U-Boot 加载到 SRAM 后,引导加载器开始执行。此阶段的关键任务包括:
- DRAM 初始化:通过读取硬件寄存器或 FUSE 配置,完成 DDR 控制器的时序训练。
- 设备树加载:从存储介质(FAT 分区、eMMC 分区)读取
.dtb文件到内存。 - 设备树解析:U-Boot 使用
fdt命令或内部 API 解析设备树,获取内存布局、外设地址等。 - 内核镜像加载:将压缩后的内核镜像(
Image.gz)解压到指定地址。 - 跳转到内核:设置启动参数(x0 = 设备树地址,x1 = 内核自身地址),执行
br指令。
常见陷阱: 设备树地址必须 8 字节对齐,且不能与内核镜像加载地址重叠。若设备树被内核覆盖,会导致内存节点解析失败,系统 panic。
1.5 内核早期启动:建立 C 语言环境
内核入口点 _text 位于 arch/arm64/kernel/head.S。此阶段完全由汇编语言实现,直到调用 start_kernel() 才进入 C 语言世界。核心步骤包括:
| 步骤 | 汇编代码位置 | 操作内容 |
|---|---|---|
| 1. 保存启动参数 | head.S:preserve_boot_args | 将 x0(设备树地址)、x1(机器类型)保存到全局变量 |
| 2. 建立页表 | head.S:__create_page_tables | 创建恒等映射(identity map)与内核映射,使用 2MB/1GB 块 |
| 3. 开启 MMU | head.S:__enable_mmu | 设置 SCTLR_EL1.M=1,刷新 TLB,启用虚拟地址 |
| 4. 设置 C 栈 | head.S:__primary_switch | 初始化 SP_EL1 指向 init_thread_union 栈顶 |
| 5. 跳转 C 入口 | head.S:__primary_switched | 调用 start_kernel(),传递设备树地址 |
1.6 页表建立与 MMU 开启的细节
在开启 MMU 之前,内核必须建立至少两个映射:
- 恒等映射(Identity Map):将当前执行地址(物理地址)映射到相同的虚拟地址,确保开启 MMU 瞬间指令流不中断。
- 内核映射(Kernel Map):将内核镜像映射到高地址(如
0xFFFF_8000_0000_0000),供后续 C 代码使用。
页表层级使用 4 级或 3 级(取决于配置),典型结构如下:
1.7 进入 C 语言环境:start_kernel() 的调用
当 MMU 开启、栈指针设置完毕后,内核执行以下汇编指令跳转到 C 入口:
/* arch/arm64/kernel/head.S - __primary_switched */
adrp x29, init_thread_union
add sp, x29, #THREAD_SIZE
stp xzr, x30, [sp, #-16]! /* 保存返回地址 */
mov x0, x20 /* x20 保存设备树地址 */
bl start_kernel /* 进入 C 语言世界 */
此时,x0 寄存器中存放的是设备树二进制块(DTB)的物理地址。内核的 setup_arch() 函数将立即解析该地址,提取内存节点、CPU 节点与中断控制器信息。
调试检查点: 在
start_kernel() 入口处,使用 JTAG 读取 x0 寄存器的值,并与 U-Boot 中 fdt addr 命令显示的地址对比。若不一致,说明启动参数传递出错。