2. 硬件初始化基础:ARM64处理器模式(EL0-EL3)、异常级别与启动权限、复位向量表(Reset Vector)解析

ARMv8-A 架构引入的异常级别(Exception Levels)是理解 Linux 内核启动流程的基石。与传统的 ARM32 处理器模式不同,ARM64 通过四个特权等级(EL0-EL3)实现了更严格的隔离和更清晰的启动权限划分。本节将深入剖析这些级别的定义、转换规则以及复位后的硬件行为。

2.1 ARM64 异常级别(EL0-EL3)概述

ARM64 定义了四个异常级别,数字越大,特权越高。每个级别拥有独立的虚拟内存配置(TTBR0_ELx/TTBR1_ELx)、堆栈指针(SP_ELx)以及异常向量表(VBAR_ELx)。

异常级别 名称 典型用途 执行权限
EL0 用户态(User) 应用程序(App) 最低,无法访问系统寄存器
EL1 内核态(Kernel) 操作系统内核(Linux Kernel) 可配置 MMU、中断、系统调用
EL2 虚拟机监视器(Hypervisor) KVM、Xen、Type-1 Hypervisor 虚拟化扩展,控制 VM 切换
EL3 安全监视器(Secure Monitor) ARM Trusted Firmware (ATF)、Secure Boot 最高,控制安全世界切换
关键概念: 在典型的非虚拟化 Linux 启动场景中,系统从 EL3 开始执行固件,然后切换到 EL1 运行内核。如果启用了 KVM,则 EL2 的 Hypervisor 层会介入管理虚拟机。

2.2 异常级别与启动权限

复位(Reset)后,ARM64 处理器默认进入最高的异常级别——EL3。这是启动流程的起点,也是硬件初始化权限最高的阶段。不同异常级别下,可访问的寄存器集合和系统控制能力存在显著差异。

  • EL3 权限: 可以配置 SCR_EL3(安全配置寄存器),决定是否启用 EL2、EL1 的 AArch32 支持,以及设置安全世界状态。这是 TrustZone 技术的核心。
  • EL2 权限: 可以配置 HCR_EL2(虚拟化配置寄存器),控制虚拟内存系统架构(VMSA)的虚拟化行为,并决定是否将某些异常路由到 EL2。
  • EL1 权限: 可以配置 SCTLR_EL1(系统控制寄存器),启用 MMU、缓存、对齐检查等。Linux 内核主要工作在此级别。
  • EL0 权限: 无法访问任何系统控制寄存器,只能通过 SVC(Supervisor Call)指令陷入 EL1 请求内核服务。
启动权限降级路径: 典型的启动流程遵循严格的权限降级:
EL3 (ATF/BL1) → EL2 (Hypervisor/BL2) → EL1 (Linux Kernel) → EL0 (User App)
每次降级通过 ERET 指令实现,该指令会从当前异常级别的保存状态寄存器(SPSR_ELx)中恢复目标异常级别。

2.3 复位向量表(Reset Vector)解析

ARM64 的异常向量表(Vector Table)是处理所有异常(包括复位、中断、同步异常)的入口点。每个异常级别都有自己独立的向量表,基地址由 VBAR_ELx 寄存器指定。

复位向量是向量表中的第一个条目,它定义了处理器上电或复位后执行的第一条指令的地址。在 ARM64 中,复位向量的地址由硬件实现定义,通常位于 ROM 或固定的物理地址(如 0x00000000 或 0xFFFF0000)。

/* ARM64 异常向量表结构(每个条目 128 字节) */
.align 11          /* 2^11 = 2048 字节对齐 */
.global vectors
vectors:
    /* 当前异常级别,使用 SP_EL0 */
    .align 7        /* 2^7 = 128 字节对齐 */
    .ventry  el1_sync_invalid      // 同步异常 (Synchronous)
    .ventry  el1_irq_invalid       // IRQ 中断
    .ventry  el1_fiq_invalid       // FIQ 中断
    .ventry  el1_error_invalid     // SError 错误

    /* 当前异常级别,使用 SP_ELx */
    .align 7
    .ventry  el1_sync              // 同步异常 (如系统调用)
    .ventry  el1_irq               // IRQ 中断
    .ventry  el1_fiq               // FIQ 中断
    .ventry  el1_error             // SError 错误

    /* 低异常级别(AArch64),使用 SP_EL0 */
    .align 7
    .ventry  el0_sync_64           // 来自 EL0 的同步异常
    .ventry  el0_irq_64            // 来自 EL0 的 IRQ
    .ventry  el0_fiq_64            // 来自 EL0 的 FIQ
    .ventry  el0_error_64          // 来自 EL0 的 SError

    /* 低异常级别(AArch32),使用 SP_EL0 */
    .align 7
    .ventry  el0_sync_32           // 来自 AArch32 EL0 的同步异常
    .ventry  el0_irq_32            // 来自 AArch32 EL0 的 IRQ
    .ventry  el0_fiq_32            // 来自 AArch32 EL0 的 FIQ
    .ventry  el0_error_32          // 来自 AArch32 EL0 的 SError
避坑指南: 向量表必须严格 2KB 对齐(.align 11),且每个条目必须 128 字节对齐(.align 7)。如果对齐错误,处理器在发生异常时会跳转到错误地址,导致不可预测的行为。在启动汇编代码中,务必在定义 vectors 标签前使用 .align 11。

2.4 复位后的硬件状态与启动流程

当 ARM64 处理器从复位中唤醒时,硬件处于一个确定但极简的状态。理解这个初始状态对于编写启动代码至关重要。

  • 异常级别: EL3(最高特权)。
  • MMU: 关闭(指令和数据缓存可能处于未知状态,通常需要 invalidate)。
  • 堆栈指针: SP_EL3 的值由硬件复位值决定,通常指向一个安全 RAM 区域。
  • 向量表: VBAR_EL3 指向复位向量地址。
  • 通用寄存器: 大部分寄存器(X0-X30)的值是未知的,除了 X0 可能包含一些平台特定的信息(如 DTB 地址)。

下图展示了从复位到 Linux 内核入口的典型权限切换流程:

ARM64 启动流程:异常级别切换 ARM64 启动流程:异常级别切换与权限降级 复位 (Reset) EL3 / ROM Code ERET ATF / BL1 EL3 固件初始化 ERET U-Boot / BL2 EL2 或 EL1 ERET Linux Kernel EL1 权限降级 User App EL0 ERET 关键说明 • 复位后强制进入 EL3 • 每次切换使用 ERET 指令 • 权限只能降级,不能升级

2.5 启动权限的硬件约束

ARM64 架构对异常级别的切换施加了严格的硬件约束,这些约束直接影响了启动代码的设计:

操作 硬件约束 影响
权限提升 只能通过异常(Exception)实现 用户态无法直接切换到内核态,必须通过 SVC 指令触发同步异常
权限降级 只能通过 ERET 指令实现 内核返回用户态时,必须从 SPSR_EL1 恢复目标级别
EL3 不可绕过 复位后强制进入 EL3 所有启动流程必须经过 EL3 固件(如 ATF)的初始化
向量表独立 每个 EL 拥有独立的 VBAR_ELx 不同异常级别的异常处理程序完全隔离
实践要点: 在编写启动汇编代码时,务必在切换异常级别前完成以下操作:
1. 配置当前级别的系统寄存器(如 SCTLR_EL3、SCR_EL3)。
2. 设置目标异常级别的堆栈指针(SP_ELx)。
3. 将目标异常级别的入口地址写入 ELR_ELx(异常链接寄存器)。
4. 将目标异常级别和状态写入 SPSR_ELx。
5. 执行 ERET 指令完成切换。

2.6 复位向量表与启动加载器交互

在实际的嵌入式系统中,复位向量表通常位于 SoC 内部 ROM 中。上电后,ROM 代码会执行基本的硬件初始化(如时钟、DDR),然后跳转到下一个启动介质(如 eMMC、NAND Flash)中的启动加载器。

以典型的 ARM64 开发板(如 Raspberry Pi 4 或 QEMU virt 平台)为例,复位后的执行流如下:

  1. ROM 阶段: 处理器从复位向量地址(如 0x00000000)开始执行。ROM 代码初始化关键外设,并将完整的启动加载器(如 U-Boot SPL)加载到 SRAM 中。
  2. SPL 阶段: 运行在 EL3 的 SPL 代码初始化 DDR 控制器,然后将主启动加载器(U-Boot proper)从存储介质复制到 DDR 中。
  3. U-Boot 阶段: U-Boot 通常运行在 EL2 或 EL1。它加载设备树(DTB)和 Linux 内核镜像到内存,然后通过 booti 命令跳转到内核入口点。
  4. 内核阶段: Linux 内核在 EL1 启动,完成架构初始化、内存管理、调度器初始化等,最终启动 init 进程。
/* 典型的 U-Boot 跳转到内核的汇编片段 (arch/arm/lib/board.c) */
void boot_jump_linux(bootm_headers_t *images, int flag)
{
    unsigned long machid = images->os.arch;
    unsigned long fdt_addr = images->ft_addr;
    unsigned long kernel_entry = images->ep;

    /* 清理缓存并关闭 MMU */
    cleanup_before_linux();

    /* 设置内核启动参数 */
    printf("## Starting kernel at 0x%lx ...\n", kernel_entry);

    /* 跳转到内核入口点 (EL1) */
    kernel_entry(kernel_entry, machid, fdt_addr);
}
避坑指南: 在跳转到内核之前,必须确保关闭 MMU 和缓存(data cache 和 instruction cache),因为内核在启动早期会重新配置页表。如果缓存未清理,可能导致内核读取到过时的指令或数据,造成启动失败。通常使用 flush_dcache_all 和 invalidate_icache_all 来保证一致性。