现代软件被开发成分成不同的模块,每个模块都对系统和处理器资源有不同的访问级别。其中一个例子是操作系统内核和用户应用程序之间的分割。操作系统需要执行我们不希望用户应用程序能够执行的操作。内核需要对系统资源的高级访问,而用户应用程序需要的配置系统的能力有限。特权规定了软件实体可以看到和控制哪些处理器资源。
AArch64体系结构通过实现不同级别的特权来实现这种拆分。当前特权级别只能在处理器发生异常或从异常返回时进行更改。因此,这些特权级别在Arm体系结构中被称为异常级别(Exception Levels)。
用EL表示Exception Level,如下图所示级别越高,等级越高(EL0级别最低)。发生异常时,等级可以保持不变或者向上切换。异常退出时,等级可以保持不变或者向下切换。
哪些程序执行在相应的EL等级下,没有绝对的要求。但是一般而言,用户程序执行在EL0,os(linux)执行在EL1,EL2可以由管理程序使用,EL3可以由固件和安全网关代码使用。这就意味着大部分的PE处理执行在EL0,EL1。
只有当发生以下情况时,异常级别才能改变:
在AArch64 Exception module中,有两种类型的privilege:
这两种类型的特权都会受到当前的特权异常级别的影响。
MMU设置内存属性:包括读写,访问权限。并且MMU的配置保存在系统寄存器里面,因此对这些寄存器的访问控制由当前异常等级决定。
例如:当处理器在EL0中执行时启动的内存访问将根据无特权的访问权限进行检查。来自EL1、EL2和EL3的内存访问将根据特权访问权限进行检查。
系统寄存器用于保存处理器的配置设置,这些系统寄存器的组合定义了当前处理器的上下文。而系统寄存器的访问由当前的异常级别控制。
通常在高等级的异常下可以访问低等级异常下的寄存器。
例如:VBAR_EL1(Vector Base Address Register)是向量基地址寄存器。从_EL1的后缀可以标明软件至少需要EL1权限才能访问寄存器。
该体系结构有许多概念相似函数的寄存器,这些寄存器的名称仅因异常级别后缀而不同。这些都是独立的、单独的寄存器,它们在指令集中有自己的编码,并将在硬件中单独实现。
这些寄存器具有相似的名称,以反映它们执行相似的任务,但它们是完全独立的寄存器,具有自己的访问语义。系统寄存器名称的后缀表示可以从中访问该寄存器的最低异常级别。
每个已实现的异常级别都有一个SCTLR(System Control Register)系统控制寄存器。每个寄存器控制该EL的体系结构特性,如MMU、缓存和对齐检查:
System Control Register | Function |
---|---|
SCTLR_EL1 | Top-level system control for EL0 and EL1 |
SCTLR_EL2 | Top-level system control for EL2 |
SCTLR_EL3 | Top-level system control for EL3 |
EL1和EL0共享相同的MMU配置,控制仅限于在EL1上运行的特权代码。因此没有SCTLR_EL0,所有控件都来自EL1可访问寄存器。其他控制寄存器通常都遵循这个模型。
在系统的一般操作中,特权异常级别控制着它们自己的配置。但是,更多的特权级别有时会访问与较低的异常级别关联的寄存器。例如,这可以用于实现虚拟化特性和上下文切换。
在本文档中,关于AArch64异常模型,讨论了以下系统寄存器:
AArch64架构提供了四个异常级别。还有两个执行状态和最多四个安全状态。Armv8-A或Armv9-A处理器的当前状态由异常级别和当前执行状态决定。
PE只能在reset或异常级别切换时更改执行状态。当PE在异常级别之间切换时,可以更改执行状态,但是AArch32和AArch64之间的切换有以下两条规则:
每个异常级别的执行状态由下一个更高的已实现的异常级别上的控制寄存器定义。这就利用了EL2处的系统管理程序配置寄存器(HCR_EL2)和EL3处的安全配置寄存器(SCR_EL3)。在复位时,如果可切换,EL3的执行将由一个外部引脚设置。
大多数Corex-A系列处理器都支持两种执行状态
更多详见:TrustZone for AArch64
如果实现了TrustZone,那么处理器可以处于Secure状态或Non-secure状态。这是由SCR_EL3.NS位控制。
EL3是最有特权的异常级别,并且EL3的安全状态是固定的。EL3能够访问已存入的系统寄存器的所有副本。
在Armv8-A中,EL3始终处于安全状态。在Armv9-A中,EL3是安全状态的一部分,除非实现了RME(下面一小节会介绍,这里看不懂没关系)。
无论何时,要从一种安全状态切换到另一种安全状态,都必须通过EL3。EL3上的软件负责管理对不同可用安全状态的访问并充当看门人,控制对EL2、EL1和EL0的安全状态的访问。SCR_EL3.NS位允许在从EL3返回时更改安全状态。
在EL0、EL1和EL2时,PE可以处于安全状态或非安全状态。例如:
NS_EL1:非安全状态,异常级别1
S_EL1:安全状态,异常级别1
切换安全状态的更多细节详见: TrustZone for AArch64和Realm Management Extension
Armv9-A支持RME。当实现RME时,还支持另外两个安全状态:
RME将EL3从其他安全状态下剥离出来,因此EL3也被称为root(因为访问EL3需要被Secure、Non-secure和Realm所信任)。
下图显示了启用RME的PE中的安全状态,以及这些安全状态如何映射到异常级别:
更多RME介绍详见:Realm Management Extension
这是一个可实现选择,是否实现所有异常级别,以及每个实现的异常级别允许哪些执行状态。
EL0和EL1是唯一必须实现的、且强制性的。EL2和EL3是可选的。
如果EL3或EL2尚未实现,应考虑以下事项:
在Armv8.0-A中,EL2只存在于非安全状态下,因为在安全状态下没有虚拟化支持。Armv8.4-A增加了S. EL2作为一个可选特性的启用位(SCR_EL3。EEL2),以提供向后兼容性。
在Armv9-A中,如果支持EL2,则必须支持所有安全状态。但是失能位仍然存在。
Armv8-A处理器实现可以选择哪些执行状态对每个异常级别有效,这是定义的实现。如果在异常级别上允许AArch32,则必须在所有较低的异常级别上都允许它。例如,如果EL3允许AArch32,那么必须在所有较低的异常级别上都允许它。
许多实现允许所有执行状态和所有异常级别,但也有现有的有限制的实现。例如:
您可以参考处理器的技术参考手册(Technical Reference Manual, TRM)来检查支持哪些异常级别。
异常(Exception)情况是指通常需要采取补救措施或通过特权软件更新系统状态以确保系统顺利运行的条件或系统事件。因此,异常是可能导致当前正在执行的程序被挂起的任何事件。发生异常会导致状态发生更改,以执行代码来处理该异常。下图为处理一个异常(Handing an exception)
其他的处理器体系结构可能会将其描述为一种中断(Interrupt)。在AArch64中,中断是外部生成的特定类型。
总结:同步异常和异步异常,异步异常也被称为中断
同步异常:系统内部可控。同步异常是可能由当前正在执行的指令引起的或与之相关的异常。同步异
常与执行流是同步的,因为它们与当前执行的指令直接相关。
异步异常:通常来自外设,不可控制。异步异常与当前执行的指令没有直接关联,通常是来自处理器
外部的系统事件。
异常有许多不同的原因,包括以下原因:
当发生异常时,处理器不会执行当前代码中的下一条指令,而是停止当前的执行,并分支到一段代码来处理请求。此代码被称为异常处理程序。一旦处理了事件,执行就可以返回到原始程序。每个异常类型都有它自己的异常处理程序。图上图所示。
Arm架构将异常分为两大类类型:同步异常和异步异常。
同步异常是可能由当前正在执行的指令引起的或与之相关的异常。同步异常与执行流是同步的,因为它们与当前执行的指令直接相关。例如,一条试图写入由MMU定义的只读位置的指令将触发同步异常。
对于同步异常将满足以下几点:
尝试执行一个无效的指令可能会导致同步异常。导致无效指令的原因有很多,包括未定义的指令、当前异常级别不允许的指令或已被禁用的指令。任何试图执行核心无法识别的指令的尝试都会生成一个未定义的异常。
该体系结构还允许控制实体,如操作系统或管理系统监控程序(Hypervisor),设置陷阱,以在较低的异常级别上拦截操作。当执行给定操作,例如读取特定寄存器时,陷阱会触发异常。
例如,EL1上的操作系统内核可能会在EL0上禁止使用浮点指令,以节省在应用程序之间进行上下文切换的时间。这被称为惰性上下文切换(Lazy context-switching);例如,如果在上下文切换之前没有使用SIMD或浮点(Floating Point, FP)单元,则压栈的寄存器数量可以减少。然后,可以使用一个陷阱异常来处理边缘情况。
在这种情况下,OS内核可以通过禁用SIMD/FP单元来监视SIMD/FP操作的状态。当执行FP或SIMD指令时,陷阱异常被带到EL1的OS内核。然后,内核可以启用SIMD/FP单元,执行失败的指令,并设置一个标志,说明已使用了SIMD/FP单元。这确保了在下一个上下文交换中将大型SIMD/FP寄存器文件包含在上下文切换的寄存器上下文中。如果在下一个上下文开关中没有断言的标志,则不需要包含SIMD/FP寄存器。
捕获异常的能力对于虚拟化尤为重要。有关异常用于虚拟化的更多信息详见:AArch64 virtualization
简化说明:当执行未定义指令、当前异常等级不允许或者失效指令,会触发同步异常
同步异常也可能由内存访问引起。这可能是由于MMU执行的检查,或由于内存系统返回的错误。
例如,在使能MMU时,将检查所有由加载和存储指令引起的内存访问。如果您尝试从非特权代码访问特权地址,或尝试写入只读地址,则MMU会阻止访问并触发内存管理单元(MMU)故障。因为MMU生成的错误是同步的,所以在内存访问之前发生异常。
在AArch64中,同步aborts会导致同步异常。异步aborts会导致一个SError中断异常。(SError下面会异步异常会讲解)
简化说明:从非特权访问特权地址,写只读地址会触发同步异常
有些指令会故意生成和获取异常。这些指令用于实现系统接口调用,以允许较少特权的软件向较多特权的软件请求服务。这些方法有时被称为系统调用,通常用于基于软件的APIs。
Arm体系结构包括异常生成指令SVC、HVC和SMC。这些指令的目的只是为了生成异常,并使PE能够在异常级别之间移动:
当PE在EL0执行时,它不能直接调用EL2的管理程序或EL3的安全监视器,因为这只能从EL1和更高的Level实现。EL0处的应用程序必须使用对内核的SVC调用,并让内核调用到更高的异常级别执行该操作。
假设已经实现了各自的异常级别,OS内核(EL1)可以执行HVC指令来调用EL2处的管理程序,或者使用SMC指令调用EL3处的安全监视器。类似地,从EL2,PE可以使用SMC指令来调用EL3安全监视器。具体情况如下图所示:
因为不能采用异常来降低ELs,所以在EL2处进行的SVC调用不能导致输入返回到EL1。
如前面讨论的关于无效指令和陷阱异常(3.1.13.1.1 Invalid instructions and trap exceptions)的问题,管理程序(Hypervisor)可能会向EL1呈现系统的模拟视图。在这种情况下,Guest OS(虚拟化环境中运行的客户操作系统)将不能使用SMC直接调用设备固件。相反,通过设置统管理程序配置寄存器(Hypervisor Configuration Register)中的HCR_EL2.TSC,这样的调用将被陷入到EL2上。
调试异常是被路由到托管调试器的异常级别的同步异常。然后,调试器代码执行得很像异常处理程序代码。
以下为常见的同步调试异常,包括:
有关调试异常以及如何处理它们的更多信息详见:AArch64 self-hosted debug guide
某些类型的异常是在外部生成的,因此与当前的指令流不同步。
异步异常与当前执行的指令没有直接关联,通常是来自处理器外部的系统事件。这可能是软件需要响应的系统事件,如定时器活动或触摸屏幕。我们不知道它们什么时候会发生。
根据定义,如果异常不同步,则该异常是异步的。异步异常也被称为中断。
在发生异步异常时,程序流将被中断,并传递给代码以专门处理此外部请求。不可能准确地保证何时会发生异步异常,而AArch64架构只需要在有限的时间内发生它。
异步异常将分为以下小结讨论:
Physical interrupts(物理中断)
Virtual interrupts(虚拟中断)
Masking(屏蔽)
物理中断是指响应PE外部的信号而产生的,通常由外围设备。系统不是轮询外部信号,而是通过产生中断来通知core必须发生一些事情。
例如,一个系统可能会使用一个通用的异步接收器/发射器(UART)接口来与外部世界进行通信。当UART接收到数据时,它需要一种机制来能够告诉处理器,新的数据已经到达,并准备好被处理了。UART可以使用的一种机制是生成一个中断,以便向处理器发出信号。
复杂的系统可以有许多具有不同优先级级别的中断源,包括嵌套中断处理的能力,在其中一个高优先级的中断可以中断一个低优先级的中断。core响应此类事件的速度可能是系统设计中的一个关键问题,这被称为中断延迟
接下来,我们将讨论不同类型的物理中断。
系统错误(SError)是一种异常类型,旨在由内存系统为响应意外事件而生成。我们并不期望发生这些事件,但需要知道它们是否发生。这些都是异步报告的,因为触发该事件的指令可能已经退出。
SError的一个典型例子是之前所说的外部异步abort。出现SError中断的例子包括:
SErrors被视为一个单独的异步异常类,因为您通常对这些情况有单独的处理程序。错误生成是由定义实现(IMPLEMENTATION DEFINED)。
Arm体系结构有两种异步异常类型,IRQ和FIQ,旨在用于支持外围设备中断的处理。这些是用于信号外部事件,如计时器关闭,并不代表系统错误。它们是与处理器的指令流异步的预期事件。
IRQ和FIQ具有独立的路由控制,通常用于实现Secure和Non-secure中断。
在旧版本的Arm体系结构中,FIQ被用作更高优先级的快速中断。这与AArch64不同,其中FIQ与IRQ具有相同的优先级
在几乎所有的情况下,中断控制器都与系统中的AArch64处理器配对,用于整理、优先级排序和处理所有的中断。所有的ARM实现都使用通用中断控制器(GIC)架构来管理IRQs和FIQs。GIC执行中断管理、优先级和路由任务,为每个物理中断类型提供一个信号。
该节在GIC中会单独讨论
使用虚拟化的系统对中断处理的需要更为复杂。有些中断可能由管理程序处理,有些中断可以在VM中处理。虚拟机所看到的中断都是虚拟中断。虚拟中断可以由连接外部的中断控制器产生或者由软件产生。因此,需要额外的机制来支持这一点,在AArch64中有对虚拟中断的明确支持。
设置其中一个位相当于一个中断控制器断言一个中断信号到一个vCPU。该方法的一个含义是,需要系统管理程序来模拟虚拟机中的中断控制器的操作。当用于频繁的操作时,这可能会导致大量的开销,因此建议使用中断控制器。
GICv2及更高版本通过提供物理CPU接口和虚拟CPU接口,同时支持物理中断和虚拟中断的信令。在Armv8.4-A中添加了对安全状态下的虚拟化的支持,并需要启用和支持Secure EL2。
- 该节在GIC中会单独讨论
- 虚拟中断的进一步讨论详见:AArch64 virtualization
可以暂时屏蔽物理和虚拟异步异常。这意味着异步异常可以保持在挂起状态,直到它们被揭开并接受异常。这对于处理嵌套的异常特别有用。
无法屏蔽同步异常。这是因为同步异常是由指令的执行直接引起的,因此,如果它们被搁置或忽略,将会阻塞执行。
2021年的扩展,Armv8.8-A和Armv9.3-A,增加了不可屏蔽的中断(NMI)支持。当支持和启用时,可以通过该功能将具有超优先级的中断呈现给处理器。当没有超优先级的中断被屏蔽时,超优先级允许进行中断。
Masking将在Routing and interrupt controllers 中进一步介绍。在关于异常屏蔽和不可屏蔽中断(NMI)将在**Exception masking and non-maskable interrupts (NMI)**讨论。
正如我们在Exception types一章中提到的,当发生异常时,当前程序流将被中断。本章更深入地讨论了在实践中如何处理异常情况。
在AArch64中,在讨论处理异常时使用了特定的术语:
因此,当识别异常时处理器处于的状态称为taken from。PE在识别异常之后的状态是taken to。例如,可以从(taken from)AArch32 EL0到(taken to)AArch64 EL1的异常。
处理异常后,系统需要从已获取的状态返回。这被称为一个异常返回,并且Arm体系结构有一个触发异常返回的指令。
当发生异常时,处理器将PE的当前状态与异常返回地址一起保存,然后进入一个特定的模式来处理该异常。
当前状态taken from于PSTATE寄存器,这将在Saving the current processor state一节中进一步讨论。此状态写入已保存的程序状态寄存器(Saved Program Status Register,SPSR),返回地址写入异常链接寄存器(Exception Link Register,ELR)。对于同步异常和SErrors,另一个寄存器,异常综合寄存器(Exception Syndrome Register,ESR)也会被更新。该寄存器记录了异常发生的原因。
在ARM架构中,PSTATE(Processor State)和CPSR(Current Program Status Register)是两个用于记录和控制处理器状态的寄存器。尽管它们在不同的ARM架构版本中可能具有不同的名称和位域配置,但通常具有相似的功能。
PSTATE:
PSTATE是在ARMv8-A架构中引入的寄存器,用于表示和控制处理器的执行状态、模式和相关标志。
PSTATE包含了一系列位域,例如NZCV(条件标志位)、DAIF(禁用异步中断标志)、SSBS(规范性存储旁路攻击保护)等。
通过读取和修改PSTATE寄存器的值,可以控制和反映处理器的当前状态和相关标志。
CPSR:
CPSR(Current Program Status Register)是在ARMv7及之前的架构版本中使用的寄存器,用于记录当前程序的执行状态和相关标志。
CPSR包含了一系列位域,如N(负数标志)、Z(零标志)、C(进位标志)、V(溢出标志)等。
与PSTATE类似,通过读取和修改CPSR寄存器的值,可以控制和反映处理器的当前状态和相关标志。
当异常taken to AArch64状态的异常级别(ELx)时,将发生以下所有情况:
除此之外:
对任何给定异常的异常处理都从一个称为异常向量的固定内存地址开始。当发生异常时,处理元素(PE)分支到向量表中的一个位置。
AArch64中的向量表与许多其他处理器体系结构不同,因为它们包含指令,而不是地址。每个入口最多包含32条指令;刚好足以执行基本压栈和调用特定于异常的处理代码。
最多包含32条指令:这是通过使用异常类型的编码作为索引值来实现的。由于异常类型的编码使用5位来表示,因此最多可以表示32种不同的异常类型
向量表的位置通常被配置为包含处理程序代码,以执行通用操作,并根据异常类型分支到进一步的异常处理代码,如下图所示:这个向量代码被限制为32个字的代码。异常处理程序包含处理请求操作的代码,并启用从异常状态返回。
每个异常类型的目标都是要获取异常的异常级别(EL)。因此,采取异常将允许路由到不同的EL。唯一的方法是通过采取一个异常去获得特权。并且放弃或减少特权的唯一方法是执行异常返回。
这就意味着:
需要注意的是,采取异常或执行异常返回并不需要更改EL。异常的目标可能与当前的EL相同。目标EL可以根据异常类型或根据系统寄存器内的配置位隐式定义。
在AArch64中,您只有将EL0的异常带到更高的异常级别。因为异常不会在EL0处理并且也没有EL0异常向量表。
正如在Execution and Security states章节中所讨论的,PE也只能在重置或从异常中获取或返回时更改执行状态。AArch32和AArch64执行状态之间的交互作用称为互处理。关于更改执行状态,必须记住以下几点:
在Armv9-A体系结构和一些Armv8-A实现中,AArch32仅在EL0处受支持,并且不能接受异常到EL0。这意味着,要更改EL0的执行状态,就需要移动到一个更有特权的EL,然后再次返回。
由于异常可以从AArch32获取到AArch64,AArch64处理程序代码可能需要访问AArch32寄存器。AArch32通用寄存器被直接映射到AArch64寄存器,以允许处理程序代码访问AArch32寄存器:
当从AArch32移动到AArch64时,在AArch32状态下无法访问的寄存器保留了之前的AArch64执行的值。对于在两种执行状态下都可以访问的寄存器,64位寄存器的上半部分要么包含0,或者是使用AArch32从异常级别获取异常的旧值:
下面的部分将讨论过程中从正在执行的异常到完成异常返回的每个步骤。
AArch64有一个被称为PSTATE的处理器状态的概念,正是这个信息存储在SPSR中。PSTATE包含诸如当前异常级别和算术逻辑单元(ALU)标志。在AArch64中,这包括:
例如,PSTATE中的异常掩码位(DAIF)允许屏蔽异常事件。当设置了相关位时,相关异常不会发生。
当发生异常时,必须保留当前状态,以便稍后可以返回到正确的状态。PE会自动保留异常返回地址和当前的PSTATE。如在发生异常PSTATE的值将存储在SPSR(Saved Program Status Register)中。
每个异常级别都有一个SPSR:SPSR_ELx。在发生异常时,使用的SPSR_ELx是发生异常到的异常级别。例如,如果异常带到EL1,则更新SPSR_EL1。
然后,PE将当前的PSTATE更新为体系结构中为该异常类型定义的PSTATE,以反映新的状态。这包括更新受影响的目标异常级别和安全级别。
一旦PSTATE被更新,PE就可以分支到向量表中的异常处理程序。执行从由异常类型定义的目标异常级别上的异常向量开始。为了从异常中返回,处理器可以将SPSR的内容恢复到PSTATE,并分支到ELR中指定的返回地址。如下图所示:
每个异常类型都有一个目标异常级别:
异常的目标由架构固定,或由使用路由控制的软件进行配置。然而,异常永远不能被带到EL0。
同步异常将根据与异常生成指令SVC、HVC和SMC相关联的规则进行路由。当实现时,可以将其他异常类路由到EL2(管理程序)或EL3(安全监视器)。IRQ、FIQ和SErrors的路由是独立设置的。例如,一个实现可以将所有IRQ路由到EL1,如下图所示:
使用安全配置寄存器(Secure Configuration Register)SCR_EL3和系统管理程序配置寄存器(Hypervisor Configuration Register)HCR_EL2来配置路由。SCR_EL3寄存器指定哪些异常被路由到EL3,而HCR_EL2寄存器同样指定哪些异常被路由到EL2。
这些类型允许将不同的中断类型路由到不同的异常级别。例如,IRQ可能由EL1的操作系统处理,而SErrors通常是由运行在EL3的固件处理。
在每个控制寄存器中都有单独的位,允许单独控制IRQ、FIQ和SError中断。此外,使用SCR_EL3创建的路由配置也会覆盖由SCR_EL2创建的路由配置,当他们的配置相冲突时。这些寄存器中的路由位在重置时有一个未知值(UNKNOWN value),因此它们必须由软件初始化。
Arm通用中断控制器(GIC)体系结构通常用于执行中断管理、优先级化和路由等任务。这可以减少与虚拟化相关的管理花销。
一个异常不能被带到一个未实现的异常级别。对未实现的异常级别的调用是未定义。类似地,当对给定的安全状态禁用或未实现EL2时,尝试返回到EL2会导致在异常返回(ERET)上生成错误。
前面在Masking一节中提到过,异步异常可以暂时屏蔽并保持待定状态,直到异常被解除屏蔽并获取。路由也会影响屏蔽,因为屏蔽的能力取决于当前和目标异常级别。
路由到较高异常级别的异常无法被较低的EL屏蔽。例如,如果中断在EL1中被屏蔽,并且一个中断被路由到EL2,那么EL1掩码将不会影响EL2的操作。但是,请注意,当PE执行上次从EL2退出时,EL2中断可能已经被屏蔽,这仍然可能导致中断在进入EL2时被屏蔽。
路由到当前异常级别的异常可以被当前级别屏蔽。路由到较低异常级别的异常总是被屏蔽。异常将等待,直到PE更改为等于或低于路由到的异常级别。这符合你永远不能因为采取例外而失去特权的规则。
接受异常的异常级别的执行状态由更高的异常级别决定。假设所有异常级别都已实现,下表显示了如何确定执行状态:
当使用AArch64对异常进行处理时,向量表是正常内存中的一个区域,其中包含随后用于处理异常的指令。
当发生异常时,core需要能够执行与该异常对应的处理程序。处理程序充当调度代码,识别异常的原因,然后调用相关的处理程序代码来处理异常。在内存中存储处理程序的位置称为异常向量。在AArch64中,异常向量存储在异常向量表中。
每个异常级别都有它自己的向量表,其基地址由它自己的向量基地址寄存器VBAR_EL定义,其中为1、2或3。请注意,没有针对EL0的专用向量表,因为异常永远不会被带到EL0。
所有的向量表都使用相同的格式。根据异常的类型和异常的来源,有不同的异常类别入口。每个异常类别在距离向量基地址的固定偏移量处都有一个异常向量。
所使用的入口取决于以下因素:
这将被特权软件记录到系统寄存器中,以便当发生异常时,core可以找到相应的处理程序。这些向量被分为两组,每一组被分为两个子组:
因此,异常到达的哪个向量已经提供了关于异常时处理器状态的信息。我们将在Stack pointer selection and stack pointer registers一节中介绍所选堆栈点的重要性。
请注意,从较低的EL报告的执行状态直接适用于taken to的EL,而不一定是taken from的EL。
例如,如果从EL0到EL1出现异常,则根据EL1的执行状态选择向量表入口。但是,如果从EL0到EL2出现异常,则所使用的向量依赖于EL1,而不是EL0。这是因为我们通常希望在EL2上运行虚拟机或其他虚拟化管理软件,管理运行在EL0和EL1上运行的虚拟机(VMs)。虚拟机监控程序(hypervisor)需要了解虚拟机中操作系统的执行状态,而不是通常由操作系统托管的应用程序。也有一些特殊的情况,比如当HCR_EL2.TGE被设置为1。更多详细介绍见:AArch64 virtualization
一个简单的异常处理程序将指示系统堆栈所有可损坏的寄存器,这可能是根据异常类型不同的所有寄存器,然后调用特定于异常的处理程序代码。在返回时,这些寄存器将被恢复,并调用一个异常返回指令(ERET)。
向量表中的32个字足以包含用于堆栈、调用特定于异常的处理程序代码、恢复寄存器和启动ERET返回的指令。下图展现了简单异常处理程序示例:
当在AArch64中执行时,该体系结构允许选择两个堆栈指针寄存器: SP_EL0或SP_EL,其中是当前的异常级别。例如,在EL1中,可以选择SP_EL0或SP_EL1。这在AArch64 vector tables一节中提供的向量表示例中可见。
当发生异常时,将自动选择目标异常级别SP_ELx的堆栈指针。PE所选择的堆栈指针使用PSTATE按照以下规则进行配置:
当我们在向量表上输入第一级处理程序时,我们正在使用SP_ELx。默认情况下,这应该用于保存寄存器上下文。但是,我们通常会切换到使用SP_EL0来进行进一步的处理。
例如,ELx堆栈可以用于存储以其他方式可能受到异常处理程序的影响或损坏的寄存器,这样可以在从异常返回时恢复它们。这对于在处理由堆栈溢出引起的异常时维护有效的堆栈特别有用。返回操作将需要反向执行此操作。
例如,如果PE在EL1处执行,并且发出IRQ中断信号,则会触发IRQ异常。如果IRQs已被配置为路由到EL1,那么它将使用SP_EL1.SP,除非PSTATE有不同的配置。在上面提供的示例向量表中,执行将从地址VBAR_EL1 + 0x280开始执行。
这种分离意味着我们可以将用于异常输入和高级异常处理的堆栈与用于其他线程的堆栈隔离开来,例如,C代码具有高堆栈使用率。SP_EL0通常分配更大的堆栈空间,因此在这种情况下总是使用更好的堆栈。这也意味着异常条目并不依赖于主堆栈的状态。有一些单独的向量表条目支持这种变化详见上面AArch64 vector tables向量表偏移量。
正如在AArch64 vector tables一节中提到的,异常到达的向量已经提供了关于异常时处理器状态的信息。例如,由于异常在异常输入和退出期间应该被屏蔽,因此向量表“使用SP_ELx时来自当前EL的异常(Exception from the current EL while using SP_ELx)”区域中的代码只会在异常情况下被触发。这个向量分组中的异步异常表明发生了重大的系统错误,需要处理,例如内核错误。
当异常处理程序处理异常后,处理程序返回到异常发生之前运行的代码。
它通过执行以下操作来实现:
ERET指令将以前的处理器状态从关联的SPSR和分支恢复到ELR中记录的异常返回地址。返回到异常状态的级别是根据SPSR_EL中的值进行配置的,其中是从其中返回的级别。SPSR_ELx还包含目标执行状态。
在SPSR_ELx中指定的执行状态必须与任何一个SCR_EL3.RW或HCR_EL2.RW中的配置相匹配。否则将产生一个非法的异常返回。
在执行ERET指令时,PSTATE被从SPSR_ELx恢复,程序计数器(PC)被更新到ELR_ELx中的值。这两个更新是原子和无条件执行的,这样PE不会处于未定义的状态。
如在Saving the current processor state一节中介绍的,每个EL上的异常链接寄存器(ELR)保存异常返回首地址。这是由异常的类型所决定的。
对于同步服务调用,如SVC,这是紧随异常调用之后的指令。对于其他同步异常,这是生成该异常的指令的地址。对于异步异常,异常返回首地址是在执行异常时没有完全执行的第一个指令。允许处理程序在需要时根据异常类型修改ELR_ELx的内容。
本节从端到端(end-to-end)的角度提供了同步和异步异常处理的简要示例。
如在Synchronous exceptions一节中所讨论的,如果一个异常是通过直接执行指令生成的,并且返回地址表示导致该异常的指令,则可以认为该异常是同步的。以下寄存器提供了处理异步异常(SErrors)的关键信息:
异步异常,也称为中断,是PE和当前执行指令的外部。Arm架构不定义何时采用异步异常。因此,异步异常相对于其他同步异步异常的优先级是实现定义(IMPLEMENTATION DEFINED)的。
例如,如果PE在AArch32中的EL0处执行应用程序代码,并且发生IRQ中断。在本例中,HCR_EL2和SCR_EL3已被配置为能够将IRQ异常路由到AArch64中的EL1。具体总结如下图所示:
前面的例子表示了一个非常简单的中断情况。有时需要能够禁用或屏蔽其他中断,以覆盖当前已完成的异常。物理和虚拟异步异常都可以暂时屏蔽,并保持挂起状态,直到打开屏蔽并发生异常。这是通过屏蔽相同类型的中断来完成的,直到以后显式地启用为止。
当处理器对AArch64执行状态执行异常时,PSTATE中断掩码(PSTATE.DAIF)将被自动设置。DAIF代表调试、中止(SError)、IRQ和FIQ。DAIF字段是4位,每个位对应于前面提到的异常类型之一。通过在字段中写入一个1,我们可以屏蔽或忽略异常类型。它可以进入待决,但不能得到处理。换句话说,PE在位被解屏蔽之前不会分支到异常处理程序,这有效地禁用了无法获取该类型的进一步异常。
中断总是在发生中断的异常级别上被屏蔽。无法屏蔽同步异常。这是因为同步异常是由指令的执行直接引起的,因此,如果它被等待或忽略,将会阻止执行。
你不能屏蔽异步异常路由到更高异常级别的。软件仍然可以在较低的EL处设置PSTATE掩码位,但是这并不会阻止异常被taken。例如,如果你将FIQ路由到EL3 (SCR_EL3.FIQ = 1),然后在EL1或EL2中设置PSTATE.F = 1并不会阻止中断被taken。
路由到较低异常级别的异常总是被屏蔽,而与PSTATE无关。例如,如果IRQ被路由到EL2或EL1 (SCR_EL1.IRQ = 0),然后当PE在EL3中时,IRQ总是被隐式掩码(屏蔽)。
如果软件支持嵌套异常,例如,允许高优先级的异常中断低优先级异常的处理,那么软件需要显式地重新启用中断。在压栈可损坏的寄存器之后,就可以嵌套异常了。
在2021年的Armv8.8-A和Armv9.3-A扩展中添加了不可屏蔽中断(NMI)支持。具有超优先级的中断被归类为NMI,即使当PSTATE异常掩码会阻止它时,也可以使用。
对NMIs有一些限制。在第一次接受中断异常时,所有的中断都被屏蔽,包括NMI。这是为了允许软件在被后续的中断覆盖之前保存所需的状态。
在某些时间点上,软件无法处理任何中断,包括NMI。为了解决这个问题,我们增加了一个新的PSTATE mask:ALLINT。这允许软件在屏蔽不存在中断、大多数中断(不包括NMIs)和所有中断(包括NMIs)之间进行选择。
使用系统控制寄存器可以选择三种型号的NMI支持:
NMI支持要求系统的中断控制器能够以超优先级呈现中断。如果使用Arm的通用中断控制器,则在GICv3.3和GICv4.2中添加了对NMIs的支持。
Learn the architecture - AArch64 Exception
Model