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_ELx 和 SPSR_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 |