/* TODO 本系列文章是对 ARMv8 Cortex-a 系列编程向导手册拙劣的翻译和注解,若有出入,以官方文档为准 */
在 ARM 架构中,中断可以打断当前软件的执行流,中断被认为是异常的一种。
每当异常发生,都要求异常处理程序进行某些动作来确保系统的稳定性。每一种异常都有相关的异常处理程序。
AArch64 中存在如下的类别的异常:
中断。存在两种类型的中断:IRQ 与 FIQ。
FIQ 比 IRQ 的优先级更高,这两种类型的异常通常与内核的输入中断引脚有关系。外部硬件符合中断条件并发送给中断引脚,对应的中断类型会被触发。如果当前中断没有失能的话。
FIQ 与 IRQ 都是内核实际的物理信号,在绝大多数芯片上,各种各样的中断源由一个中断控制器控制。这个中断控制器负责对中断的仲裁与优先级,并提供内核需要的 IRQ 与 FIQ 信号。
由于 IRQ 与 FIQ 的发生不与内核正在执行的代码相关联,所以 IRQ 与 FIQ 被称为异步异常。
中止。指令提取失败或数据访问失败都可能导致中止。中止一般来自于外部内存系统在内存访问时返回一个错误响应。换句话说,MMU 也可以生成一个中止,操作系统可以根据 MMU 中止来动态分配内存。
指令提取中止会在指令尝试执行之前触发,数据访问中止会在读写操作之后触发。
中止被认为是同步异常,因为他们是指令执行的结果,异常处理函数的返回地址(ELR_ELn)保存着触发异常的指令地址。
复位。复位是一个特殊异常,且复位会让芯片跳转到已实现的最高异常等级。复位时,芯片指令执行的地址由芯片实现时定义。RVBAR_ELn
包含复位向量的地址,n
为芯片已实现的最高异常等级。
所有的核都有一个复位输入引脚,这是最高优先级的异常而且不可屏蔽,复位向量被用于芯片上电后执行初始化程序。
异常生成指令。某些指令的执行可以生成异常,执行这些指令通常用于请求高异常等级的代码来提供某些服务。
异常生成指令 | 描述 |
---|---|
SVC | SVC 指令能够让 EL0 的用户代码请求 EL1 的异常,即 OS 提供的服务 |
HVC | HVC 指令能够让 EL1 的 OS 内核代码请求一个 EL2 的异常服务 |
SMC | SMC 指令可以让 EL1 / EL2 的代码请求一个 EL3 的服务 |
如果在 EL0 异常等级触发了指令提取异常,那么这个异常会发生在 EL1,如果非安全状态下 HCR_EL2.TGE
置位,那么这个异常会发生在 EL2。
如果在其他任意的异常等级触发了指令提取异常,那么不会改变异常等级。
处理器只能在触发异常或异常返回时来实现异常等级的改变。当从高异常等级切换到低异常等级时,执行状态可以从 AArch64 切换为 AArch32,或保持不变;当从低异常等级切换为高异常等级时,执行状态可以从 AArch32 切换为 AArch64, 或保持不变。
异常流程图如下:
当异常发生时,内核跳转到异常向量表对应指定异常的实体,这个异常向量表包含了分配代码,且通常标识了异常发生的原因,并选择合适的异常处理程序来处理异常。当异常处理结束,使用 ERET
实现从异常返回。
在第四章中,描述了处理器的当前状态保存在 PSTATE
位域集合中,当异常发生时,PSTATE
位域集合保存在 SPSR_ELn
寄存器中。
根据异常发生时的执行状态, SPSR_ELn
的保存情况,分为下列两种:
SPSR.M
位域用于记录异常发生之前的执行状态:0 表示 AArch64,1 表示 AArch32.
PSTATE 位域集合如下(不全):
DAIF
位域可以用于屏蔽异常事件,如果想要屏蔽某一类型异常,置位即可:
异常屏蔽位 | 描述 |
---|---|
D | 调试异常屏蔽位 |
A | SError 中断屏蔽位,比如说,异步外部异常 |
I | IRQ 中断屏蔽位,置 1 屏蔽 |
F | FIQ 中断屏蔽位,置 1 屏蔽 |
SPSel
用于选择当前异常等级使用的堆栈指针。
IL
位域如果置位,那么下一个指令的执行会触发一个异常。
CurrentEL
保存当前异常等级。
NZCV
保存算术运算结果标志位。
当异常发生时,处理器硬件自动完成某些动作。比如,使用 PSTATE
位域更新 SPSR_ELn
,异常返回地址被保存在 ELR_ELn
中。类比下图:
注意:如果触发了同步异常或者 SError 异常,那么SER_ELn
寄存器会被更新,用于指示触发异常的原因。
当异常处理结束,必须使用 ERET
指令告知处理器,处理器会使用 SPSR_ELn
的值恢复 PSTATE
位域,并使用ELR_ELn
的值拷贝到 PC
寄存器中
ARMv8-A 架构提供了专门的寄存器用于保存子函数返回地址(X30)与异常返回地址(ELR_ELn).
每一个异常等级都拥有专门的堆栈指针:SP_ELn
, 除了 EL0 之前的其他异常等级,不仅可以使用该异常等级的堆栈指针寄存器SP_ELn
,还可以使用 SP_EL0
。
堆栈指针寄存器的切换通过 SPSel
完成,如下代码所示:
MSR SPSel, #0 // switch to SP_EL0
MSR SPSel, #1 // switch to SP_ELn
在 AArch64 架构中,异常要么是同步的,要么是异步的。
如果一个异常是由于指令的执行而产生的话,那么称为同步异常,并且ELR_ELn
保存的返回地址就是产生该异常的指令。
一个异步异常不由正在执行的指令产生,而是由外部信号导致,比如 IRQ 与 FIQ 以及 SError(system error)。
下列情况都能导致同步异常:
发生同步异常的情况有很多中:
SVC
、HVC
与 SMC
。其中,MMU 导致的中止在 linux 系统中也许是正常的操作,因为 linux 会使用 MMU 中止来分配新的内存页。
在 ARMv7-A 架构中,预取指中止,数据访问中止,未定义指令异常是分开的概念,但在 AArch64 中,它们都属于同步异常。
处理同步异常的寄存器 | 描述 |
---|---|
ESR_ELn | ESR_ELn 寄存器保存了同步异常产生的原因 |
FAR_ELn | FAR_ELn 寄存器保存了触发同步指令异常、数据访问中止、对齐错误异常的虚拟地址 |
ELR_ELn | ELR_ELn 寄存器保存了异常返回地址,也保存了导致数据访问中止异常的地址 |
SCR_EL3 | 用于指定哪些异常路由到 EL3 |
HCR_EL2 | 用于指定哪些异常路由到 EL2 |
如果运行在 EL0 的用户应用代码想要请求运行在 EL1的 OS 内核提供的服务,那么用户应用代码可以使用 SVC
指令生成一个异常,改变异常等级到 EL1。
这个指令可以传递一个参数,这个参数保存在特定寄存器中。
前面我们已经知道:
使用 SVC
指令生成 EL1 异常。
使用 HVC
指令可以生成 EL2 异常。
使用 SMC
指令可以生成 EL3 异常。
当处理器运行在 EL0 异常等级,那么代码不能直接调用 HVC
指令和 SMC
指令,只能调用 SVC
指令。
HVC
与 SMC
指令只能在 EL1 及更高的异常等级使用。
AArch64 中,未分配指令会导致一个同步异常。
下列场景会触发未分配指令异常:
HVC
。PSTATE.IL
位置位后,执行的第一条指令。ESR_ELn
包含了一些信息,异常处理函数可以通过这些信息来确定触发异常的原因,这些异常信息只包含同步异常与 SError,而不包含 IRQ 与 FIQ。
ESR_ELn
的位域定义如下:
SVC
指令时这个位域包含了调用 SVC
指令附带的立即数,如下:svc #0x12346
通过前面的学习得知,当异常发生时,处理器可以改变执行状态。
SPSR
寄存器包含了异常发生之前的执行状态与异常等级。
每一个异常等级的执行状态通过下列因素控制:
RMR_ELn
寄存器来改变最高异常等级的执行状态,(这会导致一个软复位)。SCR_EL3.RW
位与 HCR_EL2.RW
位控制。SCR_EL3.RW
在 EL3 中编码,并用于控制低异常等级的执行状态;HCR_EL2.RW
可以在 EL2 与 EL3 中编码,可以控制 EL1 与 EL0 的执行状态。一个 EL1 异常如下所示:
当一个异常发生时,异常等级也许会不变或者提高。
SCR_EL3
与 HCR_EL2
存在单独的位来控制 IRQ、FIQ、SError 的路由设置。
如果异常发生之前的执行状态位 AArch32, 异常发生后的执行状态为 AArch64, 那么我们我们可以访问 Xn 寄存器来访问 AArch32 的寄存器。
当异常发生,处理器必须执行对应异常的异常处理函数。异常处理函数的位置存储在异常向量中。
在 ARM 架构中,所有的异常向量都存储在异常向量表中。
在 AArch64 中,每一个异常等级都有自己的异常向量表(除了 EL0)。
每一个异常向量在异常向量表都有基于异常向量表起始地址固定的偏移地址。异常向量表的地址通过 VBAR_ELn
寄存器设置。
在 AArch64 中,每一个异常向量都有 0x80 字节宽度,而在 ARMv7 架构中,每一个异常向量只有 4 字节宽度。
所以,ARMv7 架构的异常向量必须是一个跳转指令,跳转到实际的异常处理函数。而 AArch64 中,异常向量的空间更宽,我们在异常向量中进行一些顶层的异常处理。
每一个异常向量表都包含 16 个异常向量,每一个异常向量拥有 0x80 字节宽度。
异常向量表可以分为 4 部分,每一部分包含 4 个异常向量,异常向量的使用取决于下列因素:
图解如下:
举一个例子:
如果代码运行在 EL1,此时触发了一个 IRQ 异常,这个 IRQ 异常与 hypervisor 与 secure 无关,被路由到 EL1,且此时 SPSel 位被置位,所以我们使用 SP_EL1 堆栈指针,那么此时异常向量的地址为 VBAR_EL1 + 0x280
。
在 AArch64 中,由于不存在 LDR PC, [PC, #offset]
跳转指令,所以我们在异常向量中必须使用更多的指令来跳转到实际的异常处理函数。
复位异常的地址是完全独立的地址,该地址基于芯片实现定义的,这个地址可以在 RVBAR_EL1/2/3
中查看。
在 ARM 架构中,中断一般由外部的信号触发,外部信号输出到内核的 IRQ 与 FIQ 引脚。 FIQ 常常被用于安全中断源。
当 AArch64 状态下触发一个异常,那么 PSTATE 的中断掩码位域会自动设置。这意味着中断处理过程中异常被失能。如果软件想要支持异常嵌套,那么软件需要手动清除 PSTATE 的中断屏蔽位,如下:
MSR DAIFClr, #0
IRQ_Handler
SUB SP, SP, #<frame_size> // SP = SP -
STP X0, X1, [SP} // Store X0 and X1 at the base of the frame
STP X2, X3, [SP] // Store X2 and X3 at the base of the frame + 16 bytes
... // more register storing
...
// Interrupt handling
BL read_irq_source // a function to work out why we took an interrupt
// and clear the request
BL C_irq_handler // the C interrupt handler
// restore from stack the corruptible registers
LDP X0, X1, [SP] // Load X0 and X1 at the base of the frame
LDP X2, X3, [SP] // Load X2 and X3 at the base of the frame + 16 bytes
... // more register loading
ADD SP, SP, #<frame_size> // Restore SP at its original value
…
ERET
如果使能中断嵌套,那么我们不仅需要保存通用目的寄存器,还必须保存 SPSR_ELn
与 ELR_ELn
,然后重新使能中断(清除中断屏蔽位)。流程图解如下:
对于 AArch64, ARM 提供了专门的中断控制器,中断控制器的编程接口定义在 GIC 架构中。
这一章节主要介绍 GICv2。(但是 ARMv8-A 一般使用 GICv3!)
GIC 支持对软件中断、私有中断以及共享外设中断的路由设置,比如多核系统中,设置中断绑核。
GIC 架构提供了管理中断源,设置中断行为与设置中断绑核的寄存器。软件可以对单个中断进行使能或失能、设置优先级等操作。
GIC 接收外部信号并通过 IRQ 与 FIQ 信号发送给内核。
从软件的角度,GIC (GICv2)包含下列两个主要的功能模块:
GIC 功能模块 | 描述 |
---|---|
Distributor | 用于控制单个中断源的属性,比如优先级,中断状态,安全状态,路由信息,使能与失能状态。 |
CPU Interface | 用于屏蔽某一优先级的中,应答中断,通知 Distributor 中断处理结束,每一个核都有自己的 CPU Interface |
中断通过中断向量号标识,每一个中断向量号独一无二的对应一个中断源,软件可以根据中断向量号来识别当前触发的中断,并调用对应的中断处理函数。中断向量号由芯片设计时确定。
中断可以分为下列类型:
通过 Distributor 的寄存器设置,中断可以设置为边沿触发或电平触发。
任何一个中断可以处于下列中断状态:
中断优先级与中断绑核都可以在 Distributor 中配置。
一个发送给 Distributor 的已触发的外设中断处于 Pending 状态,Distributor 会将当前最高优先级的 Pending 中断发送给对应核的 CPU Interface,对应核的 CPU Interface 比较中断屏蔽优先级,如果通过,会通过 IRQ 与 FIQ 信号引脚触发对应的核的异常。
当中断处理函数服务一个中断时,必须读 CPU Interface 寄存器应答中断,当中断处理函数结束时,必须写 CPU Interface 寄存器来报告中断处理结束。
GICv2 可以作为内存映射的外设访问,所有的核可以访问同一个 Distributor, 但是每一核都有自己的 CPU Interface,每一个核使用相同的寄存器地址来访问自己的 CPU Interface.
Distributor 可以让我们配置每一个中断的属性,比如:
CPU Interface 可以设置中断屏蔽优先级,并帮助内核服务一个中断。
在芯片复位后,GICv2 的 Distributor 与 CPU Interface 都是失能的,此时我们必须初始化 GICv2。
对于 Distributor, 初始化代码必须配置每一个中断的优先级,安全属性,失能中断,最后,初始化代码必须通过GICD_CTLR
寄存器使能 Distributor 本身。
对于 CPU Interface, 初始化代码必须设置中断优先级屏蔽设置,与中断抢占设置,然后初始化代码必须通过 GICC_CTLR
寄存器来使能 CPU Interface 本身。
在内核处理中断之前,软件代码必须设置中断向量表,清除PSTATE
中断屏蔽位等。
当一个核获取一个中断,PC会跳转到异常向量表的指定异常向量处开始执行。
异常向量会跳转到实际的中断服务函数中,在中断服务函数中我们必须读中断应答寄存器来获取中断ID号。通过中断 ID 号,我们可以调用特定的中断服务回调函数。
当中断服务结束,我们必须写中断结束寄存器来改变中断状态,并通知 GIC 中断处理结束,可以发送下一个中断给 CPU。
如果 CPU 内核读取到中断 ID 号为 1023, 那么表示没有中断等待处理(一般不会发生这种情况)。
GIC 管理很多个中断源并通过 IRQ 与 FIQ 发送给 CPU 内核。