PAYATHON 2026

Raspberry Pi 3B 的 AArch64 内核如何正确切换到 EL1?

数据小许

直接跳到 el_check_el1 不会切换 Exception Level。如果入口处于 EL2,这条分支只会让程序继续在 EL2 执行。进入 EL1 需要配置目标执行状态和运行环境,再通过 eret 完成切换。

现有代码中,HCR_EL2.RW 和 SPSR_EL2 = 0x3c5 的设置与返回 AArch64 EL1h 的目标一致,但缺少明确的 SCTLR_EL1 初始化和 EL1 的 FP/SIMD 访问配置。仅凭灰屏无法确定故障点,也无法确认 framebuffer 初始化是否完整执行。

先确认切换前后的 CurrentEL

当前的无条件分支会跳过所有切换代码:

b el_check_el1

调试实际的切换路径时,需要移除这条分支,恢复根据 CurrentEL 分派的流程。CurrentEL 的原始值中,EL1 对应 0x4,EL2 对应 0x8;右移两位后分别是 1 和 2。

进入 C 后,可以读取当前级别:

static inline unsigned current_el(void)
{
    unsigned long value;
    __asm__ volatile("mrs %0, CurrentEL" : "=r"(value));
    return (unsigned)((value >> 2) & 3UL);
}

通过 UART 或调试器检查结果。原来的 boot_el 只记录进入切换流程之前的级别,多个核心又会写入同一个变量,因此不能用它确认每个核心切换后的状态。

eret 从 SPSR_EL2 恢复目标状态,并从 ELR_EL2 取得目标地址。SPSR_EL2 指定的 AArch64 状态还必须与 HCR_EL2.RW 一致。Arm 异常模型说明

补齐 EL1 的运行环境

EL2 和 EL1 使用各自的系统控制寄存器。代码能在 EL2 运行,不能说明进入 EL1 后的内存访问、栈和指令权限也已配置正确。

需要检查以下设置:

  • SCTLR_EL1:明确设置 EL1 的 MMU、缓存、字节序等状态,不能依赖未确认的启动值。
  • CPACR_EL1.FPEN:如果 C 代码或库函数会使用 FP/SIMD 指令,需要允许相应访问。
  • CPTR_EL2:控制低级别的 FP/SIMD 等访问是否陷入 EL2,并按处理器要求处理保留位。
  • SP_EL1:EL1h 使用该栈指针,调用 C 前必须为当前核心设置有效且满足 16 字节对齐要求的栈。
  • VBAR_EL1:安装有效的异常向量表,以便在异常发生时定位原因。

Cortex-A53 的 CPTR_EL2 有必须写为 1 的 RES1 位,因此需要调整原来的 msr cptr_el2, xzr。在该处理器上,可使用 0x33ff 保留这些位,同时清除 TFP 和 TCPAC。Cortex-A53 技术参考手册

修改启动汇编的示例

以下片段用于 Raspberry Pi 3B 的 Cortex-A53,假设当前启动环境采用小端、物理地址执行,且不依赖已经启用的 MMU 或缓存。该片段说明基本切换路径,尚未包含异常向量表实现,也没有经过该项目的硬件验证。

可以替换原来的 el_check、el_check_el2,新增 el1_setup,并保留原有的 el_check_el1 核心分派和各个 entryN_start:

el_check:
    mrs x0, CurrentEL
    lsr x0, x0, #2

    ldr x2, =boot_el
    str x0, [x2]

    cmp x0, #2
    beq el_check_el2
    cmp x0, #1
    beq el1_setup
    b hold

el_check_el2:
    // EL1 executes AArch64; stage 2 translation is disabled.
    mov x0, #(1 << 31)
    msr hcr_el2, x0

    // Permit EL1 access to the physical counter and timer.
    mrs x0, cnthctl_el2
    orr x0, x0, #3
    msr cnthctl_el2, x0
    msr cntvoff_el2, xzr

    // Cortex-A53 RES1 bits; no FP/SIMD trap to EL2.
    mov x0, #0x33ff
    msr cptr_el2, x0

    // Cortex-A53: required RES1 bits, little endian,
    // MMU and instruction/data caches disabled.
    ldr x0, =0x30d00800
    msr sctlr_el1, x0

    // Mask D, A, I, F; return to AArch64 EL1h.
    mov x0, #0x3c5
    msr spsr_el2, x0

    adr x0, el1_setup
    msr elr_el2, x0
    isb
    eret

el1_setup:
    // Select SP_EL1. entryN_start sets its value below.
    msr spsel, #1

    // Permit FP/SIMD accesses at EL1 and EL0.
    mrs x0, cpacr_el1
    orr x0, x0, #(3 << 20)
    msr cpacr_el1, x0
    isb

    // x1 still contains the core index.
    b el_check_el1

这里继续使用原来的 entryN_start 设置每个核心的栈。在抵达对应的 mov sp, x0 之前,不应插入函数调用或任何使用栈的代码。若要处理这段早期代码本身发生的异常,应进一步提前设置每核 SP_EL1 并安装异常向量表。

上述 SCTLR_EL1 和 CPTR_EL2 常量针对这里的处理器与启动假设。已有地址映射、缓存或其他执行状态时,需要按实际环境配置,不能直接套用。

为什么没有浮点运算也要检查 FP/SIMD

即使 C 源码没有浮点表达式,编译器仍可能使用 SIMD 寄存器完成结构体复制、清零或其他优化。仅清除 EL2 的相关陷阱,不能说明 EL1 的 CPACR_EL1.FPEN 已经允许访问。

如果异常记录显示 ESR_EL1.EC = 0x07,应检查故障指令和 FP/SIMD 访问配置。这个异常类别与相关访问陷阱有关。Arm ESR_EL1 寄存器说明

也可以暂时用 GCC 的 -mgeneral-regs-only 编译相关 C 代码,辅助判断故障是否与编译器生成的 FP/SIMD 指令有关。该选项不会约束手写汇编,预编译库也需要单独检查。GCC AArch64 编译选项

用异常信息定位灰屏

建议先只运行核心 0,暂缓写入其他核心的启动地址和执行 sev,等单核路径稳定后再恢复多核启动。每个核心都需要单独执行相关系统寄存器初始化。

在 eret 前、EL1 入口、kernel() 入口,以及 lfb_init()、sd_init()、fs_mount() 前后添加 UART 标记,先找出程序最后执行到的位置。

同时安装按 2 KiB 对齐的 EL1 异常向量表。发生同步异常时,至少记录:

  • ESR_EL1:异常类别及附加信息。
  • ELR_EL1:发生异常的指令地址。
  • FAR_EL1:对相应地址异常有效的故障地址。
  • SPSR_EL1:异常发生前的处理器状态。

0x3c5 虽然屏蔽了 D、A、I、F 对应的异常类别,但不会阻止同步异常。如果仍有陷入 EL2 的可能,也应准备 EL2 的异常记录。

kernel.c 是否需要修改

已展示的 kernel.c 没有明确的 EL2 专用系统寄存器访问,暂时没有依据要求重写它。可以先修正启动汇编,再结合异常地址检查实际调用到的驱动和底层函数。

如果故障指令访问了 EL2 专用寄存器,需要将该操作保留在 EL2 初始化阶段,或改用 EL1 可用的机制;如果是内存访问异常,则应检查 EL1 的映射、地址和设备访问属性。拿到切换后的 CurrentEL、最后一个执行标记和异常记录后,才能判断灰屏的具体原因。

备注:内容仅供参考。