请阅读【ARMv8/v9 ARM64 System Exception】
文章目录
上篇文章:ARMv8/v9 异常模型系列 3 - Process State介绍
下篇文章:ARMv8/v9 异常模型系列 5 - IRQ 异常处理流程
1. ARMv8 异常向量表
ARM芯片手册通常使用 ELn 来表示第 n个异常等级。
在 ARMv8 架构的AArch64时,每种 EL 都具有独立的16个Entires, 这16
个 Entires分为四类异常:
IRQ
FIQ
SError
Synchronous
根据触发一种异常时是否会产生EL
级别的迁移和使用产生迁移时使用的是AArch64还是AArch32的指令集又具有四种不同的区域。
- EL0 是用户态。
- EL1 是内核态也就是特权模式。
- EL2 是虚拟化的hypevisor模式。
- EL3 是安全 monitor。
当在EL0
产生异常而未产生EL
的迁移时,使用的是SP0
保存的栈空间地址。
而在除EL0
之外的ELn
产生异常而未导致EL
的迁移时使用的是对应的SPn
保存的栈空间地址。
在AArch64中,EL
的迁移只能是从低EL向高EL
进行迁移,如果使用AArch64
指令集和AArch32
指令集时产生了异常并且也发生了EL的迁移时,则使用的是将迁移到的EL对应的SPn
。
1.1 ARMv8-A Exception Handling
异常主要是分成2大类,一类是同步,另外一类是异步。
- 同步异常:
导致异常发生的原因是执行了某个指令,异常返回地址就指明了是因为执行了那个“圈套”指令导致的,这个异常发生是精确的。- 同步异常的产生是和cpu core执行的指令异常或试图改变执行权限引起的异常
- 返回地址是硬件保存下来并提供给handler,以便进行异常返回现场的处理。
- 同步异常分两种:
- 一是abort类,例如未定义的指令、
data abort
、prefetch instruction abort
、SP未对齐异常,debug exception
等等。 - 另一种是正常指令执行造成的,包括
SVC/HVC/SMC
指令,这些指令的使命就是产生异常,改变执行权限。
- 一是abort类,例如未定义的指令、
- 异步异常:
这个异常的发生不是因为执行了某个指令而中的圈套,异常返回地址没法指明是中了哪条指令的圈套,这个异常发生是不精确的。- 异步异常可以理解为中断,CPU是不可预知的。
- 异常和 CPU 执行的指令无关。
- 返回地址是硬件保存下来并提供给 handler,以便进行异常返回现场的处理。
- 根据这个定义 IRQ、FIQ 和 SError interrupt 属于 asynchronous exception。
同步异常就是说中了指令的圈套,这样的,这些异常的发生是比较精准的。
而异步异常是和运行的指令没有关系,因为它是被中圈套。在ARMv7的那些异常,比如 data abort等,就符合这里的同步异常,而IRQ 和 FIQ 中断就符合异步异常。
1.1.1 什么是精确异常
先来说说什么是非精确异常?:在多发射乱序执行的流水线 CPU 上,从指令进入流水线到异常事件的发生,期间要经过若干流水级,此时 PC 的值已指向其后的某条指令,在实现非精确异常的 CPU 上就把此时的 PC 值作为引起异常指令的所在,也就是记录异常指令的PC并非真正的引起异常的指令所在,而是其后面的某条指令所在。
精确异常(precise exception),也就是记录异常指令的PC并非真正的引起异常的指令所在,而是其后面的某条指令所在。实现精确异常的 CPU,在最后指令提交时 (commit) 按指令流的顺序提交,异常的抛出也在该指令提交时,这样就能精确计算出引起异常的指令相对于当前 PC 的偏移,从而保证精确异常。不管是何类异常,记录异常指令的PC之前的所有指令都会被执行完成 (commit),之后的指令不会被执行。
在 AArch64中,除了SError interrupt 这种 exception,其他的 exception 都是 precise exception。
1.1.2 SPSR_ELn Register
进入异常时会将 PSTATE
,即PE状态放在异常等级对应 的 SPSR_ELn
,
SP位: 栈指针寄存器选择位,对于Aarch64,栈指针寄存器有四个,EL0/LE1/EL2/EL3,但是SP位只有1bit
, 只能指示使用SP_EL0
还是SP_ELx
,所以就需求配合其他域来使用,这时就出现了SP_ELx
域。
对于EL1/EL2/EL3三个栈指针是怎么选择的?当异常发生时,现根据PSTATE
中的SP
位选决定使用是否使用SP_EL0
,然后再根据PSTATE
中的 EL 2 bit
决定到对应的异常等级的栈指针SP_ELx
(x>0)。
一般异常等级里执行程序时,都会再将SP位清零,也就是继续用SP_EL0
为什么不直接用SP_ELx
呢?
如果当ELx的异常发生了,然后用上了栈指针寄存器SP_ELx
,然后就开始执行处理的指令流,然后正在处理过程中,又发生了一个同级的异常。栈指针又立马选到了对应的异常等级的栈指针,这就是为何EL0的SP栈指针寄存器单独用了一个标志位来表示。
AIF 位: 分别为 SError
、IRQ
、FIQ
屏蔽位,置位后对应中断不能发生。
在AArch64中,调用ERET从一个异常返回时,会将SPSR_ELn
恢复到 PSTATE
中。
1.1.3 Exception Handling Flow
- 处理器的状态保存到对应(target)的异常等级的
SPSR_ELn
寄存器里, 如果当前的异常发生在EL1
,则将PSTATE
的状态保存到SPSR_EL1
中; - 返回地址(PC)保存到对应的异常等级的
ELR_ELn
寄存器里,n 对应异常等级; PSTATE
寄存器里的 D,A,I,F 域都设置为1,关掉其他异常,以防止处理异常时被打断了;- 如果是同步异常,根据
ESR_ELn
寄存器的值找到对应的原因; - 设置一下栈指针,指向对应异常等级里的栈;
- 迁移到对应的异常等级,然后跑到异常向量表里;
- 从
ELR_ELn
从取出地址放到 PC、从SPSR_ELn
取出状态放到PSTATE
。
AArch64 state异常处理流程 | 说明 |
---|---|
1、保存 PSTATE 数据到SPSR_ELx ,(x = 1,2,3) | 异常返回时需要从SPSR_ELx 中恢复 PSTATE |
2、保存异常进入地址到 ELR_ELx ,同步异常(und/abt等)是当前地址,而异步异常(irq/fiq等)是下一条指令地址 | 64位架构 LR 和 ELR 是独立分开的,这点和 32 位架构有所差别 |
3、保存异常原因信息到ESR_ELx | ESR_ELx.EC 代表 Exception Class,关注这个bit |
4、PE 根据目标 EL 的异常向量表中定义的异常地址强制跳转到异常处理程序 | 跳转到哪个 EL 使用哪个向量偏移地址又路由关系决定 |
5、堆栈指针 SP 的使用由目标 EL 决定 | (SPSR_ELx.M[0] == 1) ? h(ELx): t(EL0) |
1.2 Excepiton Vectors
在 ARMv7 的异常向量表,每个表项只有 4 个字节,只能存放一条跳转指令,但 ARMv8 里升级了,每个表项需要 128个字节,这样可以存放 32 条指令。ARMv8 指令集里一条指令的位宽是 32bit 的,而不是64bit。
Q1: 异常向量表里要包含 4 组,每一组包含4个表项。这怎么理解?
A:ARMv8 有 4 个异常级别,每一个异常级别对应一个 VBAR(Vector Base Address Register) 寄存器(VABR_EL1
, VABR_EL2
,VABR_EL3
),用来指向异常向量表的基地址,每一个异常向量表的大小为128个字节,也即可以存放32条指令;
同时每一个异常向量表会分为4组,每一组包含4 种异常,如下表 1-1所示。
Q2:current EL with SP_EL0是啥意思呢?
A:当系统运行在EL1的时候,然后使用EL0
的SP
,貌似还没有这种场景。
current EL with SP_EL1,这是说系统运行在EL1里,SP
也是使用EL1,那就是在内核态里发生了异常。
Q3:Current Exeception level 是什么意思?
A:比如当前系统只运行Linux内核不包括虚拟化或者安全特性,那最高的EL等级是EL1
,那么它就一定有EL0
。所以上面说的Current EL
就是说的当前系统最高的EL
等级。
如果没有使用 secure monitor 状态和 hvc 状态时,OS 运行在内核态时(EL1
)发生了 IRQ 中断,这时候应该跳转到异常表里的0x280处。
Q4:如果 OS 运行在用户态并且正在执行32bit的app,发生了 IRQ 中断,它应该跳转到哪里呢?
A:应该这时应该跳转到0x0x680
处。
ARMv8 有4张向量表,每张向量表有4中异常:同步异常、irq异常、fiq异常、系统错误异常,而4张表分别对应:
- 发生中断时,异常等级不发生变化,并且不管怎么异常模式,sp只用
SP_EL0
; - 发生中断时,异常等级不发生变化,并且sp用对应异常私有的
SP_ELn
; - 发生中断时,异常等级发生变化,这种情况一般是用户态向内核态发生迁移,当前表示64位用户态向64位内核态发生迁移;
- 发生中断时,异常等级发生变化,这种情况一般是用户态向内核态发生迁移,当前表示32位用户态向64位/32位内核发生迁移。
1.2.1 向量表的配置
Q:关于异常向量表,还有一个问题需要考虑,那就是异常向量表放在哪里?
A:ARM v8里提供了VBAR
(Vector Base address register), ARMv8
中,EL1,EL2,EL3都具有独立的 VBAR
寄存器,该寄存器就是用于存放各VBAR_ELn
的异常向量表的基地址。当发生异常时,如果该异常产生了EL
的迁移,那么完成迁移操作之后会到迁移到的 EL
中的VBAR
寄存器中找到向量表的基地址,然后命中对应的 handler.
Linux内核代码(以runninglinux_4.0
为例)。代码路径是在arch/arm64/kernel/entry.S
文件里。
.align 11
ENTRY(vectors)
/* 第1处 */
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
/* 第 2 处 */
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error_invalid // Error EL1h
/* 第 3 处 */
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error_invalid // Error 64-bit EL0
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid_compat, 32 // Error 32-bit EL0
#else
/*第 4 处*/
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endif
END(vectors)
kernel_ventry
是宏,翻译后的函数名是(以第2处为例):
/*
* Bad Abort numbers
*-----------------
*/
#define BAD_SYNC 0
#define BAD_IRQ 1
#define BAD_FIQ 2
#define BAD_ERROR 3
.macro kernel_ventry, el, label, regsize = 64
.align 7
.Lventry_start\@:
.if \el == 0
/*
* This must be the first instruction of the EL0 vector entries. It is
* skipped by the trampoline vectors, to trigger the cleanup.
*/
b .Lskip_tramp_vectors_cleanup\@
.if \regsize == 64
mrs x30, tpidrro_el0
msr tpidrro_el0, xzr
.else
mov x30, xzr
.endif
.Lskip_tramp_vectors_cleanup\@:
.endif
sub sp, sp, #S_FRAME_SIZE
#ifdef CONFIG_VMAP_STACK
/*
* Test whether the SP has overflowed, without corrupting a GPR.
* Task and IRQ stacks are aligned so that SP & (1 << THREAD_SHIFT)
* should always be zero.
*/
add sp, sp, x0 // sp' = sp + x0
sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp
tbnz x0, #THREAD_SHIFT, 0f
sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0
sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp
b el\()\el\()_\label
上面汇编代码 b el\()\el\()_\label
会转化成下面内容:
el1_sync
el1_irq
el1_fiq_invalid
el1_error_invalid
上图中 4 处对应EL1表项的 4 组。上文中说过每个表项是128个字节吗,但是从上图中并卡不出来,注意上面“ENTRY(vectors)
”, 这里使用的是align伪指令,它使用align 7
,表示按照2的7次方来对齐,2的7次方是128。关于ARM伪指令的详细介绍见 【ARM64 常见汇编指令学习系列文章】
\* Vectory entry *\
.macro ventry label
.align 7
b \label
.endm
上篇文章:ARMv8/v9 异常模型系列 3 - Process State介绍
下篇文章:ARMv8/v9 异常模型系列 5 - IRQ 异常处理流程
推荐阅读:
http://blog.chinaunix.net/uid-69947851-id-5830546.html