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(所有安全相关控制关闭)

1.3 从 Reset 到 Boot ROM 的流程

下图展示了从 CPU 复位到 Boot ROM 执行完毕的典型流程,包含关键决策点:

ARM64 Reset 到 Boot ROM 流程 ARM64 Reset → Boot ROM 执行流程 上电 / 复位信号 硬件初始化(EL3) 读取 RVBAR 地址 跳转 Boot ROM 入口 加载启动设备中的下一级镜像 说明: RVBAR 地址由 SoC 设计 决定,常见为 0x0000_0000 或 0xFFFF_0000。 调试提示: 在 Boot ROM 阶段, 通常无法设置断点, 需使用硬件触发。

1.4 引导加载器阶段:设备树的首次登场

当 Boot ROM 将 SPL 或 U-Boot 加载到 SRAM 后,引导加载器开始执行。此阶段的关键任务包括:

  1. DRAM 初始化:通过读取硬件寄存器或 FUSE 配置,完成 DDR 控制器的时序训练。
  2. 设备树加载:从存储介质(FAT 分区、eMMC 分区)读取 .dtb 文件到内存。
  3. 设备树解析:U-Boot 使用 fdt 命令或内部 API 解析设备树,获取内存布局、外设地址等。
  4. 内核镜像加载:将压缩后的内核镜像(Image.gz)解压到指定地址。
  5. 跳转到内核:设置启动参数(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 级(取决于配置),典型结构如下:

ARM64 早期页表结构 ARM64 早期页表结构(4 级) PGD 页全局目录 PUD 页上级目录 PMD 页中间目录 PTE 页表条目 早期启动页表特点: • 使用块映射(Block Mapping),PMD 级映射 2MB,PUD 级映射 1GB • 恒等映射与内核映射共享同一组页表,通过不同虚拟地址区分 • 页表内存分配在 .idmap.text 段,确保开启 MMU 前可访问 • 调试时检查 TTBR0_EL1 与 TTBR1_EL1 的值,确认页表基址

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 命令显示的地址对比。若不一致,说明启动参数传递出错。

1.8 本章小结