异常分类
在 ARM64 体系结构中,异常分为同步异常和异步异常。
同步异常是试图执行指令时生成的异常,或是作为指令的执行结果生成的异常。同步异常包括如下。
- 系统调用。异常级别 0 使用 svc指令陷入异常级别 1,异常级别1 使用hv指令陷入异常级别2,异常级别 2 使用 smc指令陷入异常级别 3。
- 数据中止,即访问数据时的页错误异常,虚拟地址没有映射到物理地址,或者没有写权限。
- 指令中止,即取指令时的页错误异常,虚拟地址没有映射到物理地址,或者没有执行权限。
- 栈指针或指令地址没有对齐。
- 没有定义的指令。
- 调试异常。
异步异常不是由正在执行的指令生成的,和正在执行的指令没有关联。异步异常包括以下。
- 中断(normal priority interrupt,IRQ),即普通优先级的中断。
- 快速中断(fast interrupt,FIQ),即高优先级的中断。
- 系统错误(System Error,SError),是由硬件错误触发的异常,例如最常见的是把脏数据从缓存行写回内存时触发异步的数据中止异常。
异常向量表
当异常发生的时候,处理器需要执行异常的处理程序。存储异常处理程序的内存位置称为异常向量,通常把所有异常向量存放在一张表中,称为异常向量表。对于 ARM64 处理器的异常级别 1、2 和 3,每个异常级别都有自己的异常向量表,异常向量表的起始虚拟地址存放在寄存器 VBAR_ELn(向量基准地址寄存器,Vector Based Address Register)中。每个异常向量表有 16 项,分为 4 组,每组 4项,每项的长度是 128 字节(可以存放32 条指令)。linux的异常向量表代码在arm64\kernel\entry.S中:
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
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
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
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 // Error 64-bit EL0
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_compat, 32 // Error 32-bit EL0
SYM_CODE_END(vectors)
我们看到有4组,每一组的4项都是分别表示发生了同步异常,irq,firq和系统错误。这4组的区别就是,第一组表示异常发生在EL0,处理异常的特权等级也是EL0。第二组表示异常发生在ELn(n可以为1,2,3),处理异常的特权等级也是ELn(n可以为1,2,3),但是这里是linux内核,所以我们的特权为EL1,我们可以理解为异常发生在EL1,处理异常的特权等级也是EL1。这两组的共同点是异常的发生和处理在同一个特权级别,不需要进行特区那级别的切换;而后面两组则是异常的发生和处理不在同一个特权级别,需要进行特区那级别的切换。在linux这里就是说,第三组和第四组表示异常发生在EL0,但是异常处理却在EL1,而他们的区别就是,第三组表示异常发生在64位环境下,第四组表示异常发生在32位环境下。
我们现在知道这4组,16项的含义了,我们看fiq都是invalid的,是因为linux不支持fiq,所以没有对fiq异常进行处理。我们也看到第一组都是invalid的,是因为EL0发生了异常,不会在EL0处理,会陷入EL1处理,所以第一组也不需要实现。
我们再看看kernel_ventry这个宏吧:
.macro kernel_ventry, el, label, regsize = 64
.align 7
.Lventry_start\@:
.if \el == 0 //如果在EL0中,不用看肯定不是
/*
* 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 //如果不在EL0中,
mov x30, xzr //给x30清零
.endif
.Lskip_tramp_vectors_cleanup\@:
.endif
sub sp, sp, #S_FRAME_SIZE //把x0-x30寄存器入栈,修改sp值
#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 //前面是保存现场,后面是恢复现场,这里是真正的处理函数了
0:
/*
* Either we've just detected an overflow, or we've taken an exception
* while on the overflow stack. Either way, we won't return to
* userspace, and can clobber EL0 registers to free up GPRs.
*/
/* Stash the original SP (minus S_FRAME_SIZE) in tpidr_el0. */
msr tpidr_el0, x0
/* Recover the original x0 value and stash it in tpidrro_el0 */
sub x0, sp, x0
msr tpidrro_el0, x0
/* Switch to the overflow stack */
adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0
/*
* Check whether we were already on the overflow stack. This may happen
* after panic() re-enables interrupts.
*/
mrs x0, tpidr_el0 // sp of interrupted context
sub x0, sp, x0 // delta with top of overflow stack
tst x0, #~(OVERFLOW_STACK_SIZE - 1) // within range?
b.ne __bad_stack // no? -> bad stack pointer
/* We were already on the overflow stack. Restore sp/x0 and carry on. */
sub sp, sp, x0
mrs x0, tpidrro_el0
#endif
b el\()\el\()_\label
.org .Lventry_start\@ + 128 // Did we overflow the ventry slot?
.endm
.macro tramp_alias, dst, sym, tmp
mov_q \dst, TRAMP_VALIAS
adr_l \tmp, \sym
add \dst, \dst, \tmp
adr_l \tmp, .entry.tramp.text
sub \dst, \dst, \tmp
.endm
也就是说,最主要的就是“b el()\el()_\label”这一句代码了,前面是保存现场,后面是恢复现场,这句是真正的处理函数。第二组第一项kernel_ventry 1, sync ,在这里就是b el1_sync ,也就是跳转到el1_sync函数了。到这里,我们把每一项支持的异常向量跳转函数写成表格就是:
异常向量 | 跳转的函数 |
---|---|
kernel_ventry 1, sync | el1_sync |
kernel_ventry 1, irq | el1_irq |
kernel_ventry 1, error | el1_error |
kernel_ventry 0, sync | el0_sync |
kernel_ventry 0, irq | el0_irq |
kernel_ventry 0, error | el0_error |
kernel_ventry 0, sync_compat, 32 | el0_sync_compat |
kernel_ventry 0, irq_compat, 32 | el0_irq_compat |
kernel_ventry 0, error_compat, 32 | el0_error_compat |