RASPI裸机7(exceptions)

1 树莓派3的异常处理机制

树莓派3B(RPi3B)采用的是Broadcom BCM2837芯片,其内核为四核ARM Cortex‑A53,支持 ARMv8/AArch64 64 位架构。所以整体来说这里的异常机制就是ARMv8的异常机制。

首先要理解的是什么是异常(exceptions)?这里虽然叫做异常,但有和一般理解的异常就是出错不同,这里ARMv8的异常其实也包含了事件或者资源调度,需要跳转到下级处理的,有一些则是系统崩溃了,只能给出一个提示的。

1.1 和ARM Cortex-M区别

之前看过pico的裸机,感觉也没什么太特别的,就是一个中断向量表。具体可以看https://blog.csdn.net/fanged/article/details/156105747

但是查了一下资料,才发现A系列和M系列两者区别还是挺大的。

特性 树莓派 Pico (RP2040) 树莓派 3B (BCM2837)
内核 ARM Cortex-M0+ (双核) ARM Cortex-A53 (四核)
指令集 ARMv6-M (仅 32 位 Thumb) ARMv8-A (支持 64 位 AArch64)
异常级别 仅 Privileged / Unprivileged EL0, EL1, EL2, EL3 (四级权限)
内存管理 无 MMU(直接访问物理地址) 拥有 MMU(虚拟内存地址映射)

树莓派 Pico:简单线性的 NVIC

Pico 的异常处理由 NVIC(嵌套向量中断控制器) 管理。

  • 结构: 向量表非常直接,表里存放的是函数指针(地址)

  • 触发逻辑: 硬件发生中断 -> 查表找到地址 -> 自动压栈寄存器 -> 跳入函数。

  • 感知: 对于开发者来说,就是一个向量表,只需要把中断服务函数的地址填进去即可。

Pico (MCU): 异常处理是确定性的。因为没有 MMU,中断发生到执行第一行代码的时间几乎是固定的(Cycles 级别)。

树莓派 3B:分层分组的跳转表

3B 的向量表(VBAR)存放的是代码指令(通常是 32 字节或 128 字节一段),而不是函数指针。

  • 结构: 表被分成了 4 组(对应当前 EL、低级 EL 等),每组包含 4 种异常类型(Synchronous, IRQ, FIQ, SError)。

  • 触发逻辑: 发生异常 -> PC 直接跳到向量表对应偏移处执行代码。由于只有 128 字节,通常这里写的是一条 b handle_irq 指令。

  • 权限切换: 3B 必须处理权限跨越(例如EL0的用户代码报错,硬件要自动切到 EL1 的内核代码)。

树莓派 3B (SoC): 异常处理极其复杂。发生异常时,硬件可能需要处理:

  • TLB Miss: 访问内存异常时,可能还要去查页表。

  • Cache 一致性: 多核环境下,异常处理可能涉及缓存失效或同步。

  • Pipeline Flush: A53 的流水线比 M0+ 深得多,异常会导致大规模的流水线排空。

此外,四个 A53 核心共享一个全局中断控制器,但每个核心有自己的本地中断(Local IRQ)。异常分发需要经过更复杂的路由(Routing)逻辑。

1.2 树莓派3B的运行等级

树莓派3B具有4个用户等级,在初始化,以及异常发生的时候都会涉及。比如EL0的异常,会跳转到EL1处理。这次这个示例程序没有做那么复杂,都是跑在EL1。这四个等级如下:

Level 用途 运行 权限
EL0 用户程序 App、进程、用户程序 不能访问特权寄存器, MMU、Cache、中断配置等。硬件只能通过syscall或者异常进入更高 EL
EL1 操作系统 Linux 内核、RTOS 内核 管理虚拟内存(MMU/Translation Table),异常 / 中断处理,外设、Cache、电源管理等。
EL2 Hypervisor,虚拟化态(Hypervisor) Hypervisor(KVM、Xen、QEMU 管理层) 隔离多个虚拟机(VM),截获 VM 对硬件的访问。
EL3 Secure Monitor,安全世界态(Secure Monitor / TF‑A) ARM Trusted Firmware‑A(TF‑A) 安全世界/普通世界切换,启动时初始化硬件,提供安全服务(如加解密、密钥、指纹)。 任何世界都不能直接访问 EL3,只能通过SMC异常进入。

获取运行等级可以取寄存器CurrentEL,代码如下:

cpp 复制代码
asm volatile ("mrs %0, CurrentEL" : "=r" (el));

uart_puts("Current EL is: ");
uart_hex((el>>2)&3);
uart_puts("\n");

在异常发生时,如果要切换运行级别:

1 异常进入(从低 → 高)

只能通过中断,系统调用(svc #0),指令错误、缺页、未定义指令,安全调用(smc)→ 进 EL3。

2 异常返回(从高 → 低)

指令:eret,自动恢复 PSTATE 状态(SP、DAIF、EL 等级)。

1.3 等级的启动流程

典型的非安全世界启动流程:

1 CPU 上电 → EL3

2 EL3 初始化安全 → 跳转到 EL1(Linux)

3 EL1 初始化 MMU、中断 → 运行用户程序 → EL0

4 EL0 发系统调用 / 中断 → 自动进 EL1

5 EL1 处理完 → eret 回 EL0

如果有虚拟化的时候则是EL0 → EL1 → EL2 → EL3。

1.4 核心寄存器和汇编指令

异常处理核心寄存器:

寄存器名称 全称 所属级别 核心功能描述
CurrentEL Current Exception Level 所有 只读。获取当前运行的 EL 级别(如 EL1, EL2 等)。
SCTLR_ELx System Control Register EL1/2/3 系统控制。控制 MMU 开启/关闭、数据/指令缓存(Cache)、对齐检查等。
SPSR_ELx Saved Program Status Register EL1/2/3 状态备份。当异常发生时,自动保存上一个级别的 PSTATE(如条件标志、屏蔽位)。
ELR_ELx Exception Link Register EL1/2/3 返回地址 。保存触发异常时的指令地址,用于 eret 返回。
VBAR_ELx Vector Base Address Register EL1/2/3 向量表基址。存放异常向量表的起始地址(必须 2KB 对齐)。
ESR_ELx Exception Syndrome Register EL1/2/3 异常原因。记录触发异常的具体类型(如系统调用、指令异常、数据中止)。
FAR_ELx Fault Address Register EL1/2/3 故障地址。当发生内存访问失败(如 Page Fault)时,记录出错的虚拟地址。
HCR_EL2 Hypervisor Configuration Register EL2 虚拟化配置。控制 EL1 是否运行在 AArch64 模式,以及异常是否路由到 EL2。
SCR_EL3 Secure Configuration Register EL3 安全配置。定义安全态/非安全态切换,以及 EL2/EL1 的执行模式。

异常处理关键汇编指令,主要是前四个:

指令 全称 用法示例 功能描述
SVC Supervisor Call svc #0 系统调用。用户态 (EL0) 请求内核 (EL1) 服务的唯一标准手段。
HVC Hypervisor Call hvc #0 虚拟化调用。EL1 请求 EL2 (Hypervisor) 服务。
SMC Secure Monitor Call smc #0 安全调用。非安全态请求 EL3 (Secure Monitor) 切换到安全态。
ERET Exception Return eret 异常返回 。利用 ELR_ELxSPSR_ELx 恢复状态并实现降级跳转
MRS Move System Reg to General mrs x0, CurrentEL 读取系统寄存器到通用寄存器。
MSR Move General to System Reg msr vbar_el1, x2 写入通用寄存器值到系统寄存器。
WFI / WFE Wait for Interrupt/Event wfi 低功耗挂起。停止 CPU 执行直到收到中断或特定事件。
MSR DAIFSet Mask Interrupts msr daifset, #2 屏蔽中断。快速屏蔽/开启 IRQ (#2) 或 FIQ (#1)。

2 代码

依然来自:https://github.com/bztsrc/raspi3-tutorial/tree/master/11_exceptions

start.S

这次这里分成两个部分。

3.1 初始化

第一个是初始化,这部分的流程和1.3的差不多。首先进入时状态是EL3/EL2,降级到EL1,之后初始化BSS,最后进入main。因为这里是裸机演示程序,而且需要操作硬件,所以这里最终并没有进入EL0,而是停在了EL1。

cpp 复制代码
.section ".text.boot"

.global _start

_start:
    // read cpu id, stop slave cores
    mrs     x1, mpidr_el1
    and     x1, x1, #3
    cbz     x1, 2f
    // cpu id > 0, stop
1:  wfe
    b       1b
2:  // cpu id == 0

    // set top of stack just before our code (stack grows to a lower address per AAPCS64)
    ldr     x1, =_start

    // set up EL1
    mrs     x0, CurrentEL
    and     x0, x0, #12 // clear reserved bits

    // running at EL3?
    cmp     x0, #12
    bne     5f
    // should never be executed, just for completeness
    mov     x2, #0x5b1
    msr     scr_el3, x2
    mov     x2, #0x3c9
    msr     spsr_el3, x2
    adr     x2, 5f
    msr     elr_el3, x2
    eret

    // running at EL2?
5:  cmp     x0, #4
    beq     5f
    msr     sp_el1, x1
    // enable CNTP for EL1
    mrs     x0, cnthctl_el2
    orr     x0, x0, #3
    msr     cnthctl_el2, x0
    msr     cntvoff_el2, xzr
    // enable AArch64 in EL1
    mov     x0, #(1 << 31)      // AArch64
    orr     x0, x0, #(1 << 1)   // SWIO hardwired on Pi3
    msr     hcr_el2, x0
    mrs     x0, hcr_el2
    // Setup SCTLR access
    mov     x2, #0x0800
    movk    x2, #0x30d0, lsl #16
    msr     sctlr_el1, x2
    // set up exception handlers
    ldr     x2, =_vectors
    msr     vbar_el1, x2
    // change execution level to EL1
    mov     x2, #0x3c4
    msr     spsr_el2, x2
    adr     x2, 5f
    msr     elr_el2, x2
    eret

5:  mov     sp, x1

    // clear bss
    ldr     x1, =__bss_start
    ldr     w2, =__bss_size
3:  cbz     w2, 4f
    str     xzr, [x1], #8
    sub     w2, w2, #1
    cbnz    w2, 3b

    // jump to C code, should not return
4:  bl      main
    // for failsafe, halt this core too
    b       1b

    // important, code has to be properly aligned
    .align 11

3.2 异常向量表

第二个部分是提供了异常的向量表,这里主要就是直接将异常寄存器的内容取出,封装到函数exc_handler中处理。

cpp 复制代码
_vectors:
    // synchronous
    .align  7
    mov     x0, #0
    mrs     x1, esr_el1
    mrs     x2, elr_el1
    mrs     x3, spsr_el1
    mrs     x4, far_el1
    b       exc_handler

    // IRQ
    .align  7
    mov     x0, #1
    mrs     x1, esr_el1
    mrs     x2, elr_el1
    mrs     x3, spsr_el1
    mrs     x4, far_el1
    b       exc_handler

    // FIQ
    .align  7
    mov     x0, #2
    mrs     x1, esr_el1
    mrs     x2, elr_el1
    mrs     x3, spsr_el1
    mrs     x4, far_el1
    b       exc_handler

    // SError
    .align  7
    mov     x0, #3
    mrs     x1, esr_el1
    mrs     x2, elr_el1
    mrs     x3, spsr_el1
    mrs     x4, far_el1
    b       exc_handler

可以看到,正如第一部分所写,异常分为多个种类,在这里将各种参数压到函数栈中。

寄存器 代码中的位置 名称 核心含义:它能告诉你什么?
x0 mov x0, #1 自定义分类标签 "谁触发的?" 这是软件人为定义的编号(0:Sync, 1:IRQ...)。因为硬件已经分流了,通过这个手动设置的值,后续程序能直接区分异常大类。
x1 mrs x1, esr_el1 Exception Syndrome Register (异常综合寄存器) "发生了什么事?" 包含异常的具体原因(EC 码)。例如:是指令被禁止访问、还是发生了系统调用 svc?(注:对 IRQ 而言此值通常无效)。
x2 mrs x2, elr_el1 Exception Link Register (异常链接寄存器) "在哪里发生的?" 保存了异常发生时的指令地址(PC) 。当处理完异常执行 eret 时,CPU 会回到这个地址继续执行。
x3 mrs x3, spsr_el1 Saved Program Status Register (保存的程序状态寄存器) "当时的状态如何?" 保存了异常发生瞬间的 CPU 状态(如 PSTATE),包括:当时的权限等级、中断屏蔽位、算术标志位等。
x4 mrs x4, far_el1 Fault Address Register (故障地址寄存器) "访问了哪个非法地址?" 仅在内存访问出错(如 Data Abort)时有效。它记录了导致出错的那个内存虚拟地址

这些内容在ARM官网都能查到。

3.3 异常处理

最后的异常都换到C代码中处理,是exc_handler,这里的五个参数就是对应的上面表单内容。

cpp 复制代码
/**
 * common exception handler
 */
void exc_handler(unsigned long type, unsigned long esr, unsigned long elr, unsigned long spsr, unsigned long far)
{
    // print out interruption type
    switch(type) {
        case 0: uart_puts("Synchronous"); break;
        case 1: uart_puts("IRQ"); break;
        case 2: uart_puts("FIQ"); break;
        case 3: uart_puts("SError"); break;
    }
    uart_puts(": ");
    // decode exception type (some, not all. See ARM DDI0487B_b chapter D10.2.28)
    switch(esr>>26) {
        case 0b000000: uart_puts("Unknown"); break;
        case 0b000001: uart_puts("Trapped WFI/WFE"); break;
        case 0b001110: uart_puts("Illegal execution"); break;
        case 0b010101: uart_puts("System call"); break;
        case 0b100000: uart_puts("Instruction abort, lower EL"); break;
        case 0b100001: uart_puts("Instruction abort, same EL"); break;
        case 0b100010: uart_puts("Instruction alignment fault"); break;
        case 0b100100: uart_puts("Data abort, lower EL"); break;
        case 0b100101: uart_puts("Data abort, same EL"); break;
        case 0b100110: uart_puts("Stack alignment fault"); break;
        case 0b101100: uart_puts("Floating point"); break;
        default: uart_puts("Unknown"); break;
    }
    // decode data abort cause
    if(esr>>26==0b100100 || esr>>26==0b100101) {
        uart_puts(", ");
        switch((esr>>2)&0x3) {
            case 0: uart_puts("Address size fault"); break;
            case 1: uart_puts("Translation fault"); break;
            case 2: uart_puts("Access flag fault"); break;
            case 3: uart_puts("Permission fault"); break;
        }
        switch(esr&0x3) {
            case 0: uart_puts(" at level 0"); break;
            case 1: uart_puts(" at level 1"); break;
            case 2: uart_puts(" at level 2"); break;
            case 3: uart_puts(" at level 3"); break;
        }
    }
    // dump registers
    uart_puts(":\n  ESR_EL1 ");
    uart_hex(esr>>32);
    uart_hex(esr);
    uart_puts(" ELR_EL1 ");
    uart_hex(elr>>32);
    uart_hex(elr);
    uart_puts("\n SPSR_EL1 ");
    uart_hex(spsr>>32);
    uart_hex(spsr);
    uart_puts(" FAR_EL1 ");
    uart_hex(far>>32);
    uart_hex(far);
    uart_puts("\n");
    // no return from exception for now
    while(1);
}

这里的代码主要是显示异常内容。值得注意的是这里虽然叫做异常(exceptions),但有一些是相当于事件或者资源调度,直接跳转到下级处理的,有一些是系统崩溃了,就给一个提示的。

第一个参数就是说明了当前异常是什么类型:

异常名称 英文全称 触发原因(案发现场) 硬件行为(即时反应) 软件应对(挽救或处理)
同步异常 Synchronous 执行指令时"当场"触发。如: 1. 系统调用(SVC) 2. 缺页/权限错误(MMU) 3. 非法指令/对齐错误 精确(Precise): ELR_ELx 准确指向导致报错的那条指令。处理器必须停下解决。 1. 正常服务: 执行系统调用后返回。 2. 修复重试: 分配内存页后重新执行指令。 3. 终止: 杀死报错进程。
外部中断 IRQ 外设发出的信号。如: 1. 定时器到期 2. 串口(UART)接收数据 3. GPU 渲染完成 异步: CPU 执行完当前指令后才跳入。ELR_ELx 指向下一条待执行指令。 任务切换: 读取外设寄存器,处理数据(如读取传感器),然后返回主程序继续跑。
快速中断 FIQ 高优先级、低延迟中断信号。在 AArch64 中常用于安全世界(EL3)或特定高性能外设。 高优先级: 拥有独立屏蔽位,通常不被 IRQ 打断。进入速度理论上比 IRQ 更快。 实时响应: 处理对延迟极度敏感的硬件任务。在 Linux 中较少直接使用,常驻留于固件中。
系统错误 SError 异步总线故障。如: 1. 访问了不存在的物理地址 2. 内存条/缓存发生不可纠正的错误 3. 外设写回失败 模糊(Imprecise): ELR_ELx 往往不指向出错点,因为错误是总线在很久之后才反馈的。 1. 记录: 打印寄存器快照寻找蛛丝马迹。 2. 重启: 对于内核级 SError,通常只能触发 Watchdog 重启。

后面的几个参数就暂时不多描述了。

3 运行不同的异常

使用上面的代码,验证一下不同的异常。

3.1 非法地址读写Data Abort

原始代码中的异常是

cpp 复制代码
    // generate a Data Abort with a bad address access
    r=*((volatile unsigned int*)0xFFFFFFFFFF000000);
    // make gcc happy about unused variables :-)
    r++;

运行结果:

3.2 非法指令undefined instruction

异常代码:

cpp 复制代码
// 构造一个硬件不认识的指令
asm volatile (".word 0x00000000"); // 0 地址通常是非法指令

运行结果:

qemu-system-aarch64 -M raspi3b -kernel kernel8.img -serial stdio

Synchronous: Unknown:

ESR_EL1 0000000002000000 ELR_EL1 0000000000080CA0

SPSR_EL1 00000000800003C4 FAR_EL1 0000000000000000

解释: 这是非法指令异常。

3.3 非法取指Instruction Abort

代码:

cpp 复制代码
void (*func)(void) = (void*)0xFFFFFFFFFFFF0000;
func();

结果:

hp@DESKTOP-430500P:~/raspi3-tutorial/11_exceptions$ make run

qemu-system-aarch64 -M raspi3b -kernel kernel8.img -serial stdio

Synchronous: Instruction abort, same EL:

ESR_EL1 0000000086000007 ELR_EL1 FFFFFFFFFFFF0000

SPSR_EL1 00000000800003C4 FAR_EL1 FFFFFFFFFFFF0000

3.4 系统调用SVC

异常代码:

cpp 复制代码
asm volatile ("svc #0x123"); // 触发同步异常,立即从 EL0 跳到 EL1

运行结果:

hp@DESKTOP-430500P:~/raspi3-tutorial/11_exceptions$ make run

qemu-system-aarch64 -M raspi3b -kernel kernel8.img -serial stdio

Synchronous: System call:

ESR_EL1 0000000056000123 ELR_EL1 0000000000080CA4

SPSR_EL1 00000000800003C4 FAR_EL1 0000000000000000

3.5 BRK异常

代码

cpp 复制代码
asm volatile ("brk #0"); 

结果:

hp@DESKTOP-430500P:~/raspi3-tutorial/11_exceptions$ make run

qemu-system-aarch64 -M raspi3b -kernel kernel8.img -serial stdio

Synchronous: Unknown:

ESR_EL1 00000000F2000000 ELR_EL1 0000000000080CA0

SPSR_EL1 00000000800003C4 FAR_EL1 0000000000000000

3.6 FPU exception

代码:

cpp 复制代码
asm volatile("fadd d0, d0, d0");

结果:

hp@DESKTOP-430500P:~/raspi3-tutorial/11_exceptions$ make run

qemu-system-aarch64 -M raspi3b -kernel kernel8.img -serial stdio

Synchronous: Unknown:

ESR_EL1 000000001FE00000 ELR_EL1 0000000000080CA0

SPSR_EL1 00000000800003C4 FAR_EL1 0000000000000000

3.7 经典的Segmentation Fault

这个错误基本上是日常开发见的最多的异常。但是,段错误(Segmentation Fault)其实是操作系统的概念,不是CPU的异常类型。

日常中如果访问了非法地址,比如:

cpp 复制代码
*(int*)0xdeadbeef = 1;

这里出来的异常是Data Abort,就是3.1的异常。但是在Linux内核中进行了处理,最后呈现出来的就是Segmentation fault,完整路径如下:

用户程序非法访问

CPU Data Abort

kernel exception handler

do_page_fault()

无法修复

send SIGSEGV

Segmentation fault

3.8 异常小结

实验 代码 结果
illegal instruction .word 0 Unknown
SVC svc #0 SVC
data abort 访问非法地址 Data Abort
instruction abort 跳到非法地址 Instruction Abort
BRK brk #0 debug exception
相关推荐
wearegogog1233 小时前
基于TMS320F28035的太阳能MPPT逆变器程序实现
嵌入式
济6173 小时前
ARM Linux 驱动开发篇--- Linux 按键输入实验--- Ubuntu20.04互斥体实验
linux·嵌入式·嵌入式linux驱动开发
_OP_CHEN3 小时前
【Linux系统编程】(四十六)线程池原理与实现:从固定线程池到线程安全单例模式
linux·单例模式·操作系统·线程池·进程·线程安全·c/c++
你家人养牛11 小时前
numworks移植记录:7.移植LCD驱动——添加到numworks中
嵌入式
你家人养牛11 小时前
numworks移植记录:10.编译问题汇总与解决方案
嵌入式
你家人养牛11 小时前
numworks移植记录:11.编译问题汇总与解决方案(二)
嵌入式
你家人养牛11 小时前
numworks移植记录:5.从 Makefile 到 CMake —— 提取模块依赖并集成到 ESP-IDF
嵌入式
你家人养牛11 小时前
numworks移植记录:4.使用 CLion + ESP-IDF 编译,添加模块并集成编译出 bin 文件
嵌入式
你家人养牛11 小时前
numworks移植记录:8.按键扫描——用74HC595和74HC165扩展GPIO实现矩阵键盘
嵌入式