
aarch64异常模型以及Linux arm64中断处理
iliuqi
严格来说,中断是说软件执行流程的东西,但是,在arm术语中,统称为异常。异常是需要特权软件(异常处理程序)执行某些操作以确保系统顺利运行的条件或系统事件。每种异常类型都有一个异常处理程序。一旦处理完异常,特权软件就会让内核准备好恢复它在处理异常之前所做的任何事情。下面介绍了几种异常:
Interrupt
一般有两种,分为irq和fiq。fiq的优先级高于IRQ,这两种异常通常都与内核上的输入引脚相关。假设中断未被禁用,外部硬件断言了一个中断请求并在当前指令完成执行时触发相应的异常类型(irq or fiq),fiq和irq对core来说都是物理信号,当被断言时,如果内核当前已启用,则会发生相应的异常。在几乎所有系统上,各种中断源都使用中断控制器连接。中断控制器对中断进行仲裁并确定其优先级,进而提供串行化的单个中断信号,然后将其连接到内核的FIQ或IRQ信号。由于IRQ和FIQ中断的发生在任何时间,与内核正在执行的软件没有直接关系,因此它们被归类为异步异常 。Abort
在指令获取失败(指令中止)或数据访问失败(数据中止)时生成中止。它可来源于内存访问错误时的外部内存系统(可能表明指定的地址与系统中的实际内存不对应)。也可来源于core内的内存管理单元 (MMU) 生成中止。操作系统可以使用MMU中止来为应用程序动态分配内存。
当一条指令被提取时,它在流水线中被标记为abort。仅当内核随后尝试执行指令时,才会发生指令中止异常。异常发生在指令执行之前。如果在中止指令到达流水线执行阶段之前清除了流水线,则不会发生中止异常。数据中止异常是由于加载或存储指令而发生的,并且被认为是在数据读取或写入之后发生的。
如果abort是由于执行或试图执行指令流而生成的,并且返回的地址提供了引起中止指令的详细信息,则该中止被描述为同步abort。
异步abort不是通过执行指令生成的,而返回地址可能并不总是会提供引起abort原因的详细信息。在ARMv8-A中,指令和数据abort是同步的。异步异常包括IRQ/FIQ和System errors (SError)。参考 Synchronous and asynchronous exceptions。Reset
复位被视为最高异常级别的特殊向量。
这是 ARM 处理器在触发异常时的指令跳转位置。这个向量使用 IMPLEMENTATION DEFINED 地址。RVBAR_ELn 包含此复位向量地址,其中 n 是实现的最高异常级别的编号。所有内核都有一个复位输入,并在复位后立即发生复位异常。它是最高优先级的异常,不能被屏蔽。此异常用于在上电后在内核上执行代码以对其进行初始化。生成异常的指令
执行某些指令会产生异常。通常执行此类指令可以从运行更高权限级别的软件请求服务:- Supervisor Call (SVC) 指令可以使用户模式程序能够请求操作系统(OS)服务。
- Hypervisor Call (HVC) 指令使客户操作系统(guest OS)能够请求管理程序服务。
- Secure monitor Call(安全监控 SMC)指令使非安全世界请求安全世界服务
一、异常处理寄存器
我们已知的当前处理器的状态是存在于PSTATE寄存器内的。如果发生了异常,PSTATE寄存器里的内容会被保存到Saved Program Status Register(SPSR_ELn)中。而当前的PC的地址则存在于Exception Link Register(ELR_ELn),在异常结束时使用的返回地址存储在ELR_ELn中
二、同步与异步中断
在 AArch 中,异常可以是同步的,也可以是异步的。如果异常是由于执行或尝试执行指令而生成,并且返回的地址提供了指令的详细信息,则将其看作为同步异常。一个异步异常不会通过执行指令产生,并且返回的地址中,也许不会提供引起此异常的细节信息。
异步异常的来源是 IRQ(正常优先级中断)、FIQ(快速中断)或 SError(系统错误)。 系统错误有许多可能的原因,最常见的是异步数据中止(例如,将错误数据从高速缓存行写回外部存储器触发的中止)。
同步异常有多种来源:
- 指令从 MMU 中止。例如,通过从标记为从不执行的内存位置读取指令。
- 从 MMU 中止数据。例如,权限失败或对齐检查。
- SP 和 PC 对齐检查。
- 同步外部中止。例如,读取翻译表时发生的 abort。
- 未分配指令。
- 调试异常
2.1 同步中断
同步中断可能通过多种可能的原因产生
- 产生于 MMU. 例如权限失效或者访问标志错误的内存区域
- SP 和 PC 对齐检查
- 未定义的指令
- 服务呼叫(SVC,SMC,HVC)
2.2 Exception Syndrome Register(ESR_ELn)
异常状态寄存器(Exception Syndrome Register, ESR_ELn)包含允许异常处理程序确定异常原因的信息。它只对针对同步异常和 SError 做更新,不为 IRQ 或 FIQ 更新,因为这些中断处理程序通常从通用中断控制器(GIC)的寄存器中获取状态信息。
- ESR_ELn 的 Bits[31:26] 表示异常类,它允许处理程序区分各种可能的异常原因 (如未分配的指令,源自 MCR/MRC 的到 CP15 的异常,FP 操作的异常,执行了 SVC,HVC 或 SMC,数据中止和对齐异常)。
- 位 [25] 表示陷入的指令长度 (0 表示 16 位指令,1 表示 32 位指令),并且也为某些异常类设置。
- 位 [24:0] 形成指令特定症状 (ISS) 字段,该字段包含特定于该异常类型的信息。例如,当执行系统调用指令 (SVC、HVC 或 SMC) 时,该字段包含与操作码相关的立即值,例如对于 SVC 0x123456 的0x123456。
三、AArch64异常向量表
当异常发生时,处理器必须执行与异常对应的处理程序代码。 存储处理程序的内存位置称为异常向量。 在 ARM 体系结构中,异常向量存储在一个表中,称为异常向量表。
每个异常级别都有自己的向量表,即 EL3、EL2 和 EL1 各有一个。 该表包含要执行的指令,而不是一组地址。 个别异常的向量位于表开头的固定偏移量处。 每个表基的虚拟地址由基于向量的地址寄存器 VBAR_EL3、VBAR_EL2和 VBAR_EL1 设置。
向量表中的每个条目有 16 条指令长。 与每个条目为 4 个字节的 ARMv7 相比,这本身就是一个重大变化。ARMv7 向量表的这种间距意味着每个条目几乎总是某种形式的分支,指向内存中其他地方的实际异常处理程序。在 AArch64 中,向量的间距更宽,因此顶层处理程序可以直接写入向量表中。
下图展示了一个向量表。基址是由VBAR_ELn给出,然后每一项都与一个定义好了的偏移量。每一个表都有16项,每项128字节(32调指令)大小。该表实际上由4组4项组成。使用哪一项取决于几个因素
- 异常类型(SError、FIQ、IRQ 或同步)
- 如果在相同的异常级别处理异常,则要使用的堆栈指针(SP0 或 SPx)
- 如果异常是在较低的异常级别上被接受的,则降低一个异常的执行状态 (AArch64 或 AArch32)
举一个例子:
如果内核代码在EL1处执行并且产生IRQ中断信号,发生了IRQ异常。在Linux内核中经过处理,在SP_EL1处设置了SPSel位,因此使用了SP_EL1。因此会从VBAR_EL1+0x280开始执行。那问题来了VBAR_EL1+0x280的地址是什么呢?为啥要从这个地址来执行?
这里介绍一下VBAR_EL1寄存器:
VBAR寄存器,全名为Vector Base Address Register
这个寄存器其实就是存放向量表基地址的,下面介绍向量表基地址在linux内核中是如何被设置的?
3.1 linux kernel arm64 中断向量表
1 | /* |
3.1.1 kernel_entry
kernel_entry的看起来还是比较复杂的,这里说明几点:
- .align=7 这个说明代码是按照2^7=128对齐的,这个和向量表中的每一个offset对齐
- 最终调用的函数为
b el\el\ht\()_\regsize\()_\label
1 | .macro entry_handler el:req, ht:req, regsize:req, label:req |
所以也就是调用了kernel_entry 0 64
1 | .macro kernel_entry, el, regsize = 64 |
3.1.2 VBAR_EL1寄存器设置
那向量表是什么时候被Linux内核的VBAR寄存器的呢?
1 | //linux-5.15/arch/arm64/kernel/head.S |
okay,到现在为知,我们知道了向量表的创建、VBAR_EL1寄存器的地址的设置。那当EL1_IRQ中断触发后会执行到哪里呢?我们所知道的是中断触发后,会进入我们设置的中断处理函数中去执行,那两者是如何关联起来的呢?
3.1.3 el\el\ht()\regsize()\label()_handler函数
上文分析到kernel_entry,最终调用的是b el\el\ht\()_\regsize\()_\label
1 | .macro entry_handler el:req, ht:req, regsize:req, label:req |
所以这个例子,我们中断触发后会走到el1h_64_irq_handler函数
3.1.4 el1h_64_irq_handler
1 | asmlinkage void noinstr el1h_64_irq_handler(struct pt_regs *regs) |
所以最终执行了handle_arch_irq函数,而这个函数是在irq.c中被设置的
3.1.5 handle_arch_irq
1 | //linux-5.15/arch/arm64/kernel/irq.c |
3.1.6 gic_handle_irq
kernel启动后,gic中断控制器的驱动启动会执行到gic_init_bases
1 | //linux-5.15/drivers/irqchip/irq-gic-v3.c |
3.1.7 handle_domain_irq
这里补充两个知识点:
- IRQ number
CPU需要为每一个外设中断编号,我们称之为IRQ number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断 - HW interrupt ID
对于interrupt controller而言,它收集了多个外设的interrupt request line并向上传递,因此,interrupt controller需要对外设中断进行编码。Interrupt controller用HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。
这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个interrupt controller上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制
1 | int handle_domain_irq(struct irq_domain *domain, |
3.1.8 注册中断处理函数
这段以函数request_irq为例,此函数会调用request_threaded_irq
1 | int request_threaded_irq(unsigned int irq, irq_handler_t handler, |