一、异常向量表
从ARM64的u-boot启动文件start.S中,我们可以知道u-boot启动早期会设置异常向量表:
/*
* Could be EL3/EL2/EL1, Initial State:
* Little Endian, MMU Disabled, i/dCache Disabled
*/
adr x0, vectors /* 把当前代码段中名为vectors的符号的地址(PC相对算出来的地址)放入x0 */
/* 读取CurrentEL(当前异常级别)并根据结果跳到对应的标签(3f表示EL3分支,2f表示EL2,1f表示EL1) */
switch_el x1, 3f, 2f, 1f
3: msr vbar_el3, x0
mrs x0, scr_el3
orr x0, x0, #0xf /* SCR_EL3.NS|IRQ|FIQ|EA */
msr scr_el3, x0
msr cptr_el3, xzr /* Enable FP/SIMD */
#ifdef COUNTER_FREQUENCY
ldr x0, =COUNTER_FREQUENCY
msr cntfrq_el0, x0 /* Initialize CNTFRQ */
#endif
b 0f
2: msr vbar_el2, x0
mov x0, #0x33ff
msr cptr_el2, x0 /* Enable FP/SIMD */
b 0f
1: msr vbar_el1, x0
mov x0, #3 << 20
msr cpacr_el1, x0 /* Enable FP/SIMD */
0:
ARM64有专用的寄存器用于存储异常向量表基地址:(Vector Base Address Register for Exception Level x**),**用于存放异常向量表的基地址。AArch64定义了四个(实际上常用的是EL1、EL2、EL3):
**VBAR_ELx**
:用于 EL1(内核/操作系统)
VBAR_EL1 :用于 EL2(Hypervisor/虚拟化层)
VBAR_EL2:用于 EL3(Secure Monitor/TrustZone Monitor)
VBAR_EL3不存在(因为 EL0 = 用户态,不能直接接收异常向量)
VBAR_EL0
从上述代码可知,u-boot根据不同运行级别,会将异常向量表设置到对应运行级别的
vectors。
VBAR_ELx实现在arch/arm/cpu/armv8/exceptions.S中:
vectors
.align 11
.globl vectors
vectors:
.align 7 /* Current EL Synchronous Thread */
stp x29, x30, [sp, #-16]!
bl _exception_entry
bl do_bad_sync
b exception_exit
.align 7 /* Current EL IRQ Thread */
stp x29, x30, [sp, #-16]!
bl _exception_entry
bl do_bad_irq
b exception_exit
.align 7 /* Current EL FIQ Thread */
stp x29, x30, [sp, #-16]!
bl _exception_entry
bl do_bad_fiq
b exception_exit
.align 7 /* Current EL Error Thread */
stp x29, x30, [sp, #-16]!
bl _exception_entry
bl do_bad_error
b exception_exit
.align 7 /* Current EL Synchronous Handler */
stp x29, x30, [sp, #-16]!
bl _exception_entry
bl do_sync
b exception_exit
.align 7 /* Current EL IRQ Handler */
stp x29, x30, [sp, #-16]!
bl _exception_entry
bl do_irq
b exception_exit
.align 7 /* Current EL FIQ Handler */
stp x29, x30, [sp, #-16]!
bl _exception_entry
bl do_fiq
b exception_exit
.align 7 /* Current EL Error Handler */
stp x29, x30, [sp, #-16]!
bl _exception_entry
bl do_error
b exception_exit
上述汇编代码则是u-boot的异常向量表实现,其中每个条目汇编指令几乎是一样的:
:对齐到128字节 (2^7)。,ARM64 的异常向量表要求每个入口(同步/IRQ/FIQ/SError)占用128字节,不能重叠。所以这里每个handler前都
align 7,保证落在正确位置。从u-boot的汇编代码可以清除看见每一个异常条目的位置:
.align 7

:保存当前帧指针 (x29) 和返回地址 (x30/LR) 到栈上。
stp x29, x30, [sp, #-16]!
,然后把这两个寄存器压栈。
sp = sp - 16
:跳到通用的异常入口函数,通常做保存寄存器上下文的工作。
bl _exception_entry:调用具体的异常处理函数,比如这里是 “bad sync”(未定义或错误的同步异常)。
bl do_bad_sync:最终跳到统一的
b exception_exit,在那儿会恢复寄存器、执行
exception_exit返回。
eret
二、异常进入处理
异常进入处理由实现, 异常保存现场的入口函数,用于保存寄存器状态,保证进入异常处理函数时用户寄存器和关键信息不会丢失 ,
_exception_entry实现(arch/arm/cpu/armv8/exceptions.S)如下 :
_exception_entry
/*
* Enter Exception.
* This will save the processor state that is ELR/X0~X30
* to the stack frame.
*/
_exception_entry:
stp x27, x28, [sp, #-16]!
stp x25, x26, [sp, #-16]!
stp x23, x24, [sp, #-16]!
stp x21, x22, [sp, #-16]!
stp x19, x20, [sp, #-16]!
stp x17, x18, [sp, #-16]!
stp x15, x16, [sp, #-16]!
stp x13, x14, [sp, #-16]!
stp x11, x12, [sp, #-16]!
stp x9, x10, [sp, #-16]!
stp x7, x8, [sp, #-16]!
stp x5, x6, [sp, #-16]!
stp x3, x4, [sp, #-16]!
stp x1, x2, [sp, #-16]!
/* Could be running at EL3/EL2/EL1 */
switch_el x11, 3f, 2f, 1f
3: mrs x1, esr_el3
mrs x2, elr_el3
mrs x3, daif
mrs x4, vbar_el3
mrs x5, spsr_el3
sub x6, sp, #(8*30)
mrs x7, sctlr_el3
mrs x8, scr_el3
mrs x9, ttbr0_el3
b 0f
2: mrs x1, esr_el2
mrs x2, elr_el2
mrs x3, daif
mrs x4, vbar_el2
mrs x5, spsr_el2
sub x6, sp, #(8*30)
mrs x7, sctlr_el2
mrs x8, hcr_el2
mrs x9, ttbr0_el2
b 0f
1: mrs x1, esr_el1
mrs x2, elr_el1
mrs x3, daif
mrs x4, vbar_el1
mrs x5, spsr_el1
sub x6, sp, #(8*30)
mrs x7, sctlr_el1
mov x8, #0 /* Not used, EL1 don't have register, like 'scr_el1' */
mrs x9, ttbr0_el1
0:
stp x2, x0, [sp, #-16]!
stp x3, x1, [sp, #-16]!
stp x5, x4, [sp, #-16]!
stp x7, x6, [sp, #-16]!
stp x9, x8, [sp, #-16]!
mov x0, sp
ret
上述汇编代码具体实现步骤如下:
(1)保存通用寄存器
stp x27, x28, [sp, #-16]!
...
stp x1, x2, [sp, #-16]!
把 x1–x30(除 x0 留到后面特殊处理)全部压栈。是 Store Pair,把两个寄存器一次存到内存,并更新栈指针
stp。
sp 表示:栈指针先减去16(预留空间),再把两个寄存器存进去。这样做相当于把30个通用寄存器的值保存到栈中。
[sp, #-16]!
(2)判断当前异常是在哪个EL发生
switch_el x11, 3f, 2f, 1f
是一个宏,根据当前运行级别(EL3 / EL2 / EL1)跳转到不同的标签。因为异常可能发生在EL1内核、EL2 hypervisor或EL3 secure monitor级别。
switch_el
(3)保存异常相关的系统寄存器
每个EL对应分支,读取一批寄存器,汇编指令如下:
mrs x1, esr_el1 // 异常综合状态寄存器
mrs x2, elr_el1 // 异常返回地址
mrs x3, daif // 中断屏蔽位
mrs x4, vbar_el1 // 向量基地址
mrs x5, spsr_el1 // 保存程序状态
sub x6, sp, #(8*30) // 计算通用寄存器保存区基址
mrs x7, sctlr_el1 // 系统控制寄存器
mov x8, #0 // EL1没有scr_el1,所以写0占位
mrs x9, ttbr0_el1 // 页表基地址
对EL2/EL3的情况,逻辑一样,只是寄存器不同。此处不再展开描述。
(4)把这些系统寄存器继续压栈
stp x2, x0, [sp, #-16]!
stp x3, x1, [sp, #-16]!
stp x5, x4, [sp, #-16]!
stp x7, x6, [sp, #-16]!
stp x9, x8, [sp, #-16]!
这里保存ELR、ESR、DAIF、VBAR、SPSR、SCTLR、SCR/HCR(或占位)、TTBR0等关键信息。
(5)准备异常栈帧
mov x0, sp
ret
最后把作为参数放到
sp里返回。也就是说,这样后续的
x0、
do_sync等异常处理函数拿到的是一个指针,指向完整的异常上下文(栈帧)。
do_irq
三、异常处理
在u-boot中,多种异常类型都具有一一对应的异常处理函数,如下表所示:
| 异常来源 (Current EL) | Handler函数 | 说明 |
|---|---|---|
| Synchronous (Thread) | |
当前EL线程模式下发生的同步异常(未定义指令、非法访存等) |
| IRQ (Thread) | |
当前EL线程模式下发生的普通中断 |
| FIQ (Thread) | |
当前EL线程模式下发生的快速中断 |
| Error (Thread) | |
当前EL线程模式下的SError |
| Synchronous (Handler) | |
当前EL处理器模式下的同步异常 |
| IRQ (Handler) | |
当前EL处理器模式下的普通中断 |
| FIQ (Handler) | |
当前EL处理器模式下的快速中断 |
| Error (Handler) | |
当前EL处理器模式下的SError |
四、异常退出处理
异常退出处理由实现(arch/arm/cpu/armv8/exceptions.S):
exception_exit
exception_exit:
add sp, sp, #(8*8)/* see: sys registers size of struct pt_regs */
ldp x2, x0, [sp],#16
switch_el x11, 3f, 2f, 1f
3: msr elr_el3, x2
b 0f
2: msr elr_el2, x2
b 0f
1: msr elr_el1, x2
0:
ldp x1, x2, [sp],#16
ldp x3, x4, [sp],#16
ldp x5, x6, [sp],#16
ldp x7, x8, [sp],#16
ldp x9, x10, [sp],#16
ldp x11, x12, [sp],#16
ldp x13, x14, [sp],#16
ldp x15, x16, [sp],#16
ldp x17, x18, [sp],#16
ldp x19, x20, [sp],#16
ldp x21, x22, [sp],#16
ldp x23, x24, [sp],#16
ldp x25, x26, [sp],#16
ldp x27, x28, [sp],#16
ldp x29, x30, [sp],#16
eret
的作用是从异常栈帧恢复CPU寄存器状态,把在
exception_exit保存的通用寄存器(x1 … x30)、以及在进入时读取并压栈的系统寄存器(
_exception_entry等)依次恢复到对应寄存器里,最后执行
ELR_x, ESR_x, DAIF, VBAR, SPSR, SCTLR, SCR/HCR, TTBR0跳回异常发生前的上下文。
eret
(1)调整堆栈指针位置
:把
add sp, sp, #(8*8)增加
sp字节。这64字节是用来跳过之前为系统寄存器留出的空间或占位。 在异常返回前,把栈顶调整回来,跳过系统寄存器保存的那一块空间 。
8*8 = 64
先移动跳过某个固定大小的区域,以便接下来的
sp恰好从正确的位置弹出我们需要的成对保存的寄存器值。
ldp
_exception_entry (保存) exception_exit (恢复)
高地址 ─────────────────────────────────────────────────────────────────────────
stp x27, x28, [sp, #-16]! ───────► ldp x27, x28, [sp], #16
stp x25, x26, [sp, #-16]! ───────► ldp x25, x26, [sp], #16
stp x23, x24, [sp, #-16]! ───────► ldp x23, x24, [sp], #16
stp x21, x22, [sp, #-16]! ───────► ldp x21, x22, [sp], #16
stp x19, x20, [sp, #-16]! ───────► ldp x19, x20, [sp], #16
stp x17, x18, [sp, #-16]! ───────► ldp x17, x18, [sp], #16
stp x15, x16, [sp, #-16]! ───────► ldp x15, x16, [sp], #16
stp x13, x14, [sp, #-16]! ───────► ldp x13, x14, [sp], #16
stp x11, x12, [sp, #-16]! ───────► ldp x11, x12, [sp], #16
stp x9, x10, [sp, #-16]! ───────► ldp x9, x10, [sp], #16
stp x7, x8, [sp, #-16]! ───────► ldp x7, x8, [sp], #16
stp x5, x6, [sp, #-16]! ───────► ldp x5, x6, [sp], #16
stp x3, x4, [sp, #-16]! ───────► ldp x3, x4, [sp], #16
stp x1, x2, [sp, #-16]! ───────► ldp x1, x2, [sp], #16
stp ELR, x0, [sp, #-16]! ───────► ldp x2(ELR), x0, [sp], #16
stp DAIF, ESR, [sp, #-16]! ───────► (跳过,add sp, sp, #64)
stp SPSR, VBAR, [sp, #-16]! ───────► (跳过)
stp SCTLR, SPbase, [sp, #-16]! ───────► (跳过)
stp TTBR0, SCR/HCR, [sp, #-16]! ───────► (跳过)
低地址 ─────────────────────────────────────────────────────────────────────────
(2)设置异常返回地址
:根据当前CPU的Exception Level(可能存放在
switch_el x11, 3f, 2f, 1f中或该宏通过系统寄存器检测EL)跳转到相应标签(EL3->3f, EL2->2f, EL1->1f)。目的是把
x11写回到正确等级的
x2(EL1/EL2/EL3有各自的ELR寄存器)。
ELR_ELx
随后三段:
ldp x2, x0, [sp],#16
switch_el x11, 3f, 2f, 1f
3: msr elr_el3, x2
b 0f
2: msr elr_el2, x2
b 0f
1: msr elr_el1, x2
把写入对应EL的
x2,也就是设置好返回地址(异常返回时
ELR_ELx会跳回
eret指定的地址)。
ELR_ELx
(3)恢复通用寄存器
ldp x1, x2, [sp],#16
ldp x3, x4, [sp],#16
...
ldp x29, x30, [sp],#16
这些是按对恢复通用寄存器(x1…x30),每条把两个寄存器从栈上取出并把
ldp 加16。
sp
恢复顺序是入栈时的逆序(LIFO),这样能把每个寄存器恢复到它进入异常前的值。注意 之前已经在
x0中恢复。
ldp x2,x0, [sp],#16
(4)eret异常返回
(Exception Return)会做两件关键事:
eret
1、从当前中恢复程序状态(PSTATE: condition flags, interrupt masks 等)到
SPSR_ELx。通常
PSTATE曾读取并压栈
_exception_entry,但实际
spsr_elx的恢复路径可以是通过在返回前把
SPSR写回
spsr_elx寄存器(若需要),或由硬件在
spsr时依据
eret恢复。
SPSR_ELx
2、跳转到指定的地址(之前我们用
ELR_ELx恢复过)。
msr elr_elX, x2
运行结果就是把CPU恢复到异常发生前的执行现场并继续执行。
eret


