一、异常等级
ARM 的异常等级分为四个等级:
- EL0:非特权模式,用户态,运行用户应用程序
- EL1:特权模式,内核态,运行操作系统内核(VHE 模式下操作系统内核运行在 EL2)
- EL2:虚拟化模式,运行虚拟机监控器 Hypervisor
- EL3:安全模式,运行可信执行环境(TEE)

二、异常类型
当异常发生时,通常会 trap 到更高的 EL
级别进行处理。然后处理完通过eret指令返回到之前的 EL
级别继续执行。

返回到原先的 Level 后,我们应该执行原先发生异常的那条指令呢?还是跳过它继续执行下一条指令呢?这取决于异常的类型。
ARM64 异常类型分为同步异常和异步异常两种
- 同步异常一般指的是异常是由当前指令引起的,例如除零、非法指令等,因此这种异常需要由高级别的决定怎么处理,能否修复。除系统调用外,同步异常
eret返回后重新执行发生异常的那条指令。- 系统调用异常(SVC、HVC、SMC)
- MMU 引发的异常(缺页、访问没有权限的页面)
- 对齐检查异常(SP 和 PC 对齐检查)
- 未分配指令异常(非法指令、没有被定义的指令、在不恰当的等级访问没有权限的寄存器)
- 异步异常一般指的是异常是由外部事件引起的,例如中断等,当前正在执行的指令跟出现的异常是没有依赖关系的,因此这种异常处理完后直接继续执行下一条指令。
- IRQ 中断
- FIQ 中断
- SERROR 中断,没法被修复的错误
三、异常入口
异常入口是要清楚发生异常后,CPU 要做什么。 当异常发生时,CPU 会自动执行以下操作:
- PSTATE 保存到
SPSR_ELx中- PSTATE 包含当前的处理器状态信息,包括条件标志、中断使能位等
- 返回地址 PC 保存到
ELR_ELx中 - PSTATE 寄存器里的 DAIF 标志位会被设置为 1,相当于把调试异常、SError 异常、IRQ 异常和 FIQ 异常全部关闭,防止在异常处理过程中被打断。
- 更新
ESR_ELx寄存器,记录异常的类型和原因 - SP 执行
SP_ELx寄存器,切换到对应 EL 级别的栈 - 切换到对应的 EL 级别,跳转到异常向量表中对应的异常处理程序地址
四、异常返回

异常返回时,操作系统会执行 eret 指令,CPU
会自动执行以下操作:
- 从
ELR_ELx寄存器中恢复 PC 指针- 同步异常(非系统调用的同步异常):PC 执行异常现场的当条指令
- 异步异常:PC 执行异常现场的下一条指令
- 从
SPSR_ELx寄存器中恢复 PSTATE 状态SPSR.M[3:0]字段记录了要返回恢复到哪个 EL 级别SPSR.M[4]记录了异常现场的模式:- 0:AArch64 模式
- 1:AArch32 模式

五、异常处理的路由
异常处理的路由指的是发生异常后,要跳转到哪个 EL 级别进行处理。
当出现异常时,CPU 会根据当前的 EL 级别和异常类型,切换到相应的 EL 级别进行处理。通常情况下,异常会从较低的 EL 级别切换到较高的 EL 级别进行处理。例如,当在 EL0 运行的用户应用程序发生异常时,CPU 会切换到 EL1 进行处理。
异常也可以同级别处理,例如在 EL1 运行的内核代码发生异常时,CPU 仍然在 EL1 进行处理。但是 EL0 的异常不能在 EL0 进行处理,必须切换到高级别中。
为了实现跳转,每个 EL
等级需要在系统启动时分配一个对应的栈空间,一般 4KB
大小即可。当要做异常跳转的时候,让 SP_ELx 指向对应 EL
级别的栈地址。
六、异常向量表
异常向量表需要软件配置,且有固定的格式,每个EL都有自己的异常向量表(除了EL0,EL0不处理异常)。
EL1的异常向量表的基地址可以通过寄存器VBAR_EL1来配置,所有要切到
EL1 处理的异常,都会跳转到这个地址开始执行。

由于 64 位的地址中,前面bit[10:0]是保留地址,不使用的,因此异常向量表的地址需要2KB对齐(2^11 = 2048)。所以一般在配置异常向量表基地址的时候都会用align 11来对齐。
异常向量表的设计理念是必须放在一个连续的块中,这个块的大小是 2KB(2048 字节),因为异常向量表中有 16 个入口,每个入口占用 128 字节(16 x 128 = 2048 字节)。
先看一个简单的地址空间示意(只画一小段,VBAR_EL1 对齐示意图(2KB 对齐直观图)):
1 | 地址(假设高位都是 0xFFFF0000_0000_0000 之类,这里只关心低 16bit) |
VBAR_EL1 的低 11 位是 RES0,也就是:
1 | VBAR_EL1[10:0] = 0 |
也就是只能指向每个 2KB 块的起始地址。
如果你写入一个没对齐的地址,比如:
1 | 你写: VBAR_EL1 = 0x0000_0000_0001_2345 |
所以向量表必须整体放在 [0x0000_0000_0001_2000, 0x0000_0000_0001_27FF] 这 2KB 里。
在 spec 里,定义了四种异常类型的入口顺序:
- 同步异常
- IRQ 异常
- FIQ 异常
- SError 异常
每个 EL 级别都有这四种异常类型的入口,因此 lnux 内核里保证 VBAR_EL1 的异常向量表和对齐方式以为下:

其中,kernel_ventry是宏定义,定义在arch/arm64/kernel/vectors.S文件中:
1 | .macro kernel_ventry type, el, label, regsize = 64 |

其中,kernel_entry宏是用来保存异常现场的(上下文),保存CPU中的重要信息。


四种异常类型的入口地址偏移分别是:

实验一:跳转到 EL1
QEMU 虚机跳转到 Benos 代码时,是出于 EL2 级别的,因此我们需要在 Benos 内核初始化代码中,切换到 EL1 级别。
因此,我们需要在 EL2 中触发一次异常,并在同级别 EL2 中处理这个异常,从而实现跳转到 EL1。
跳转到 EL1 需要配置 EL1 的一些相关寄存器做以下几步:
- 设置
HCR_EL2寄存器,这个寄存器是配置 hypervisor 的一些配置项,这里的RW,bit[31]域表示了 EL1 运行在 AArch64 模式还是 AArch32 模式- 0:AArch32 模式 (默认)
- 1:AArch64 模式
- 设置
SCTRL_EL1寄存器,这个寄存器是配置一些如 MMU、cache、大小端的配置项M,bit[0]域表示是否开启 MMU- 0:关闭 MMU(目前系统内核还没写内存翻译的代码,因此先关闭)
- 1:开启 MMU
EE,bit[25]域表示 EL1 的大小端模式- 0:小端模式(这里也设置成小端)
- 1:大端模式
- 设置
SPSR_EL2寄存器,这个寄存器是保存异常现场的 PSTATE 状态,需要关闭中断并设置返回的 EL 级别M,bit[3:0]域表示要切到哪个 EL 级别(我们这里是要切到 EL1)0b0101:EL1h 模式,使用 SP_EL1 寄存器作为栈指针
- 设置异常返回寄存器
ELR_EL2,让他返回到 EL1_entry 入口汇编函数里 - 执行
eret指令,跳转到 EL1 级别
1 | // sysregs.h |
1 | // boot.s |
实验二:实现同步异常处理
本实验的异常暂时不做保存异常上下文的操作,只是简单地将异常全部触发 panic。
ARM v8 中指令通常是 4 字节对齐的,因此我们可以通过故意执行一个未对齐的内存访问指令来触发同步异常。
首先创建一个 EL1 的异常向量表vectors:
1 | // vectors.S |
1 | // kernel.c |
编写完 EL1 的异常向量表后,需要在进入 EL1
的el1_entry函数中,配置VBAR_EL1寄存器,让 CPU
知道 EL1 的异常向量表地址:
1 | // boot.s |
完成后,就可以正常编译运行了。但是此时我们还没有触发异常,因此我们需要在kernel_main函数中,故意通过指令未对齐来触发Instruction
Alignment Fault(指令对齐故障):
1 | // kernel.c |
如果我们只是通过下面的方式来访问内存,是不触发异常的:
1 | // entry.S |

但是如果我们在trigger_alignment前面加上一个字节的偏移,就会触发异常:
1 | // entry.S |


进一步通过单步调试来查看单步调试到trigger_alignment的时候发生的报错:

从上面x/4i 0x83004的反汇编结果可以看到,CPU
在执行到ldr x1, [x0]这条指令时,触发了对齐异常
按照指令规则,当前应该会执行0x83004这条指令,CPU会尝试去执行这条指令,但是由于我们塞了一个.string "t",导致此时
CPU
尝试从string test处执行,也就是说,错误地把它当成一条指令来执行了,结果自然就触发了对齐异常。也就是说,内存跑飞了。
进一步地,如果我们想执行trigger_alignment函数后不触发异常,可以将string_test进行对齐:
1 | // entry.S |
| 情况 | 内存布局 | 执行结果 |
|---|---|---|
| 不加字符串 | trigger_alignment 位于
0x83004 (对齐) |
CPU 正常读取 ldr
指令,执行成功。 |
| 加了字符串 | 0x83004 变成了数据
't' |
CPU 把数据当指令读,读到非法编码,崩了。 |
| 加了字符串且没对齐 | trigger_alignment 位于
0x83006 |
CPU 根本无法从该地址提取指令,直接触发对齐故障。 |