在ARMv8中,异常包括中断、中止、复位、系统调用四种类型,异常本身又会根据其异常等级、同步或异步异常的属性,被核心以不同方式进行处理。当异常发生时,CPU内核能感知异常发生,而且会生成一个目标异常等级(targetr exception level)。CPU会进行一次执行状态切换(Interprocessing)。
异常基本概念
1、中断
在ARM64处理器中,中断请求被分为普通中断请求(Interrupt Request IRQ)和快速中断请求(Fast Interrupt Request FIQ)两种。中断发生的时刻与当前正在执行的指令无关,因此中断是一种异步异常。
2、中止
中止包含指令中止(instruction abort)和数据中止(data abort)两种。通常指访问内存地址时遇到了错误(如缺页等),处理器内部的MMU捕获这些错误并且报告给处理器。数据中止发生在处理器加载活存储外部存储单元时,指令中止发生在处理器尝试执行某条指令时。
3、复位
复位(reset)操作时优先级最高的一种异常处理。复位操作通常用于让CPU复位引脚产生复位信号,从而让CPU进入复位状态,从而重新启动。
4、系统调用
ARMv8体系提供了3种软件产生的异常和3种系统调用。系统调用允许软件主动地通过特殊指令请求更高异常等级的程序所提供的服务。
SVC指令:允许用户态应用程序请求操作系统内核服务。
HVC指令:允许客户操作系统请求虚拟机监控器服务。
SMC指令:允许普通世界种的程序请求安全监控器服务。
5、异常等级
操作系统种,处理器运行模式通常分为两种:一种是特权模式,另一种是非特权模式。操作系统内核运行在特权模式,访问系统的所有资源;而应用程序运行在非特权模式,它不能访问系统的某些资源,因为它权限不够。这些特权模式在ARMv8的体系结构里称为异常等级EL。
6、同步异常和异步异常
同步异常指处理器执行某条指令而直接导致的异常,往往需要在异常处理函数里处理该异常后,处理器才能继续执行。比如数据中止时,我们在异常主力函数种修改这个地址,就需要等处理程序跑完才能继续执行。
异步异常指异常触发的原因与处理器当前正在执行的指令无关。主要包括SError、vSError、IRQ、vIRQ、FIQ、vFIQ这几种。
异常处理流程
可以参见A-Profile Architecture中章节D.1.10的描述,ARMv8在感知到异常时会执行如下:
1、ARMv8对异常的感知位于指令边界,即在时钟上,完成当前指令的执行,准备开始下一条指令执行之前,才会去检查有没有中断请求。因此可以说每执行一条指令,核心都会去检测有没有对应的请求。
2、当感知到异常存在时,首先会根据PSTATE寄存器中的I、F、A三位确认是否会响应对应的异常,如果确认要响应,则把PSTATE寄存器中的值保存到对应目标异常等级的SPSR_ELx中。
3、把返回地址保存在对应目标异常等级的ELR_ELx中。
4、把PSTATE寄存器里的DAIF标志位都置为1,相当于同时屏蔽调试异常、SError、IRQ、FIQ。意思是说执行异常处理时不再响应新的异常。
5、对于同步异常,要分析异常的原因,并把具体原因写入ESR_ELx中。
6、切换SP寄存器位目标异常等级的SP_ELx或者SP_EL0寄存器
7、从异常发生现场的异常等级切换到对应目标异常等级,然后跳转到异常向量表里。
异常向量表是一个由操作系统准备的一段内存数据。对ARMv8来说,异常向量表是一个包含16个入口的跳转指令表,每个入口占128个字节(32条指令),总共占用2KB对齐的内存空间。
在进入异常向量表并完成异常处理后,会执行一条ERET指令从异常返回:
1、从ELR_ELx中恢复PC指针
2、从SPSR_ELx中恢复PSTATE寄存器状态
然后继续上次的执行。
对于异步异常,由于异常处理本身与当前执行指令独立,所以完成异常处理后可以接着继续执行,就不用再执行异常发生前的那条指令了。
对于同步异常,比如数据异常,相当于异常发生时原来的指令并不会执行完,所以返回的时触发同步异常的那条指令,把它执行完。
异常向量表
当异常发生时,处理器必须跳转和执行与异常相关的处理指令。异常相关的处理指令通常存储再内存中,这个存储位置称为异常向量。在ARM体系架构中,异常向量存储在一个表中。在ARMv8中,每个异常等级都有专门的异常向量表。
ARMv8中每一个表项是128字节,每个跳转指令是4字节,总共可以放32条指令,向量表为:
处理器在内核态(EL1异常等级)中触发IRQ,并且系统通过配置SPSel寄存器来使用SP_ELx寄存器作为栈指针,处理器会跳转到“VBAR_EL1 + 0x280”地址处的异常向量中。如果系统通过配置SPSel寄存器来使用SP_EL0寄存器作为栈指针,那么处理器会跳转到“VBAR_EL1 + 0x80”地址处的异常向量中。
处理器在用户态(EL0)执行时触发了IRQ,假设用户态的执行状态为AArch64并且该异常会陷入EL1中,那么处理器会跳转到“VBAR_EL1 + 0x480”地址处的异常向量中。假设用户态的执行状态为AArch32并且该异常会陷入EL1中,那么处理器会跳转到“VBAR_EL1 + 0x680”地址处的异常向量中。
异常向量
在kernel中/arch/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
#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_compat, 32 // Error 32-bit EL0
#else
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
SYM_CODE_END(vectors)有关kernel_ventry中的宏展开:
.macro kernel_ventry, el, label, regsize = 64
.align 7
sub sp, sp, #S_FRAME_SIZE
b el\()\el\()_\label
.endm这里el为异常级别,label中为异常类型标签,regsize寄存器宽度。当CPU触发异常并跳转到向量表时,宏首先在栈上分配一块大小为S_FRAME_SIZE的空间,用于保存CPU寄存器的上下文,包括通用寄存器、异常返回地址、处理器状态等。b意思是跳转,这里就明白的说明了跳转到具体的异常处理函数处。