提示
- 该博客主要为个人学习,通过阅读官网手册整理而来(个人觉得阅读官网的英文文档非常有助于理解各个IP特性)。若有不对之处请参考参考文档,以官网文档为准。
1 Privilege and Exception Levels
1.1 为什么要划分权限?
现代软件被开发成分成不同的模块,每个模块都对系统和处理器资源有不同的访问级别。其中一个例子是操作系统内核和用户应用程序之间的分割。操作系统需要执行我们不希望用户应用程序能够执行的操作。内核需要对系统资源的高级访问,而用户应用程序需要的配置系统的能力有限。特权规定了软件实体可以看到和控制哪些处理器资源。
AArch64体系结构通过实现不同级别的特权来实现这种拆分。当前特权级别只能在处理器发生异常或从异常返回时进行更改。因此,这些特权级别在Arm体系结构中被称为异常级别(Exception Levels)。
1.2 Exception Levels
用EL表示Exception Level,如下图所示级别越高,等级越高(EL0级别最低)。发生异常时,等级可以保持不变或者向上切换。异常退出时,等级可以保持不变或者向下切换。
哪些程序执行在相应的EL等级下,没有绝对的要求。但是一般而言,用户程序执行在EL0,os(linux)执行在EL1,EL2可以由管理程序使用,EL3可以由固件和安全网关代码使用。这就意味着大部分的PE处理执行在EL0,EL1。
只有当发生以下情况时,异常级别才能改变:
- Taking an exception
- Returning from an exception
- Processor reset
- During Debug state
- Exiting from Debug state
当发生异常时,异常级别可以增加或保持不变,但不能通过采取异常来移动到较低的特权级别。当从异常返回时,异常级别可能会减少或保持不变,但不能通过从异常返回而移动到更高的特权级别。
1.3 Types of privilege
在AArch64 Exception module中,有两种类型的privilege:
- Privilege in the memory system:内存系统中的特权
- Privilege from the point of view of accessing processor resources:从访问处理器资源的角度来看,这是一种特权
这两种类型的特权都会受到当前的特权异常级别的影响。
1.3.1 Memory privilege
MMU设置内存属性:包括读写,访问权限。并且MMU的配置保存在系统寄存器里面,因此对这些寄存器的访问控制由当前异常等级决定。
例如:当处理器在EL0中执行时启动的内存访问将根据无特权的访问权限进行检查。来自EL1、EL2和EL3的内存访问将根据特权访问权限进行检查。
1.3.2 Register access
系统寄存器用于保存处理器的配置设置,这些系统寄存器的组合定义了当前处理器的上下文。而系统寄存器的访问由当前的异常级别控制。
通常在高等级的异常下可以访问低等级异常下的寄存器。
例如: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异常模型,讨论了以下系统寄存器:
2 Execution and Security states
AArch64架构提供了四个异常级别。还有两个执行状态和最多四个安全状态。Armv8-A或Armv9-A处理器的当前状态由异常级别和当前执行状态决定。
- Execution: AArch32与AArch64
- Security: Secure和Non-Secure,在Arm-v9A中还有RME(Realm Management Extension)状态
2.1 Execution state
PE只能在reset或异常级别切换时更改执行状态。当PE在异常级别之间切换时,可以更改执行状态,但是AArch32和AArch64之间的切换有以下两条规则:
- 对于AArch64而言:当从较低的异常级别切换到更高的级别时,执行状态可以保持不变或更改。
- 对于AArch32而言:当从较高的异常级别切换到较低的级别时,执行状态可以保持不变或更改。
将这两条规则放在一起意味着64位可以容纳32位,但却不是相反。例如,64位操作系统内核可以同时托管64位和32位应用程序,而32位操作系统内核只能托管32位应用程序。
每个异常级别的执行状态由下一个更高的已实现的异常级别上的控制寄存器定义。这就利用了EL2处的系统管理程序配置寄存器(HCR_EL2)和EL3处的安全配置寄存器(SCR_EL3)。在复位时,如果可切换,EL3的执行将由一个外部引脚设置。
2.2 Security states
大多数Corex-A系列处理器都支持两种执行状态
- Secure state:在这种状态下,PE可以同时访问安全(如:Android OS种保护诸如支付细节和密钥等信息)和非安全的物理地址空间,以及存储寄存器的安全副本。
- Non-secure state:这通常也被称为Normal World。在此状态下,PE只能访问非安全的物理地址空间。PE只能访问允许非安全访问的系统寄存器。
安全状态定义了可以访问哪些已实现的异常级别,当前可以访问哪些内存区域,以及如何在系统内存总线上表示这些访问。如果处于非安全状态,则PE只能访问非安全的物理地址空间。在安全状态下,PE可以同时访问安全和非安全的物理地址空间。
更多详见:TrustZone for AArch64
2.2.1 Changing Security state
如果实现了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
2.2.2 Realm Mangement Extension
Armv9-A支持RME。当实现RME时,还支持另外两个安全状态:
- Realm state:在这种状态下,PE可以访问非安全(Non-secure)和领域(Realm)的物理地址空间
- Root state:在这种状态下,PE可以访问所有的物理地址空间。根目录状态(Root state)仅在EL3中可用。
RME将EL3从其他安全状态下剥离出来,因此EL3也被称为root(因为访问EL3需要被Secure、Non-secure和Realm所信任)。
下图显示了启用RME的PE中的安全状态,以及这些安全状态如何映射到异常级别:
更多RME介绍详见:Realm Management Extension
2.3 Impact of implemented Exception levels
这是一个可实现选择,是否实现所有异常级别,以及每个实现的异常级别允许哪些执行状态。
EL0和EL1是唯一必须实现的、且强制性的。EL2和EL3是可选的。
如果EL3或EL2尚未实现,应考虑以下事项:
- EL2包含了许多虚拟化功能。没有EL2的实现无法访问这些特性。
更多详见:AArch64 virtualization - EL3是唯一可以改变安全状态的级别。如果一个实现选择不实现EL3,则该PE将只能访问单个安全状态。因此,你处于的状态永远是由实现定义的(IMPLEMENTATION DEFINED)。
已经开发了一些软件实现,需要平台来支持这些异常级别。例如,KVM Linux需要EL2、EL1和EL0
在Armv8.0-A中,EL2只存在于非安全状态下,因为在安全状态下没有虚拟化支持。Armv8.4-A增加了S. EL2作为一个可选特性的启用位(SCR_EL3。EEL2),以提供向后兼容性。
在Armv9-A中,如果支持EL2,则必须支持所有安全状态。但是失能位仍然存在。
Armv8-A处理器实现可以选择哪些执行状态对每个异常级别有效,这是定义的实现。如果在异常级别上允许AArch32,则必须在所有较低的异常级别上都允许它。例如,如果EL3允许AArch32,那么必须在所有较低的异常级别上都允许它。
许多实现允许所有执行状态和所有异常级别,但也有现有的有限制的实现。例如:
- Cortex-A35在所有异常级别上都支持AArch32和AArch64
- Cortex-A32只支持AArch32
- Neoverse-N2在EL0只支持AArch32。所有其他异常级别仅为AArch64
AArch32提供了与旧版32位体系结构的向后兼容性,并且从Armv9-A版本中,仅限于在EL0上的可选实现。Armv9-A在所有异常级别上都支持AArch64,并且仅在EL0上可选择地支持AArch32。这意味着可以运行之前版本应用程序,但不能运行内核、虚拟机管理程序或固件。
您可以参考处理器的技术参考手册(Technical Reference Manual, TRM)来检查支持哪些异常级别。
3 Exception types
异常(Exception)情况是指通常需要采取补救措施或通过特权软件更新系统状态以确保系统顺利运行的条件或系统事件。因此,异常是可能导致当前正在执行的程序被挂起的任何事件。发生异常会导致状态发生更改,以执行代码来处理该异常。下图为处理一个异常(Handing an exception)
其他的处理器体系结构可能会将其描述为一种中断(Interrupt)。在AArch64中,中断是外部生成的特定类型。
总结:同步异常和异步异常,异步异常也被称为中断
同步异常:系统内部可控。同步异常是可能由当前正在执行的指令引起的或与之相关的异常。同步异
常与执行流是同步的,因为它们与当前执行的指令直接相关。
异步异常:通常来自外设,不可控制。异步异常与当前执行的指令没有直接关联,通常是来自处理器
外部的系统事件。
异常有许多不同的原因,包括以下原因:
- Emulating virtual devices(模拟虚拟设备)
- Virtual memory management(虚拟内存管理)
- Handling software errors(软件处理error)
- Handling hardware errors(硬件处理error)
- Debugging
- Performing calls to different privilege or security states(执行对不同的特权或安全状态的调用)
- Handling interrupts (timers, device interactions)(处理中断(定时器、设备交互))
- Handling across different Execution states (known as interprocessing)(跨不同执行状态的处理(称为互处理))
当发生异常时,处理器不会执行当前代码中的下一条指令,而是停止当前的执行,并分支到一段代码来处理请求。此代码被称为异常处理程序。一旦处理了事件,执行就可以返回到原始程序。每个异常类型都有它自己的异常处理程序。图上图所示。
Arm架构将异常分为两大类类型:同步异常和异步异常。
3.1 Synchronous exceptions
同步异常是可能由当前正在执行的指令引起的或与之相关的异常。同步异常与执行流是同步的,因为它们与当前执行的指令直接相关。例如,一条试图写入由MMU定义的只读位置的指令将触发同步异常。
对于同步异常将满足以下几点:
- 该异常是由于直接或试图执行指令而产生的
- 处理异常后返回的地址(返回地址)与导致异常的指令有一个体系结构定义的关系
- 异常是精确的,这意味着在进入异常处理程序时呈现的寄存器状态在执行违规指令之前与每个指令都是一致的,而在执行它之后则不一致
有许多不同类型的同步异常,而且一个给定的指令可能会导致多个同步异常。Arm架构为同步异常提供了一个固定的优先级顺序。
以下各部分将更深入地讨论导致同步异常的一些不同原因: - Invalid instructions and trap exceptions(无效指令和陷入)
- Memory accesses(内存访问)
- Exception-generating instructions(异常产生指令)
- Debug exceptions(调试异常)
3.1.1 Invalid instructions and trap exceptions
尝试执行一个无效的指令可能会导致同步异常。导致无效指令的原因有很多,包括未定义的指令、当前异常级别不允许的指令或已被禁用的指令。任何试图执行核心无法识别的指令的尝试都会生成一个未定义的异常。
该体系结构还允许控制实体,如操作系统或管理系统监控程序(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
简化说明:当执行未定义指令、当前异常等级不允许或者失效指令,会触发同步异常
3.1.2 Memory accesses
同步异常也可能由内存访问引起。这可能是由于MMU执行的检查,或由于内存系统返回的错误。
例如,在使能MMU时,将检查所有由加载和存储指令引起的内存访问。如果您尝试从非特权代码访问特权地址,或尝试写入只读地址,则MMU会阻止访问并触发内存管理单元(MMU)故障。因为MMU生成的错误是同步的,所以在内存访问之前发生异常。
在AArch64中,同步aborts会导致同步异常。异步aborts会导致一个SError中断异常。(SError下面会异步异常会讲解)
简化说明:从非特权访问特权地址,写只读地址会触发同步异常
3.1.3 Exception-generating instructions
有些指令会故意生成和获取异常。这些指令用于实现系统接口调用,以允许较少特权的软件向较多特权的软件请求服务。这些方法有时被称为系统调用,通常用于基于软件的APIs。
Arm体系结构包括异常生成指令SVC、HVC和SMC。这些指令的目的只是为了生成异常,并使PE能够在异常级别之间移动:
- Supervisor Call(SVC)指令:允许EL0的用户程序在EL1请求操作系统服务
- Hypervisor Call(HVC)指令:如果实现了虚拟化扩展,则可使用HVC指令,它使操作系统能够在EL2上请求系统监控程序服务
- Secure Monitor Call(SMC)指令:如果实现了安全扩展,则可用,使正常世界(Normal world)能够从EL3上的固件请求安全世界服务
当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上。
3.1.4 Debug exceptions
调试异常是被路由到托管调试器的异常级别的同步异常。然后,调试器代码执行得很像异常处理程序代码。
以下为常见的同步调试异常,包括:
- Breakpoint Instruction exceptions(断点指令异常)
- Breakpoint exceptions(断点异常)
- Watchpoint exceptions(监视点异常)
- Vector Catch exceptions(矢量捕获异常)
- Software Step exceptions(软件步骤异常)
有关调试异常以及如何处理它们的更多信息详见:AArch64 self-hosted debug guide
3.2 Asynchronous exceptions
某些类型的异常是在外部生成的,因此与当前的指令流不同步。
异步异常与当前执行的指令没有直接关联,通常是来自处理器外部的系统事件。这可能是软件需要响应的系统事件,如定时器活动或触摸屏幕。我们不知道它们什么时候会发生。
根据定义,如果异常不同步,则该异常是异步的。异步异常也被称为中断。
在发生异步异常时,程序流将被中断,并传递给代码以专门处理此外部请求。不可能准确地保证何时会发生异步异常,而AArch64架构只需要在有限的时间内发生它。
异步异常将分为以下小结讨论:
-
Physical interrupts(物理中断)
- SError(系统错误)
- IRQ and FIQ
-
Virtual interrupts(虚拟中断)
-
Masking(屏蔽)
3.2.1 Physical interrupts
物理中断是指响应PE外部的信号而产生的,通常由外围设备。系统不是轮询外部信号,而是通过产生中断来通知core必须发生一些事情。
例如,一个系统可能会使用一个通用的异步接收器/发射器(UART)接口来与外部世界进行通信。当UART接收到数据时,它需要一种机制来能够告诉处理器,新的数据已经到达,并准备好被处理了。UART可以使用的一种机制是生成一个中断,以便向处理器发出信号。
复杂的系统可以有许多具有不同优先级级别的中断源,包括嵌套中断处理的能力,在其中一个高优先级的中断可以中断一个低优先级的中断。core响应此类事件的速度可能是系统设计中的一个关键问题,这被称为中断延迟
接下来,我们将讨论不同类型的物理中断。
3.2.2 SError
系统错误(SError)是一种异常类型,旨在由内存系统为响应意外事件而生成。我们并不期望发生这些事件,但需要知道它们是否发生。这些都是异步报告的,因为触发该事件的指令可能已经退出。
SError的一个典型例子是之前所说的外部异步abort。出现SError中断的例子包括:
- 已通过所有MMU检查,但随后在内存总线上遇到一个错误的内存访问
- 奇偶校验或错误纠正代码(ECC)检查RAMs,例如这些RAM内置在缓存中
- 通过将脏数据(dirty data)从高速缓存行回写到外部内存而触发的abort
SErrors被视为一个单独的异步异常类,因为您通常对这些情况有单独的处理程序。错误生成是由定义实现(IMPLEMENTATION DEFINED)。
3.2.3 IRQ and FIQ
Arm体系结构有两种异步异常类型,IRQ和FIQ,旨在用于支持外围设备中断的处理。这些是用于信号外部事件,如计时器关闭,并不代表系统错误。它们是与处理器的指令流异步的预期事件。
IRQ和FIQ具有独立的路由控制,通常用于实现Secure和Non-secure中断。
在旧版本的Arm体系结构中,FIQ被用作更高优先级的快速中断。这与AArch64不同,其中FIQ与IRQ具有相同的优先级
在几乎所有的情况下,中断控制器都与系统中的AArch64处理器配对,用于整理、优先级排序和处理所有的中断。所有的ARM实现都使用通用中断控制器(GIC)架构来管理IRQs和FIQs。GIC执行中断管理、优先级和路由任务,为每个物理中断类型提供一个信号。
该节在GIC中会单独讨论
3.2.4 Virtual interrupts
使用虚拟化的系统对中断处理的需要更为复杂。有些中断可能由管理程序处理,有些中断可以在VM中处理。虚拟机所看到的中断都是虚拟中断。虚拟中断可以由连接外部的中断控制器产生或者由软件产生。因此,需要额外的机制来支持这一点,在AArch64中有对虚拟中断的明确支持。
- vSError, Virtual System Error(虚拟系统错误)
- vIRQ, Virtual IRQ(虚拟IRQ)
- vFIQ, Virtual FIQ
(虚拟FIQ)
虚拟中断按每个中断类型进行控制。这些虚拟中断的功能与物理对应的中断相同,但是它们只能向EL1发出信号。
虚拟中断可以从EL2处的管理程序(hypervisor)或使用中断控制器生成。管理程序(hypervisor)必须在管理程序配置寄存器(Hypervisor Configuration Register,HCR_EL2)中设置相应的路由位。例如,要使能vIRQ信令,系统管理程序必须设置HCR_EL2.IMO。此设置将物理IRQ异常路由到EL2,并允许将虚拟异常信号发送到EL1。
在HCR_EL2中有三个位来控制虚拟中断的生成: - VSE:设置此位将注册一个vSError
- VI:设置此位将注册一个vIRQ
- VF:设置此位将注册一个vFIQ
设置其中一个位相当于一个中断控制器断言一个中断信号到一个vCPU。该方法的一个含义是,需要系统管理程序来模拟虚拟机中的中断控制器的操作。当用于频繁的操作时,这可能会导致大量的开销,因此建议使用中断控制器。
GICv2及更高版本通过提供物理CPU接口和虚拟CPU接口,同时支持物理中断和虚拟中断的信令。在Armv8.4-A中添加了对安全状态下的虚拟化的支持,并需要启用和支持Secure EL2。
- 该节在GIC中会单独讨论
- 虚拟中断的进一步讨论详见:AArch64 virtualization
3.2.5 Masking
可以暂时屏蔽物理和虚拟异步异常。这意味着异步异常可以保持在挂起状态,直到它们被揭开并接受异常。这对于处理嵌套的异常特别有用。
无法屏蔽同步异常。这是因为同步异常是由指令的执行直接引起的,因此,如果它们被搁置或忽略,将会阻塞执行。
2021年的扩展,Armv8.8-A和Armv9.3-A,增加了不可屏蔽的中断(NMI)支持。当支持和启用时,可以通过该功能将具有超优先级的中断呈现给处理器。当没有超优先级的中断被屏蔽时,超优先级允许进行中断。
Masking将在Routing and interrupt controllers 中进一步介绍。在关于异常屏蔽和不可屏蔽中断(NMI)将在**Exception masking and non-maskable interrupts (NMI)**讨论。
4 Handing exceptions
正如我们在Exception types一章中提到的,当发生异常时,当前程序流将被中断。本章更深入地讨论了在实践中如何处理异常情况。
在AArch64中,在讨论处理异常时使用了特定的术语:
- taken:当PE响应异常时,将拿到异常。
- taken from:在接受异常之前的PE状态是接受异常的状态
- taken to:在接受异常后立即出现的PE状态是接受异常到的状态
因此,当识别异常时处理器处于的状态称为taken from。PE在识别异常之后的状态是taken to。例如,可以从(taken from)AArch32 EL0到(taken to)AArch64 EL1的异常。
处理异常后,系统需要从已获取的状态返回。这被称为一个异常返回,并且Arm体系结构有一个触发异常返回的指令。
- return from:PE在执行返回指令时处于的状态
- return to:PE在异常返回指令执行后立即出现的状态
4.1 Taking an exception
当发生异常时,处理器将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)时,将发生以下所有情况:
- 在获取异常之前,PSTATE的内容被写入了SPSR_ELx
- 异常返回地址将被写入到ELR_ELx中
除此之外:
- 对于同步异常和SError中断,异常综合信息(异常的原因)将被写入ESR_ELx
- 对于与地址相关的同步异常,如MMU故障,触发该异常的虚拟地址将被写入故障地址寄存器(Fault Address Register, FAR_ELx)
对任何给定异常的异常处理都从一个称为异常向量的固定内存地址开始。当发生异常时,处理元素(PE)分支到向量表中的一个位置。
AArch64中的向量表与许多其他处理器体系结构不同,因为它们包含指令,而不是地址。每个入口最多包含32条指令;刚好足以执行基本压栈和调用特定于异常的处理代码。
最多包含32条指令:这是通过使用异常类型的编码作为索引值来实现的。由于异常类型的编码使用5位来表示,因此最多可以表示32种不同的异常类型
向量表的位置通常被配置为包含处理程序代码,以执行通用操作,并根据异常类型分支到进一步的异常处理代码,如下图所示:这个向量代码被限制为32个字的代码。异常处理程序包含处理请求操作的代码,并启用从异常状态返回。
每个异常类型的目标都是要获取异常的异常级别(EL)。因此,采取异常将允许路由到不同的EL。唯一的方法是通过采取一个异常去获得特权。并且放弃或减少特权的唯一方法是执行异常返回。
这就意味着:
- 当出现异常时,EL可以保持不变或增加
- 当异常返回时,EL可以保持不变或减少
需要注意的是,采取异常或执行异常返回并不需要更改EL。异常的目标可能与当前的EL相同。目标EL可以根据异常类型或根据系统寄存器内的配置位隐式定义。
在AArch64中,您只有将EL0的异常带到更高的异常级别。因为异常不会在EL0处理并且也没有EL0异常向量表。
正如在Execution and Security states章节中所讨论的,PE也只能在重置或从异常中获取或返回时更改执行状态。AArch32和AArch64执行状态之间的交互作用称为互处理。关于更改执行状态,必须记住以下几点:
- 对于AArch64:当从较低的异常级别移动到更高的级别时,执行状态可以保持不变或更改
- 对于AArch32:当从较高的异常级别移动到较低的级别时,执行状态可以保持不变或更改
对该体系结构的定义如下: - 如果异常级别使用AArch32,那么所有较低的异常级别必须使用AArch32
- 如果异常级别使用AArch64,那么所有较高的异常级别必须使用AArch64
在Armv9-A体系结构和一些Armv8-A实现中,AArch32仅在EL0处受支持,并且不能接受异常到EL0。这意味着,要更改EL0的执行状态,就需要移动到一个更有特权的EL,然后再次返回。
由于异常可以从AArch32获取到AArch64,AArch64处理程序代码可能需要访问AArch32寄存器。AArch32通用寄存器被直接映射到AArch64寄存器,以允许处理程序代码访问AArch32寄存器:
当从AArch32移动到AArch64时,在AArch32状态下无法访问的寄存器保留了之前的AArch64执行的值。对于在两种执行状态下都可以访问的寄存器,64位寄存器的上半部分要么包含0,或者是使用AArch32从异常级别获取异常的旧值:
- 高32位:UNKNOWN
- 低32位:已映射的AArch32寄存器的值
下面的部分将讨论过程中从正在执行的异常到完成异常返回的每个步骤。
4.1.1 Saving the current processor state
AArch64有一个被称为PSTATE的处理器状态的概念,正是这个信息存储在SPSR中。PSTATE包含诸如当前异常级别和算术逻辑单元(ALU)标志。在AArch64中,这包括:
- Condition flags (条件标志)
- Execution state controls(执行状态控制)
- Exception mask bits(异常掩码位)
- Access control bits(访问控制位)
- Timing control bits(定时控制位)
- Speculation control bits(推断控制位)
例如,PSTATE中的异常掩码位(DAIF)允许屏蔽异常事件。当设置了相关位时,相关异常不会发生。
- D:Debug异常屏蔽位
- A:SError异步异常屏蔽位,如异步外部abort
- I:IRQ异步异常屏蔽位
- D:IRQ异步异常屏蔽位
当发生异常时,必须保留当前状态,以便稍后可以返回到正确的状态。PE会自动保留异常返回地址和当前的PSTATE。如在发生异常PSTATE的值将存储在SPSR(Saved Program Status Register)中。
每个异常级别都有一个SPSR:SPSR_ELx。在发生异常时,使用的SPSR_ELx是发生异常到的异常级别。例如,如果异常带到EL1,则更新SPSR_EL1。
然后,PE将当前的PSTATE更新为体系结构中为该异常类型定义的PSTATE,以反映新的状态。这包括更新受影响的目标异常级别和安全级别。
一旦PSTATE被更新,PE就可以分支到向量表中的异常处理程序。执行从由异常类型定义的目标异常级别上的异常向量开始。为了从异常中返回,处理器可以将SPSR的内容恢复到PSTATE,并分支到ELR中指定的返回地址。如下图所示:
4.1.2 Routing and interrupt controllers
每个异常类型都有一个目标异常级别:
- 根据异常的类型进行相应处理
- 由系统寄存器中的配置位定义
异常的目标由架构固定,或由使用路由控制的软件进行配置。然而,异常永远不能被带到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更改为等于或低于路由到的异常级别。这符合你永远不能因为采取例外而失去特权的规则。
接受异常的异常级别的执行状态由更高的异常级别决定。假设所有异常级别都已实现,下表显示了如何确定执行状态:
4.1.3 AArch64 vector tables
当使用AArch64对异常进行处理时,向量表是正常内存中的一个区域,其中包含随后用于处理异常的指令。
当发生异常时,core需要能够执行与该异常对应的处理程序。处理程序充当调度代码,识别异常的原因,然后调用相关的处理程序代码来处理异常。在内存中存储处理程序的位置称为异常向量。在AArch64中,异常向量存储在异常向量表中。
每个异常级别都有它自己的向量表,其基地址由它自己的向量基地址寄存器VBAR_EL定义,其中为1、2或3。请注意,没有针对EL0的专用向量表,因为异常永远不会被带到EL0。
所有的向量表都使用相同的格式。根据异常的类型和异常的来源,有不同的异常类别入口。每个异常类别在距离向量基地址的固定偏移量处都有一个异常向量。
所使用的入口取决于以下因素:
- 异常的类型(SError、FIQ、IRQ、同步)
- 异常taken from和taken to获取的异常级别
- 需要得到支持的执行状态
- 正在使用的堆栈指针(参见Stack pointer selection and stack pointer registers部分)
VBAR重置后寄存器的值未定义,因此必须在启用中断之前进行配置。
以下描述了向量表中来自向量基地址的固定偏移量:
这将被特权软件记录到系统寄存器中,以便当发生异常时,core可以找到相应的处理程序。这些向量被分为两组,每一组被分为两个子组:
- Exception from Lower EL
- Lower EL using AArch32
- Lower EL using AArch64
- Exception from the current EL
- Exception when SP_ELx selected
- Exception when SP_EL0 selected
因此,异常到达的哪个向量已经提供了关于异常时处理器状态的信息。我们将在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返回的指令。下图展现了简单异常处理程序示例:
4.1.4 Stack pointer selection and stack pointer registers
当在AArch64中执行时,该体系结构允许选择两个堆栈指针寄存器: SP_EL0或SP_EL,其中是当前的异常级别。例如,在EL1中,可以选择SP_EL0或SP_EL1。这在AArch64 vector tables一节中提供的向量表示例中可见。
当发生异常时,将自动选择目标异常级别SP_ELx的堆栈指针。PE所选择的堆栈指针使用PSTATE按照以下规则进行配置:
- 如果在EL0处执行,则PE将使用EL0堆栈指针SP_EL0
- 如果在EL1、EL2或EL3处执行,则PE将使用由PSTATE.SP位确定的堆栈指针:
- 如果是PSTATE.SP = 0,然后PE使用EL0堆栈指针SP_EL0
- 如果是PSTATE.SP = 1,则PE使用当前异常级别的堆栈指针ELx。
当我们在向量表上输入第一级处理程序时,我们正在使用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)”区域中的代码只会在异常情况下被触发。这个向量分组中的异步异常表明发生了重大的系统错误,需要处理,例如内核错误。
4.2 Returning from an exception
当异常处理程序处理异常后,处理程序返回到异常发生之前运行的代码。
它通过执行以下操作来实现:
- 恢复所有以前压栈的可损坏的寄存器
- 启动异常返回指令(ERET)
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的内容。
4.3 Exception handling examples
本节从端到端(end-to-end)的角度提供了同步和异步异常处理的简要示例。
4.3.1 Synchronous exception handling
如在Synchronous exceptions一节中所讨论的,如果一个异常是通过直接执行指令生成的,并且返回地址表示导致该异常的指令,则可以认为该异常是同步的。以下寄存器提供了处理异步异常(SErrors)的关键信息:
- 异常综合寄存器(ESR_ELx)提供了有关同步异常的原因和类型的信息。
- 故障地址寄存器(FAR_ELx)保存与地址相关的同步异常的故障虚拟地址,如MMU错误。
- 异常链接寄存器(ELR_ELx)保存触发指令的地址,提供异常的返回地址。
下面是一个使用系统调用的AArch64异常模型的简单示例。
如果在较低的异常级别上运行的代码必须执行特权操作,则需要调用更高的异常级别。例如,运行在EL0处的AArch32应用程序代码可能需要从位于EL1处的AArch64 OS/内核请求堆内存分配。为此,它通过执行一个SVC指令来生成一个监控系统调用,从而触发以下内容: - 当前的PSTATE保存在SPSR_EL1寄存器中
- 异常返回首地址(以下指令)被写入ELR_EL1
- 异常综合信息(导致异常的原因)被写入ESR_EL1
- 通过读取系统管理程序配置寄存器(Hypervisor Configuration Register)HCR_EL2.RW位来确定目标执行状态
- 当前的PSTATE已更新:异常级别已更改为EL1,执行状态已更改为AArch64
- 核心分支指向VBAR_EL1指向的向量表,偏移量为0x600(VBAR_EL1 + 600),因为这是一个同步异常,其中异常来自较低的EL,所有较低的ELs都是AArch32
- 已定义的寄存器被压栈起来,以维护寄存器的上下文
- 异步异常的类型是从ESR_EL1中标识出来的,在本例中是SVC
- 然后将执行特定的SVC处理程序代码
- 一旦特定的SVC处理程序代码完成,控制将返回到高级处理程序
- 处理程序恢复所有以前压栈的寄存器并执行一个ERET指令
- PSTATE从SPSR_EL1恢复(包括返回到异常级别EL0和目标执行状态AArch32),程序计数器被更新到ELR_EL1中包含的值
注意,在这个示例中,AArch64处理程序代码可能需要访问AArch32寄存器。
一个涉及系统调用的更复杂的例子是,在EL0上运行在非安全(NS)状态下的AArch64应用程序代码需要调用在EL1上运行的一个安全的、受信任的操作系统。若要切换安全状态,您必须通过EL3。然而,如Exception-generating instructions
一节所述,EL0不能直接启动对EL3的SMC调用。在本例中,将从EL0.NS到EL1.NS启动一个SVC指令。然后,NS.EL1需要通过对EL3进行SMC调用来转换到S.EL1,从而处理安全状态的更改。详见: TrustZone for AArch64。
4.3.2 Asynchronous exception handling
异步异常,也称为中断,是PE和当前执行指令的外部。Arm架构不定义何时采用异步异常。因此,异步异常相对于其他同步异步异常的优先级是实现定义(IMPLEMENTATION DEFINED)的。
例如,如果PE在AArch32中的EL0处执行应用程序代码,并且发生IRQ中断。在本例中,HCR_EL2和SCR_EL3已被配置为能够将IRQ异常路由到AArch64中的EL1。具体总结如下图所示:
- 当前的PSTATE保存在SPSR_EL1寄存器中
- 异常返回首地址(第一个尚未完成的指令)写入ELR_EL1
- 通过读取系统管理程序配置寄存器(Hypervisor Configuration Register)HCR_EL2.RW位来确定目标执行状态
- 当前的PSTATE已更新,异常级别已更改为EL1,执行状态已更改为AArch64
- 核心分支到向量表VBAR_EL1,偏移到VBAR_EL1 + 0x680,因为这是一个IRQ异常,其中异常来自一个较低的EL,所有较低的ELs都是AArch32
- 已定义的寄存器被压栈起来,以维护寄存器的上下文
- 将执行特定的IRQ处理程序代码
- 一旦特定于IRQ的处理程序代码完成,控制将返回到高级处理程序
- 该处理程序将恢复所有寄存器并执行一条ERET指令
- PSTATE从SPSR_EL1恢复(包括返回到异常级别EL0和目标执行状态AArch32),程序计数器被更新到ELR_EL1中包含的值
注意,IRQ异常本身并不区分中断的中断原因(例如,计时器或UART)。假设使用了GIC,这可以通过读取GIC的中断确认寄存器(Interrupt Acknowledge Registers,IAR)来识别。读取返回中断的ID,并在GIC中将该中断标记为活跃(Active)。一旦中断被处理,就需要通过写入GIC的中断结束寄存器(End of Interrupt Register EOIR)将GIC状态清除回非活跃(Inactive)状态。
4.3.3 Exception masking and non-maskable interrupts (NMI)
前面的例子表示了一个非常简单的中断情况。有时需要能够禁用或屏蔽其他中断,以覆盖当前已完成的异常。物理和虚拟异步异常都可以暂时屏蔽,并保持挂起状态,直到打开屏蔽并发生异常。这是通过屏蔽相同类型的中断来完成的,直到以后显式地启用为止。
当处理器对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支持:
- SCTLR_ELx.NMI = 0: NMI支持已被禁用。可以使用PSTATE.I和PSTATE.F来屏蔽中断。
- SCTLR_ELx.NMI = 1:已使能NMI支持。
- SCTLR_ELx.SPINTMASK = 0 : NMI被PSTATE.ALLINT 屏蔽。
- SCTLR_ELx.SPINTMASK = 1 : NMI被PSTATE.ALLINT屏蔽或当目标异常级别使用SP_ELx时屏蔽。
NMI支持要求系统的中断控制器能够以超优先级呈现中断。如果使用Arm的通用中断控制器,则在GICv3.3和GICv4.2中添加了对NMIs的支持。
参考文献
Learn the architecture - AArch64 Exception
Model