ARM64异常处理
ARM64的异常处理主要在armv8.6手册的D1章节中,如下图:(实验对应chapter11)
1.ARMv8架构规定,处理器在复位后默认会进入当前系统支持的最高异常等级。例如:如果系统支持EL3,那么在上电或复位后,处理器通常会运行在EL3异常等级。
2.异常分为同步异常和异步异常(异步异常又称为是中断),同步异常包括:系统调用(svc、hvc、smc等)、MMU引发的异常(例如缺页)、SP和PC对齐检查、未分配的指令等,可参考armv8.6手册的D1.12;异步异常包括:IRQ中断、FIQ中断、SError、vIRQ、vFIQ、vSError,可参考armv8.6手册的D1.13。
3.当异常发生时,CPU硬件会自动做以下事情:
-
PSTATE保存到SPSR_ELx(例如从EL0陷入到EL1,则这里的x为1)
-
返回地址保存到ELR_ELx(例如从EL0陷入到EL1,则这里的x为1)
-
PSTATE寄存器里的DAIF域都设置为1,相当于把调试异常、系统错误(SError)、IRQ中断以及FIQ中断都关闭了
-
对于同步异常,会更新ESR_ELx寄存器,该寄存器里面包含了同步异常发生的原因
-
切换SP寄存器为目标异常等级的SP_Elx或者SP_EL0寄存器
-
切换到对应的EL,然后跳转到异常向量表里执行
4.当异常发生时,操作系统需要根据异常发生的类型,跳转到合适的异常向量表(即设置好异常向量表的地址),异常向量表的每一个表项保存一个异常处理的跳转函数,然后跳转到恰当的异常处理函数并处理异常
5.异常的返回:操作系统执行一条eret语句,硬件会自动从ELR_ELX寄存器中恢复PC指针,从SPSR_ELx寄存器恢复处理器的状态。
6.注意,X30寄存器保存的是子函数的返回地址,使用ret指令来返回。ELR_ELx寄存器保存的是异常返回地址,使用eret指令来返回。
7.对于异步异常,它的返回地址是中断发生时的下一条指令,或者没有执行的第一条指令;对于不是system call的同步异常,返回的是触发同步异常的那一条指令;对于system call,它是返回svc指令的下一条指令。
8.异常处理的路由:异常发生的时候,异常处理可以在当前EL也可以在更高的EL;EL0不能用来处理异常;同步异常是可以在当前的EL里处理的;对于异步异常,可以路由到EL1,EL2,EL3处理,需要配置HCR_EL2以及SCR_EL3相关寄存的某些位,具体的路由机制可查看手册的D1.13.1节的Table D1-10路由表,也可参考ARM64体系结构编程与实践的P143。
9.栈的选择:每个异常等级都有对应栈指针寄存器SP:SP_EL0,SP_EL1,SP_EL2,SP_EL3;栈必须16字节对齐,硬件可以检测栈指针是否对齐;当异常发生时,并跳转到目标异常等级时,硬件会自动选择SP_ELx;操作系统负责分配和保证每个异常等级EL对应的栈是可用的。当配置SP_EL0作为栈指针时,可以使用后缀“t”来标记,例如,如果在EL1里使用SP_EL0作为栈指针,我们可以使用“SP_EL1t”来表示。当配置SP_ELx作为栈指针时,可以使用后缀“h”来标记,例如,如果在EL1里使用SP_EL1作为栈指针,我们可以使用“SP_EL1h”来表示。
10.当异常发生时,切换到高级别的EL,这个EL运行的模式是根据HCR_EL2.RW位来决定的,该位记录了EL1要运行在哪个模式,该位为1表示EL1的执行状态为AArch64,而EL0的执行状态由PSTATE.nRW字段来确定,为0表示 EL0 和 EL1 都在 AArch32 执行状态下。当异常发生时,执行模式可以发生改变,一个aarch32的应用程序在运行,这时候来了一个中断,它可能会跑到aarch64执行状态下的EL1处理这个中断。从一个异常返回时,SPSR寄存器记录了:返回到哪个EL(SPSR.M[3:0])、返回目标EL的执行模式(SPSR.M[4],该位为0表示aarch64,为1表示aarch32,可参考手册的D1.6.4节)。
11.从EL2切换到EL1,需要做以下几件事情:
- 设置HCR_EL2寄存器,最重要的是bit31的RW域,表示EL1要运行在哪个执行环境里,如下图是该位的描述:
- 设置SCTLR_EL1寄存器需要设置MMU,MMU的设置对应该寄存器的第0位,EL0和EL1的大小端设置分别对应该寄存器的第24和25位,如下图:
- 设置SPSR_EL2寄存器,设置模式M域为EL1h,另外需要关闭所有的DAIF,M域各位的作用如下图,用于设置异常返回时应该返回到的异常等级,以及返回后应该使用的堆栈指针(SP)寄存器:
- 设置异常返回寄存器ELR_EL2,让其返回到EL1_ENTRY汇编函数里
- 执行ERET
- 示例如下:
12.异常向量表:每个异常等级EL都有自己的异常向量表,EL0除外。异常向量表的基址需要设置到VBAR_ELx寄存器中(Vector Base Address Register),VBAR寄存器的bit[10:0]是保留的,也就是说异常向量表的起始地址必须以2KB字节对齐,每个表项可用于存放32条指令,一共128字节。异常向量表中共包含四种发生异常的情况,每个情况中又包含四类异常,所以每个异常向量表共有16个表项,如下图给出了各个表项的偏移:
上图中右侧的四类情况从下到上分别为:异常发生的EL和处理异常的EL为同一EL,且使用的SP为SP_EL0;异常发生的EL和处理异常的EL为同一EL,且使用的SP为SP_ELx;异常发生的EL比处理异常的EL级别更低,且发生异常的EL使用的是aarch32;异常发生的EL比处理异常的EL级别更低,且发生异常的EL使用的是aarch64。如下图为Linux5.0内核的异常处理对应的异常向量表:
上图中的.align 11是因为异常向量表的起始地址必须以2KB字节对齐,上图中的kernel_ventry是宏定义,具体作用是跳转到后面相应的sync、irq函数去处理异常。异常处理时,通常需要保存栈框(即保存上下文),在linux内核中定义了一个struct pt_regs的数据结构来描述内核站上保存寄存器的排列信息,通常用于保存中断上下文,其结构如下图:
ARMv8在取指时,如果指令地址不是4字节对齐的话,会导致指令地址未对齐的异常,这是一个同步异常。
13.异常综合信息寄存器ESR_ELx:这个寄存器一共包含4个字段,其中:bit32~63是保留的比特位;bit26~31是异常类型(Exception Class,简称EC),这个字段指示发生异常的类型,同时用来索引ISS域(Bit 0~24);bit25,IL,表示同步异常的指令长度;bit0~24,ISS(Instruction Specific syndrome)具体的异常指令编码,这个异常指令编码表依赖不同的异常类型,不同的异常类型有不同的编码格式。如下图所示:
EC字段共有如下图所示的类型:
例如对于数据异常(Data Abort),ISS的编码如下图所示:
上图中各字段的含义如下图所示:
根据上表中的数据异常的状态码可以进一步得知异常来源信息,具体可参考ARMv8.6手册的D13.2.36节。所以查看异常信息时的步骤为,先得到ESR_ELx寄存器的值,然后根据EC字段的值解析ISS字段获取详细的异常信息。FAR寄存器是失效地址寄存器,它存储了发生异常时刻的虚拟地址,具体可参考ARMv8.6手册的D13.2.39节。
ARM64中断
1.中断:ARM核心的两个和中断相关的管脚是nIRQ和nFIQ,每个CPU核心都有一对这样的中断相关的管脚,下图参考自Cortex_a72手册:
PSTATE状态中有两个比特位和中断相关:I位用来屏蔽IRQ中断,F位用来屏蔽FIQ中断,这两个位是CPU核心和这两个中断有关的总开关。Cortex-A72共有四个CPU核心,每个CPU核心都有4个generic timer:1.PNS Timer,非安全世界的EL1物理Timer;2.PS Timer,安全世界的EL1物理Timer;3.PNS EL2 Timer,hypervisor里的EL2物理Timer;4.虚拟Timer。
2.对于每个Timer,支持两种触发方式,第一种是64位的compare value的方式,第二种是time value的方式,就是初始化一个值,当他递减到0时,timer中断就触发(参考D11.2.4节)。例如对于PNS Timer,其相关初始化寄存器是在ARM Core中(可参考ARMv8.6手册),其中CNTP_CTL_EL0(D18.16节)寄存器是EL1物理Timer的控制寄存器,它有三个位ENABLE、IMASK、ISTATUS分别控制是否使能timer、是否屏蔽该中断、指示中断是否来临;CNTP_TVAL_EL0(D18.18节)寄存器可以看作是一个32位的倒计时计数器,是供EL1物理Timer使用的,它存储着(CNTP_CVAL_EL0-CNTPCT_EL0)的值,当CNTP_CTL_EL0.ENABLE位被置1时可读取这个寄存器的有效值,其中CNTP_CVAL_EL0是EL1物理Timer的比较值寄存器,它存储着一个64位的值,CNTPCT_EL0寄存器存储着当前EL1物理Timer的实时值(注意从不同EL读取时返回值不一样,具体参考D18.20节的P2846),当CNTP_TVAL_EL0寄存器的值小于等于0时,也就是CNTPCT_EL0的值涨到CNTP_CVAL_EL0的值时,将CNTP_CTL_EL0.ISTATUS位置1,如果CNTP_CTL_EL0.ENABLE位为1且CNTP_CTL_EL0. IMASK位为0,则触发当前时钟的时钟中断。当向CNTP_TVAL_EL0寄存器中写入某个值timevalue时,CNTP_CVAL_EL0寄存器的值会被硬件自动设置为(CNTPCT_EL0+timevalue)。EL1的PNS Timer的中断处理流程为(这里使用的是legacy的中断控制器,树莓派4b同时支持legacy和GICv2的中断控制器,实验12-1):
-
初始化Timer,设置CNTP_CTL_EL0寄存器的ENABLE域为1
-
给Timer的Time Value一个初值,设置CNTP_TVAL_EL0寄存器
-
打开树莓派中断控制器中和Timer相关的中断,设置TIMER_CNTRL0寄存器中的CNT_PNS_IRQ为1(这是树莓派的寄存器,这样设置表示将core0的PNS时钟中断定义为IRQ中断,可参考树莓派手册BCM2711 ARM Peripherals的第6.5节)
-
打开PSTATE寄存器中的IRQ中断总开关
-
Timer中断发生
-
跳转到el1_irq汇编函数
-
保存中断上下文(使用kernel entry宏)跳转到中断处理函数
-
读取ARM_LOCAL中中断状态寄存器IRQ_SOURCEO
-
判断是否CNT_PNS_IRQ中断发生
-
如果是,重新设置Time Value
-
返回到el1_irq汇编函数
-
恢复中断上下文返回中断现场
中断上下文如下图所示:
3.传统的Legacy中断控制器:传统的中断控制器,如ARM的Basic Interrupt Controller(BIC),采用简单的中断请求机制,通常支持单核处理器,采用寄存器串联的方式将每个中断源都包含在内,包含中断enable寄存器、中断disable寄存器、中断状态寄存器。在中断发生时,外设通过IRQ向控制器发出信号,控制器根据固定的优先级将中断转交给处理器。中断处理方式较为单一,优先级调度简单,且缺乏动态路由和细粒度的管理,限制了其在现代复杂系统中的应用;GIC中断控制器:ARM的GIC设计用于多核处理器系统,支持高级的中断调度和优先级控制。GIC能根据中断优先级、路由规则和处理器核心的状态动态分配中断,支持多个中断源同时触发并有效地分配给不同核心。其多级中断控制和灵活的中断屏蔽机制使其能够适应现代多核系统和虚拟化环境,提供更高效的中断处理和响应能力。GIC的发展历史如下图:
4.GIC支持的中断类型有:SGI:软件产生的中断(Software Generated Interrupt),软中断即软件产生的中断,用于给其他CPU核心发送中断信号,中断号范围为0~15,SGI通常在操作系统中用作IPI中断(Inter-Processor Interrupt,进程间中断,是一种特殊的中断机制,用于多核处理器系统中,允许一个CPU核向其他CPU核发送中断信号。其主要目的是协调多个处理器核之间的工作,实现同步或通信);PPI:私有外设中断(Private Peripheral Interrupt)私有的外设中断,该中断是某个指定的CPU独有的,中断号范围为16~31; SP1:共享外设中断(Shared Peripheral Interrupt) 共享的外设中断,所有CPU都可以访问这个中断,中断号范围为32~1019;LPI:本地特殊外设中断(Locality-specific Peripheral Interrupt),GICv3新增的中断类型,是基于消息传递的中断类型。中断号1020~1023是保留的。
5.GIC中的中断状态:不活跃状态(inactive) :中断处于无效状态;等待状态(pending) :中断处于有效状态,但是等待CPU响应该中断。活跃状态(active) :CPU已经响应该中断。活跃并等待状态(active and pending) :CPU正在响应该中断,但是该中断源又发送中断过来。如下图所示:
6.GICv2的中断控制器如下图所示,与之相关联的寄存器由两部分组成,以GICD_开头的寄存器是与Distributor相关的,以GICC_开头的寄存器是与CPU Interface相关的:
下图来自GICv2官方手册:
GICD_ITARGETSRn(Interrupt Processor Targets Registers)这个寄存器可以用来设置中断路由,因为GICv2最多支持8核,所以每个中断源用8个比特位表示,相应比特位为1表示该中断源可以路由到某个CPU核,每个GICD_ITARGETSR32位,可以表示四个中断源的路由信息,而中断源0~31这32个中断(SGI和PPI)的路由是由硬件设定好的,所以GICD_ITARGETSR0~ GICD_ITARGETSR7这几个寄存器是只读的,也就是说实际上只能软件配置SPI中断的路由。该寄存器结构如下图:
GICv2中与中断相关的寄存器都是以这种形式实现的,一个类型寄存器实际上有n个物理寄存器来控制不同的中断,具体可参考手册ARM® Generic Interrupt Controller,Architecture version 2.0第4.1节。GICD_系列寄存器的基地址是0xff840000+0x1000(同时参考树莓派手册第五版6.5.1节和gic400手册3.1节)。
7.EL1的PNS Timer的中断处理流程为(这里使用的是GICv2的GIC400中断控制器,树莓派4b同时支持legacy和GICv2的中断控制器,实验13-1):
- 设置distributor和CPU interface寄存器组的基地址(基于memory map的访问方式),读取GICD TYPER寄存器,计算当前GIC最大支持多少个中断源:
- 初始化distributor,gic_dist_init(gic),可分为如下几步:首先disable关闭distributor;设置SPI中断的路由;设置SPI中断的触发方式,下图中设为level触发;disactive所有的中断源,然后disable所有中断源(需要使用中断进行中断注册时在enable相应的中断源);重新enable打开distributor
- 初始化CPU Interface,gic_cpu_init(gic),可分为如下几步:设置中断优先级mask level,本实验用的是PNS Timer,属于PPI30号中断,所以下图只设置了PPI和SGI的优先级,注意优先级设置对应的寄存器GICD_IPRIORITYRn属于GICD_;设置GIC CPU PRIMASK,低于所设置优先级的中断将不被响应;enable打开CPU interface
- 注册中断,本次实验使用的是PNS Timer,属于PPI30号中断,所以设置GICD_ISENABLERn相应位为1即打开对应中断也就是注册中断,如下图所示,还要设置CNTP_CTL_EL0和CNTP_TVAL_EL0寄存器打开PNS中断并设置中断时间间隔,可参考legacy中断实验(前文有介绍)。然后打开设备相关的中断,例如树莓派上的generic timer,需要打开ARM_LOCAL寄存器组中的TIMER_CNTRL0寄存器中相关的CNT_PNS_IRQ位,将PNS中断设置为IRQ中断。最后还要打开CPU的PSTATE中I位(PSTATE.I)。
8.中断响应过程为:
-
中断发生
-
进入异常向量表
-
跳转到GIC中断函数里,gic_handle_irq()
-
读取GICC_IAR寄存器,获取中断号
-
根据中断号来进行相应中断处理,例如读取的中断号为30,说明的是PNS的generic timer,然后跳转到generic timer的处理函数里。
-
处理完之后向GICC_EOIR寄存器中写入处理完的中断的中断号来通知CPU中断处理完成,如下图: