AArch64异常模型

概述

AArch64 异常模型指南介绍了 Armv8‑A 和 Armv9‑A 中的异常和特权模型。它涵盖了 Arm 架构中不同类型的异常,以及与异常相关的处理器行为。

这些内容适用于底层代码(例如boot code或kernel)开发人员,对于配置或者管理异常的开发人员来说尤其重要。

文章的末尾你可以检查你学的知识。你应该能列出异常等级有哪些,状态如何切换,以及命名和描述异常执行状态。你应该能详细介绍一种简单的AArch64 向量表,以及如何使用异常处理程序。

特权等级和异常等级

在详细解释AArch64异常模型之前,我们需要先介绍一下特权的概念。

现代软件划为分为不同的模块,每个模块对系统和处理器资源具有不同的访问级别。例如,操作系统内核和用户应用程序之间的划分。操作系统可以执行我们不希望用户应用程序能够执行的操作。内核需要对系统资源进行更高级别的访问,而用户应用程序配置系统的能力则需要加以限制。特权就是规定软件实体可以查看和控制哪些处理器资源。

AArch64 架构实现了不同的特权等级。仅当处理器发生异常或从异常返回时,当前特权等级才能更改。因此,这些特权级别在 Arm 架构中被称为异常等级。

异常等级

在AArch64中,特权的名称是异常等级,通常缩写为EL。异常等级按照数字编号,通常缩写为EL<x>,其中<x>取值范围0~3。数字越大,特权级别越高。例如,最低权限级别是 EL0。

如下图所示,有四种异常等级:EL0、EL1、EL2和EL3

请添加图片描述

该架构没有指定什么软件应该使用哪个异常等级。常见的使用模型是在 EL0 上运行应用程序代码,并在 EL1 上运行操作系统 (OS),例如 Linux。 EL2 可由虚拟化使用,EL3 可由固件和安全监视器代码使用。 例如,Linux 可以调用EL3等级下的固件函数,这些函数使用软件接口标准,从较低级别的细节中抽象出用于打开或关闭内核的意图。此模型意味着大部分 PE 处理通常发生在 EL0/EL1上。

异常等级只能在以下情况发生时更改:

  • 产生异常
  • 从异常中返回
  • 处理器复位
  • 调试期间
  • 退出调试

**当发生异常时,异常等级可以增加或保持不变,但无法通过发生异常来降低异常等级。从异常返回时,异常等级可能会降低或保持不变,但无法通过从异常返回来增加权限级别。**我们将在后文进一步讨论这一点。

特权类型

有两种与AArch64异常模型相关的特权:

  • 内存系统中的特权
  • 从访问处理器资源的角度来看的特权

两种类型的特权都受到当前特权异常等级的影响。

内存特权

Arm架构的 A 系列实现了虚拟内存系统,其中内存管理单元(MMU)允许软件分配内存区域的属性。这些属性包括读/写权限,可以单独配置为特权访问和非特权访问。

当处理器在 EL0 中执行时,内存访问将根据非特权访问权限进行检查。来自 EL1、EL2 和 EL3 的内存访问将根据特权访问权限进行检查。

由于内存配置是软件通过编程MMU翻译表实现的,你应当考虑配置这些表的权限。MMU配置是存储在系统寄存器中,访问这些寄存器的能力也由当前异常等级控制。

寄存器访问

AArch64 处理器的配置设置保存在一系列系统寄存器中。系统寄存器中的设置组合定义了当前处理器上下文。对系统寄存器的访问由当前异常等级控制。

例如,VBAR_EL1是向量基地址寄存器,我们将在后文介绍它的用途。目前最重要的是”_EL1“后缀,其告诉我们软件至少需要在EL1等级下才能访问这些寄存器。

有许多功能相似的寄存器,仅在异常等级后缀上面有所不同。它们是独立的、单独的寄存器,在指令集中有自己的编码,并在硬件中单独实现。

每个异常等级都有一个系统控制寄存器 (SCTLR)。其控制该 EL 下的架构特性,例如 MMU、缓存和对⻬检查:

  • SCTR_EL1:EL0和EL1的顶层系统控制
  • SCTR_EL2:EL2的顶层系统控制
  • SCTR_EL3:EL3的顶层系统控制

较高的异常等级有权访问控制较低等级的寄存器。例如,EL2有权访问SCTLR_EL1。但无法从 EL0 访问该寄存器,如果尝试这样做都会生成异常。

通常情况下,特权异常等级只控制自己的配置。然而,更高的特权等级有时会访问与较低异常级别相关的寄存器。例如,为了实现虚拟化功能和上下文切换。

本文中讨论与AArch64异常模型相关的系统寄存器,如下:

寄存器名称描述
异常链接寄存器ELR_ELx存放异常返回的地址
异常综合信息寄存器ESR_ELx引起异常原因的信息
故障地址寄存器FAR_ELx存放虚拟故障地址
虚拟配置寄存器HCR_ELx控制虚拟化设置和EL2异常捕获
安全配置寄存器SCR_ELx控制EL3的安全状态和异常捕获
系统控制寄存器SCTR_ELx控制内存,系统配置,并提供已实施的状态信息
备份程序状态寄存器SPSR_ELx当发送ELx异常时,保存处理器状态
向量表基地址寄存器VBAR_ELx保存ELx的异常向量表基地址

执行状态和安全状态

AArch64架构提供了四个异常等级,还有两个执行状态以及最多四个安全状态。Armv8‑A 或 Armv9‑A 处理器的当前状态由异常等级和当前执行状态决定。

执行状态

当前执行状态定义了通用寄存器的标准宽度和可用指令集。执行状态还会影响内存模型的各个方面以及异常的管理方式。

Armv8‑A 和 Armv9‑A 支持两种执行状态:

  • AArch32:AArch32 是 32 位执行状态。此状态下的操作兼容以前的架构。它支持T32和A32指令集,标准寄存器宽度为32位。
  • AArch64:AArch64 是 64 位执行状态。它支持A64指令集,标准寄存器宽度为 64 位。

本文的后续章节将更深入地介绍在 AArch64 中如何处理异常。

切换执行状态

处理定义 (PE) 只能在复位或异常等级更改时进行执行状态切换。当 PE 在异常等级变更时,可以切换执行状态,但是仅允许在 AArch32 和 AArch64 之间转换,且须遵守以下规则:

  • 当从较低的异常等级切换到较高的等级时,执行状态可以保持不变或更改为AArch64。
  • 当从较高的异常等级切换到较低的等级时,执行状态可以保持不变或更改为AArch32。

将这两个规则放在一起意味着 64 位层可以兼容 32 位层,但反之则不然。例如,64 位操作系统内核可以运行 64 位和 32位应用程序,而 32 位操作系统内核只能运行32 位应用程序。

下图说明了这些场景:

请添加图片描述

在上面例子中,我们使用了操作系统和应用程序,但相同的规则适用于所有异常等级。例如, EL2 上的 32 位虚拟机程序只能在32 位操作系统的 EL1 上运行。

Armv8‑A 架构所有异常等级均支持 AArch32 和 AArch64 两种执行状态。至于每个异常等级支持一种还是两种执行状态是可选的。

对于 Armv8‑A 处理器,复位时的执行状态由实施定义的机制确定。例如,Cortex‑A32 复位时始终为 AArch32 状态。

对于 Armv9‑A 处理器,所有 EL 都需要支持 AArch64。 EL0 是否支持 AArch32 是可选的。其他异常等级均为AArch64,并且复位时执行状态始终为 AArch64。

每个异常等级的执行状态由下一个更高异常等级的控制寄存器定义。这需要利用 EL2 的虚拟配置寄存器 (HCR_EL2)和 EL3 的安全配置寄存器 (SCR_EL3)。复位时,如果可切换,EL3 的执行状态由外部引脚设置。有关路由和中断控制器的部分将在后续介绍。

安全状态

AArch64允许实现多种安全状态。这允许进一步划分软件,用来隔离和区分软件。

大多数Cortex-A处理器支持两种安全状态:

  • 安全状态:在此状态下,处理单元(PE)可以访问安全和非安全物理地址,以及影子寄存器的安全副本。
  • 非安全状态:这通常称为正常世界。在这种状态下,PE只能访问非安全物理地址空间,PE 只允许访问非安全的系统寄存器。

安全态定义了可以访问哪些异常等级,可以访问哪些内存区域,以及在系统内存总线上如何表示这些访问。如果处理非安全态,PE只能访问Non-secure物理地址空间。而在安全态下,PE可以同时访问Secure和Non-secure物理地址空间。

例如,操作系统(例如Android)运行在正常世界中,而支付或者DRM运行在安全世界中。我们对安全世界系统有着更高程度的信任,并且需要将它们隔离开,以保护支付信息和密钥等。拥有两种安全状态就提供了这种隔离机制。

下图展示了不同执行状态下的异常等级和安全状态。

请添加图片描述

在指南TrustZone for AArch64中更详细地描述了这些安全状态的使用。

改变安全状态

如果实施了TrustZone,处理器要么处于安全态,要么处于非安全态。这是由SCR_EL3.NS位进行控制选择。

你可能已经注意到上图中EL3处于安全状态。EL3是最高的异常等级,其固定为安全状态。EL3能够访问所有的影子寄存器副本。

**在Armv8-A中,EL3始终处于Secure态。在Armv9-A中,除非实现了RME,否则EL3是安全状态的一部分。如果实施了RME,Root状态与EL3下的其他安全状态隔离开。**下一节将详细地介绍RME。

如果你想从一种安全状态切换到另一种安全状态时,必须通过EL3。EL3下的软件负责管理对不同安全状态的访问,这类似于一名看门人,控制对EL2、EL1、EL0安全状态的访问。SCR_EL3.NS 位允许在从 EL3 返回时更改安全状态。

关于安全状态切换,更加详细地介绍参考TrustZone for AArch64Realm Management Extension

Realm Management Extension

Armv9-A引入了对Realm Management Extension(RME)的支持。如果实施了RME,就可以支持两种额外的安全状态:

  • Realm state:在此状态下,PE可以访问非安全和Realm物理地址空间。
  • Root state:在此状态下,PE可以访问所有的物理地址空间。Root state仅在EL3下可用。

RME将EL3与其他安全状态隔离开。通过RME,EL3脱离安全状态并进入自己的安全状态(称为根)。EL3支撑平台和初始化启动代码,因此安全状态、非安全状态以及Realm状态下的软件均信任EL3。

下图展示了PE启用RME时的安全状态以及这些安全状态如何映射到异常等级。

请添加图片描述

更多关于RME的信息,请参考Realm Management Extension

实施异常等级的影响

对于任何处理器,是否实现所有异常等级以及每个异常等级允许哪些执行状态都是可选的。

EL0 和 EL1 是唯一必须实现且强制执行的异常等级。 EL2和EL3是可选的。

请注意,如果尚未实施 EL3 或 EL2,则应考虑以下几点:

  • EL2 包含许多虚拟化功能。没有实现 EL2 无权访问这些功能。更多关于虚拟化的信息,请参考AArch64 virtualization
  • EL3是唯一可以更改安全状态的等级。如果不实施EL3,PE只能访问一种安全状态。你所处的状态时已固定,无法改变。

有许多软件实现要求平台支持这些异常等级。例如,KVM Linux需要支持EL2、EL1和EL0。

Armv8‑A 处理器可以选择在每个异常等级下哪个执行状态是有效的,这是由具体实现定义的。 如果在异常等级允许AArch32执行,则必须在所有较低的异常等级也同样允许执行。例如,如果 EL3 允许 AArch32执行,则必须在所有较低的异常等级上也允许AArch32执行。

许多实现支持所有执行状态和所有异常等级,但有些实现也存在一些限制,例如:

  • Cortex-A35支持所有异常等级下的AArch32和AArch64执行状态
  • Cortex-A32只支持AArch32
  • Neoverse-N2支持EL0下的AArch32。其他等级只支持AArch64

AArch32 提供与旧 32 位架构的向后兼容性,并且从 Armv9‑A 开始仅限于在 EL0 上使用。 Armv9‑A 在所有异常等级支持 AArch64,并且仅可在 EL0 下支持 AArch32。这意味着以前的应用程序可以运行,但不能运行内核、虚拟机管理程序或固件。

你可以参考处理器的技术参考手册(TRM),查看支持哪些异常等级。

异常类型

异常是特权软件需要更新系统状态或者采取补救措施的情况或者系统事件,用来确保系统平稳运行。因此,异常会导致当前正在执行的程序挂起。发生异常会导致状态变化,并处理执行异常代码。

异常处理

其他处理器架构可能将此描述为中断。在AArch64中,中断是一种特殊外部触发的异常。

使用异常有很多原因,如下:

  • 模拟虚拟设备
  • 虚拟内存管理
  • 处理软件错误
  • 执行硬件错误
  • 调试
  • 执行不同特权或者安全状态的调用
  • 处理中断(定时器,设备交互)
  • 不同执行状态的处理(交互处理)

当异常发生时,处理器不会执行当前下一条指令,而是切换到另外一段代码进行处理。这段代码称为异常处理程序,一旦处理完毕,异常会返回原来的程序继续执行。每个异常都有自己的异常处理程序。异常处理流程如上图所示。

Arm架构将异常分为两大类:同步异常和异步异常。

同步异常

同步异常是由当前正在执行的指令引起或者与之相关的异常。同步异常与执行流同步,因为它们与当前执行的指令直接相关。例如,尝试写入MMU定义的只读区域的指令将触发同步异常。

如果满足以下所有条件,则异常是同步的:

  • 异常是由于直接或者尝试执行某个指令引起的。
  • 处理异常后要返回的地址(返回地址)与引发异常的指令具有架构定义的关系。
  • 异常是精确的,这意味着在进入异常处理程序时,寄存器状态与引发异常指令之前的每条指令执行结果都保持一致,而引发异常指令之后的指令则不会影响寄存器状态。

同步异常有许多不同类型,并且一条指令可能引起多个同步异常。Arm架构为同步异常提供固定的优先级。

下面将更深入地讨论引起同步异常的原因:

  • 非法指令和陷阱异常
  • 内存访问
  • 异常产生指令
  • 调试异常

非法指令和陷阱异常

尝试执行非法指令会引起同步异常。非法指令的原因有很多,包括未定义的指令、当前异常等级不允许的指令或已被禁用的指令。任何尝试执行内核无法识别的指令都会生成UNDEFINED异常。

该架构还允许控制实体(例如操作系统或虚拟机管理程序)设置陷阱以拦截较低异常等级的操作。当执行给定操作(例如读取特定寄存器)时,陷阱会触发异常。

例如,处于EL1 下的操作系统内核可能会禁用 EL0 下的浮点指令,用来节省应用程序之间上下文切换的时间,这称为惰性上下文切换。例如,如果在上下文切换之前未使用 SIMD 或浮点 (FP) 单元,则可以减少压入堆栈的寄存器数量。可以使用陷阱异常来处理边缘情况。

在这种情况下,OS内核可以通过禁用SIMD/FP单元来监视SIMD/FP操作的状态。当执行 FP 或 SIMD 指令时,会向 EL1 下的操作系统内核发出陷阱异常。然后,内核可以启用 SIMD/FP 单元,并执行失败的指令,同时设置一个标志来表明 SIMD/FP 单元已使用。这确保了在下一次上下文切换时,大量SIMD/FP 寄存器包含在寄存器上下文中。如果在下一次上下文切换时没有使能该标志,则不需要包含SIMD/FP 寄存器。

陷阱异常对于虚拟化尤为重要,有关虚拟化使用异常的更多信息,请参考AArch64 virtualization guide

内存访问

同步异常也可能是由内存访问引起的,这是由于 MMU 执行检查或内存系统返回的错误造成的。

例如,当 MMU 启用时,将检查由加载和存储指令发起的对存储器的访问。如果您尝试从非特权代码访问特权地址,或尝试写入只读地址,则 MMU 会阻⽌访问内存管理单元 (MMU)并触发错误。由于 MMU 生成的错误是同步的,因此异常会发生在继续访问内存之前。

AArch64 memory management guide中更详细地讨论了内存访问错误。内存访问还可能导致异步异常,本指南后面的SError部分将对此进行介绍。

异常产生指令

有些指令会故意生成和处理异常,这些指令用来实现系统调用接口。这些接口允许较低异常等级的软件向权限较高的程序请求服务。这些有时称为系统调用,通常用于基于软件的API。

Arm架构包括三种系统调用SVC、HVC和SMC。这些指令只是让PE在不同异常等级之间切换。

  • SVC指令允许EL0下的用户应用程序请求EL1下的操作系统的服务。
  • HVC指令允许操作系统OS请求EL2下的虚拟机监控器的服务。
  • SMC指令允许普通世界中的程序请求EL3下的安全监控器的服务。

当 PE 在 EL0 上执行时,它无法直接调用 EL2 上的虚拟机管理程序或 EL3 上的安全监控器,因为这些只能在 EL1 及更高等级上实现。EL0 上的应用程序必须对内核使用SVC系统调用,并让内核执行调用更高异常等级的操作。

假设各个异常等级均已实现,操作系统内核(EL1)可以执行HVC指令来调用EL2处的管理程序或使用SMC指令调用EL3处的安全监控器。类似地,从 EL2 开始,PE 可以使用SMC指令来调用 EL3 安全监控器。

服务调用路由

由于异常不能返回到低的异常等级ELs,因此在EL2上进行的SVC调用不能返回到LE1。

调试异常

调试异常是路由到启用调试器异常等级的同步异常。调试器代码的执行方式与异常处理程序代码非常类似。

有许多同步的调试异常,如下:

  • 断点指令异常
  • 断点异常
  • 观察点异常
  • 向量捕获异常
  • 软件单步异常

有关调试异常及其处理方式的更多信息,请参阅AArch64 self-hosted debug guide

异步异常

某些异常是外部生成的,因此与指令流不同步。

异步异常不直接与当前执行的指令相关,并且通常是来自处理器外部的系统事件。这可能是软件需要响应的系统事件,例如定时器事件或屏幕触摸事件。我们不知道它们在什么时候会发生。

根据定义,如果异常是不同步的,它就是异步的。异步异常也称为中断。

在发生异步异常时,程序流会被中断,并使用专门的代码来处理此外部请求。无法保证异步异常何时发生,AArch64架构仅要求其在有限时间内处理完成。

物理中断

物理中断是响应来自 PE 外部(通常由外设)的信号而生成的中断。系统不是去轮询外部信号,而是通过生成中断来通知内核发生某些异常。

例如, 系统可能使用通用异步接收器/发送器 (UART) 接口与外界通信。当UART接收数据时,它需要一种机制来告诉处理器新数据已经到达并进行处理。 UART 可以生成中断向处理器发出信号。

复杂的系统可以有许多具有不同优先级的中断源,包括中断嵌套的处理能力,其中较高优先级的中断可以打断较低优先级的中断。内核响应此类事件的速度可能是系统设计中的一个关键问题,这称为中断延迟。

接下来我们将讨论不同类型的物理中断。

SError

系统错误 (SError) 是内存系统为响应意外事件而生成的异常类型。我们无法预计这些事件的发生,但需要知道它们是否发生了。这些通知是异步的,因为触发这些事件的指令可能已经停用了。

典型的SError是之前提到的外部异步中止。SError中断包括:

  • 已通过MMU检查,但随后遇到内存总线访问错误。
  • 对某些RAM(例如内置cache)进行奇偶校验或者纠错码(ECC)检查
  • 将脏数据从高速缓存写回到外部存储器而触发的中⽌

SError 被视为一个单独的异步异常,因为您通常会对这些异常有单独的处理程序。SError 的生成是由实现定义的。

IRQ和FIQ

Arm 架构有两种异步异常类型:IRQ 和 FIQ,旨在用于支持外设中断的处理。它们用于发出外部事件信号,例如计时器关闭,但并不表示系统错误。它们是与处理器指令流异步的预期事件。

IRQ和FIQ具有独立的路由控制,通常用于实现安全和非安全中断,如Arm Generic Interrupt Controller v3 and v4 guide中 所述。如何使用这两种异常类型是由实现定义的。

中断控制器几乎都需要与系统中的 AArch64 处理器配对,用于收集、确定优先级和处理所有中断。所有 Arm 的实现都使用 Arm 通用中断控制器 (GIC) 架构来管理 IRQ 和 FIQ。 GIC 执行中断管理、优先级划分和路由任务,为每个物理中断类型提供单独信号到内核中。有关 GIC 架构的更多信息,请参阅Arm Generic Interrupt Controller v3 and v4 guide

虚拟中断

使用虚拟化的系统对中断处理有更复杂的需求。一些中断可以由管理程序处理,一些中断可以在虚拟机内处理。 VM看到的中断是虚拟中断。虚拟中断可以由连接到中断控制器的外部设备生成,也可以由软件产生。因此需要额外的机制来支持,在AArch64中明确支持虚拟中断。

  • vSError:虚拟系统中断
  • vIRQ:虚拟IRQ
  • vFIQ:虚拟FIQ

虚拟中断按中断类型进行控制。这些虚拟中断的功能与物理中断相同,但它们只能向 EL1 发送信号。

虚拟中断可以从 EL2 上的管理程序生成,也可以通过使用中断控制器生成。虚拟机管理程序必须在虚拟机管理程序配置寄存器(HCR_EL2) 中设置相应的路由位。例如,要启用 vIRQ 信号,虚拟机管理程序必须设置HCR_EL2.IMO。 这些设置将物理 IRQ 异常路由到 EL2,并启用向 EL1 发送虚拟异常信号。

HCR_EL2 中有 3 个位控制虚拟中断的生成:

  • VSE:使能会注册一个vSError
  • VI:使能会注册一个vIRQ
  • VF:使能会注册一个vFIQ

设置这些位相当于中断控制器向 vCPU 发出中断信号。此方法的含义是管理程序需要模拟 VM 中中断控制器的操作。当频繁操作时,这可能会导致大量开销,因此建议使用中断控制器。

GICv2 及更高版本通过提供物理 CPU 接口和虚拟 CPU 接口来支持物理和虚拟中断的信号发送。有关中断控制器的更多信息,请参阅Arm Generic Interrupt Controller v3 and v4 guide。 Armv8.4‑A 中添加了对 Secure 状态虚拟化的支持,并且需要启用并支持 Secure EL2。

AArch64 virtualization guide进一步讨论了虚拟中断。

Masking

物理和虚拟的异步异常可以暂时被屏蔽。这意味着异步异常在取消屏蔽并处理之前,可以一直保持在挂起状态,这对于异常(中断)嵌套特别有用。
同步异常是不能被屏蔽的,因为这些异常直接是由指令执行引起的,如果没有处理或者忽略这些异常,这会阻止其他指令继续执行。

在2021年的扩展中,Armv8.8‑A 和 Armv9.3‑A 添加了对不可屏蔽中断 (NMI) 的支持。 当启用NMI时,处理器将得到超高优先级的中断。当没有超高优先级的中断被屏蔽时,这种机制允许发生NMI中断。

本指南后面会进一步介绍中断屏蔽,更深入地讨论了中断屏蔽和NMI功能。

异常处理

正如我们在前面异常类型介绍的那样,当异常发生时,当前的程序会中断执行。本章更深入地讨论在实践中如何处理异常。

当讨论异常时,需要使用到以下AArch64术语:

  • 当PE响应异常时,表明异常被获取
  • 发生异常的状态是指发生异常之前的PE状态
  • 异常发生后的状态是指发生异常后的PE状态

因此,当处理器识别到异常时所处的状态被称为异常发生前的状态。 PE 在异常发生后所处的状态是异常发生后的状态。例如,异常发生状态可能从 AArch32 EL0 到 AArch64 EL1。

当处理完异常后,系统需要返回到之前的状态,这称为异常返回。Arm架构有相应的指令触发异常返回:

  • exception returns from:执行异常返回前的状态
  • exception returns to:执行异常返回的状态

异常发生

当异常发生时,处理器将当前状态和异常返回地址一起保存,随后进入特定模式处理异常。

当前状态的快照是从PSTATE寄存器中获取的,这在后续章节进一步讨论。这份快照是存储到保存程序状态寄存器SPSR,返回地址存储到异常链接寄存器ELR。对于同步异常和SError,异常综合信息寄存器ESR也会更新,其记录了异常发生的原因。

当AArch64状态下,某一异常等级ELx发生异常时,将会发生以下情况:

  • 发生异常之前的PSTATE寄存器值会保存到SPSR_ELx。
  • 异常返回地址会保存到ELR_ELx。

此外:

  • 对于同步异常和SError中断,异常综合信息(异常原因)会存储到ESR_ELx。
  • 对于与地址相关的同步异常,例如MMU错误,触发异常的虚拟地址将存储到故障地址寄存器FAR_ELx。

异常相关的处理指令通常存储在内存中,这个存储位置称为异常向量。当发送异常时,PE会切换到异常向量表的某个位置。

异常向量表通常包含执行通用操作的处理代码以及根据异常类型切换到异常处理的代码,如下图所示。异常代码限制在32字节。异常处理程序包括处理请求操作的代码,以及从异常状态返回。

顶级处理程序

每个异常类型都针对某一个异常等级EL,因此异常会路由到不同的EL。这尤其重要,因为这是提升特权的唯一方法。而降低特权的唯一方法就是执行异常返回。

这意味着:

  • 异常发生时,EL可以保持不变或者增加
  • 异常返回时,EL可以保持不变或者降低

值得注意的是,发生异常或执行异常返回并不一定需要更改异常等级 EL。异常的目标EL可能与当前 EL 相同。目标 EL 可以根据异常类型隐式定义,也可以通过系统寄存器中的配置位来定义。

正如前文讨论执行状态和安全状态那一章,PE只能在复位、异常发生和异常返回时改变执行状态。AArch32 和 AArch64 执行状态之间的交互称为交互处理,请务必记住:

  • 当从较低的异常等级切换到较高的等级时,执行状态可以保持不变或更改为AArch64。
  • 当从较高的异常级别切换到较低的等级时,执行状态可以保持不变或更改为AArch32。

Arm架构规定如下:

  • 如果某一异常等级使用 AArch32,则所有较低的异常等级都必须使用 AArch32。
  • 如果某一异常等级使用 AArch64,则所有较高的异常等级都必须使用 AArch64。

在 Armv9‑A 架构和某些 v8‑A 实现中,仅 EL0 支持 AArch32,并且EL0不能处理异常。这意味着要更改 EL0 下的执行状态就需要切换到特权等级更高的 EL,然后再进行返回。

由于 AArch32 和 AArch64 均可能发生异常,因此 AArch64 处理程序可能需要访问 AArch32寄存器。 AArch32 通用寄存器直接映射到 AArch64 寄存器,以允许处理程序代码访问 AArch32 寄存器:

AArch32AArch64
R0-R12X0-X12
Banked SP 和 LRX13-X23
Banked FIQX24-X30

当从AArch32切换到AArch64时,AArch32状态下的不可访问的寄存器将保留执行AArch64之前的值。对于在两种执行状态下均可访问的寄存器,64 位寄存器的上半部分要么是全0要么是 AArch32 状态下发生异常时候的旧值。

  • 高32位:未知
  • 低32位:映射的AArch32寄存器值

接下来讨论从异常发生到异常返回过程中的步骤。

保存当前处理器状态

AArch64使用PSTATE寄存器表示处理器状态,这些信息存储在备份程序状态寄存器SPSR中。PSTATE包括类似当前异常等级和算术逻辑单元ALU标志等信息。对于AArch64,如下:

  • 条件标志
  • 执行状态控制
  • 异常屏蔽位
  • 访问控制位
  • 时序控制位
  • 推测控制位

例如,PSTATE中的异常屏蔽位DAIF允许屏蔽异常事件。当这些位被置位时,异常就不会发生。

  • D:调试异常位
  • A:SError异步异常屏蔽位,例如异常外部中止
  • I:IRQ异步异常屏蔽位
  • F:FIQ异步异常屏蔽位

当发生异常时,必须保留当前状态,以便稍后可以返回到正确的状态。PE自动保存异常返回地址和当前PSTATE,并将PSTATE存储在SPSR寄存器中。

每个异常等级都有一个SPSR,即SPSR_ELx。当发生异常时,SPSR_ELx用于目标的异常等级(异常处理),例如如果向EL1发生异常(taken to),则SPSR_EL1会被更新。

然后,PE将更新当前的PSTATE,反映新的状态,包括目标异常等级和安全等级。

一旦 PSTATE 被更新,PE 就可以切换到向量表中的异常处理程序,从目标异常等级的异常向量开始执行。为了从异常中返回,处理器需要将 SPSR 的内容恢复到 PSTATE 并切换到 ELR 中指定的返回地址。

保存和恢复处理器状态

路由和中断控制器

每个异常类型都有一个目标异常等级,如下:

  • 根据异常类型隐示定义
  • 由系统寄存器中的配置位定义

异常的目标要么由体系架构固定,要么由软件使用路由控制进行配置。然而,EL0 不会发生异常处理。

同步异常根据异常生成指令SVC,HVC,SMC相关的规则进行路由。实施后,其他类型的异常可以路由到LE2或者EL3。IRQ、FIQ和SError的路由是单独设置的,例如,可以将所有IRQ路由到EL1下,如下图所示。

IRQ路由示例

路由配置是通过安全配置寄存器SCR_EL3和虚拟配置寄存器HCR_EL2实现的。SCR_EL3 寄存器指定哪些异常被路由到 EL3,而HCR_EL2 寄存器类似地指定哪些异常被路由到 EL2。

这允许将不同的中断类型路由到不同的异常等级。例如,IRQ 可能由 EL1 上的操作系统处理,而 SErrors 通常由 EL3 上运行的固件处理。

每个控制寄存器中有单独的位配置IRQ、FIQ和SError中断。此外当路由配置存在冲突时,SCR_EL3配置的路由会覆盖HCR_EL2的。复位后的路由位值未知,因此需要软件初始化。

通常使用Arm通用中断控制器GIC来配置中断管理、优先级和路由任务,这可以减少与虚拟化相关的开销。

前面在中断屏蔽部分提到过,可以暂时屏蔽异步异常并使其处于挂起状态,直到异常被取消屏蔽并处理。路由也会影响屏蔽,因为屏蔽的能力取决于当前和目标异常等级。

路由到较高异常等级的异常无法被较低 EL 屏蔽。例如,如果中断在 EL1 中被屏蔽,并且中断被路由到 EL2,则 EL1屏蔽将不会影响 EL2 操作。但请注意,当 PE 执行最后一次从 EL2 退出时,EL2 中断可能已被屏蔽,这仍然可能导致中断在进入 EL2 时被屏蔽。

路由到当前异常等级的异常可以被当前等级屏蔽。路由到较低异常等级的异常始终被屏蔽,这种异常将被挂起,直到PE 切换到等于或低于路由到的异常等级。这符合这样的规则:你永远不会因为异常而失去特权。

异常等级变化屏蔽的影响
目标等级EL > 当前等级EL不能被CurrentEL屏蔽
目标等级EL < 当前等级EL隐式屏蔽
目标等级EL == 当前等级EL由PSTATE屏蔽位设置

异常发生时,异常等级的执行状态是由更高异常等级决定的。假设所有异常等级均已实现,下表展示了如何确定执行状态。

异常发生(taken to)的等级执行状态由什么决定
EL1HCR_EL2.RW
EL2SCR_EL3.RW
EL3EL3的复位状态

AArch64向量表

当AArch64处理异常时,向量表位于普通内存区域,其包含用于后续处理异常的指令。

当异常发生时,内核需要能够执行该异常对应的处理程序。处理程序充当调度代码,识别异常的原因,并调用相关的handler来处理异常。内存中存储handler的区域称为异常向量。在AArch64中,异常向量存储在异常向量表中。

每个异常等级都有自己的异常向量表,基地址保存在自己的向量基地址寄存器VBAR_EL<x>,其中<x>取值1,2,3。请注意EL0没有异常向量表,因为其不会处理异常。

所有的异常向量表都使用相同的格式。根据异常类型和来源,有不同的异常类别。每个异常类别均有一个异常向量,其位于向量表的固定偏移地址处。

每个异常类别取决于以下因素:

  • 异常类型(SError,FIQ,IRQ,同步)
  • 异常等级
  • 支持的异常状态
  • 正在使用的堆栈指针

下表描述了向量表中固定偏移地址处的异常向量。

地址异常类型描述
VBAR_ELx + 0x780SError/VSError异常来自低异常等级,并且所有低异常等级是AArch32状态
VBAR_ELx + 0x700FIQ/vFIQ
VBAR_ELx + 0x680IRQ/vIRQ
VBAR_ELx + 0x600Synchronous
VBAR_ELx + 0x580SError/VSError异常来自低异常等级,并且至少一个低异常等级是AArch64状态
VBAR_ELx + 0x500FIQ/vFIQ
VBAR_ELx + 0x480IRQ/vIRQ
VBAR_ELx + 0x400Synchronous
VBAR_ELx + 0x380SError/VSError使用SP_ELx,异常来自当前异常等级
VBAR_ELx + 0x300FIQ/vFIQ
VBAR_ELx + 0x280IRQ/vIRQ
VBAR_ELx + 0x200Synchronous
VBAR_ELx + 0x180SError/VSError使用SP_EL0,异常来自当前异常等级
VBAR_ELx + 0x100FIQ/vFIQ
VBAR_ELx + 0x080IRQ/vIRQ
VBAR_ELx + 0x000Synchronous

上述是由特权软件记录到系统寄存器中,这样当异常发生时,内核可以找到相应的处理程序。异常分为两组,每组又划分为两个子项:

  • 异常来自低EL
    • 在AArch32状态下的低EL
    • 在AArch64状态下的低EL
  • 异常来自当前EL
    • 当选择SP_ELx时的异常
    • 当选择SP_EL0时的异常

因此,异常触发的向量已经提供了有关异常发生时的处理器状态信息。我们将在后续堆栈指针选择和堆栈指针寄存器章节介绍堆栈选择的重要性。请注意,上报的执行状态信息是来自于紧邻异常等级EL,并不一定是发生异常的EL。

例如,如果从异常等级 EL0 到 EL1发生异常,向量表选择会基于EL1的执行状态。然而,如果从异常等级 EL0 到 EL2 发生异常,则使用的异常向量取决于 EL1,而不是EL0。这是因为我们通常期望虚拟机管理程序或其他虚拟化管理软件运行在 EL2 的虚拟机VM上,这个虚拟机VM运行在 EL0 和 EL1 上。虚拟机管理程序需要了解虚拟机内操作系统的执行状态,而不是操作系统托管的应用程序。有一些特殊情况,例如当 HCR_EL2.TGE 设置为 1 时,这些在AArch64 virtualization guide有更加详细的介绍。

一个简单的异常处理程序会要求系统将可能被破坏的寄存器压栈,这可能是所有寄存器,具体取决于异常类型,然后调用异常处理程序代码。当异常返回时,这些寄存器会重新出栈恢复,并调用异常返回指令ERET。

向量表中的 32 个字(128字节)的空间足以存放用以下指令:堆栈、调用异常的处理程序代码、恢复寄存器和启动 ERET返回。下图是一个简单的异常处理程序示例。

简单的异常处理示例

堆栈指针选择和堆栈指针寄存器

当在AArch64中执行时,Arm架构允许选择两种堆栈指针寄存器。SP_EL0 或 SP_EL<x>,其中 <x> 是当前异常等级。例如,在EL1下可用选择SP_EL0或者SP_EL1,上面介绍异常向量表时提到过。

当发生异常时,堆栈指针自动选择当前异常等级的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,除非PSTATE.SP进行了配置。从上面提供的异常向量表中看,PE将跳转到 VBAR_EL1 + 0x280 地址处的异常向量。

这意味着我们可以将异常入口和高等级异常处理隔离开来,例如堆栈使用率比较高的C代码。SP_EL0通常分配的堆栈空间比较大,因此这种情况下可以充分利用。这也意味着异常处理入口并不依赖于主栈状态。有单独的异常向量支持这些更改,如上面的向量表偏移所示。

正如上面AArch64向量表提到的,异常向到达后,已经提供了有关异常发生时处理器状态的信息。例如,由于异常进入和退出时需要屏蔽异常,因此向量表“使用SP_ELx,异常来自当前异常等级”区域中的代码只有在异常情况下才会被触发执行。此向量分组中的异步异常表明发生了重大系统错误并且需要处理,例如内核崩溃。

异常返回

一旦异常处理完毕,处理程序就会返回到发生异常之前运行的代码。其通过如下操作来实现:

  • 恢复所有先前压栈的寄存器
  • 发起异常返回指令ERET

ERET指令从相关的SPSR寄存器恢复先前的处理器状态,并切换到ELR中记录的异常返回地址。返回的异常等级是SPSR_EL<x>配置的值,其中<x>是返回的异常等级。SPSR_ELx还包括目标执行状态。

当执行ERET指令时,PSTATE从SPSR_ELx中恢复值,PC更新为ELR_ELx中的值。这两个寄存器更新是原子执行,PE不会处于未定义状态。

正如上面保存当前处理器状态章节介绍的,每个EL上链接寄存器ELR会保存首选异常返回地址,这取决于异常类型。

对于同步服务调用(例如SVC),异常返回后执行的指令是紧随异常调用之后。对于其他同步异常,这是产生异常的指令地址。对于异步异常,首选的异常返回地址是发生异常时尚未完全执行的第一条指令,允许处理程序在需要时根据异常类型修改 ELR_ELx 的内容。

异常处理示例

接下来从端到端的角度简要展示一下同步异常和异步异常的处理示例。

同步异常处理

正如前面同步异常部分所讨论的,如果异常是执行指令直接引起的,并且返回地址表示导致该异常的指令,则可将其视为同步异常。以下寄存器提供处理异常SError的关键信息:

  • 异常综合信息寄存器(ESR_ELx)提供有关同步异常原因及类型的信息。
  • 故障地址寄存器(FAR_ELx)保存与异步异常地址相关的虚拟故障地址,如MMU故障。
  • 异常链接寄存器(ELR_ELx)保存触发指令的地址,提供异常返回的地址。

下面是AArch64异常模型使用系统调用的简单示例。

如果异常等级较低的程序想执行特权操作,其必须调用到较高异常等级。例如,在 EL0 上运行的 AArch32 应用程序代码可能请求在 EL1 上运行的 AArch64 操作系统/内核分配内存。为此,它通过执行SVC指令生成特权系统调用,并触发以下操作:

  • 当前的PSTATE状态保存在SPSR_EL1寄存器中
  • 首选的异常返回地址(紧接着的指令)写入ELR_EL1
  • 异常综合信息(异常原因)写入ESR_EL1
  • 目标执行状态取决于HCR_EL2.RW的读写位
  • 更新当前状态PSTATE,异常等级更改为EL1,执行状态更改为AArch64
  • 内核切换到异常向量表VBAR_EL1的0x600偏移地址处(VBAR_EL1+600),因为这是一个同步异常,异常来自较低等级的EL,并且所有较低的EL均是AArch32执行状态
  • 相关定义的寄存器进行压栈,用以维护寄存器上下文
  • 异步异常的类型由ESR_EL1标识,在本例中是SVC
  • SVC异常处理程序开始执行
  • 一旦SVC异常处理程序执行完成,控制权交回上级处理程序
  • 上级处理程序恢复以前压栈的寄存器,并执行ERET指令
  • PSTATE从SPSR_EL1中恢复(包括返回到异常等级EL0和目标执行状态AArch32),并且PC更新为ELR_EL1中的值

注意上面这个例子中,AArch64可能需要访问AArch32寄存器。

更加复杂的情况可能会涉及到系统调用,例如运行在EL0和非安全态下的AArch64应用程序可能需要调用在EL1上运行的安全可信操作系统。为了切换安全状态,你必须穿过EL3。然而正如异常产生指令章节所述,EL0无法直接发起对EL3下的SMC调用。在这个例子中,从NS.EL0到NS.EL1启动SVC指令,然后NS.EL1对EL3调用SMC指令,切换到S.EL1下的安全状态。这些在TrustZone for AArch64 guide有所介绍。

异步异常处理

异步异常(中断)位于PE外,并且也在正在执行的指令外。Arm架构没有定义何时发生异步异常,因此异步异常相对于其他异常(同步和异步)的优先级是由实现定义的。

一个简单的例子是,如果PE在EL0异常等级和AArch32状态下执行应用程序,发生了IRQ中断。HCR_EL2 和 SCR_EL3 已配置为将 IRQ 异常路由到 AArch64 中的 EL1,具体如下图:

简单异常处理示例

  • 当前的PSTATE状态保存在SPSR_EL1寄存器中
  • 首选异常返回地址(尚未执行的下一条指令)写入ELR_EL1
  • 目标执行状态取决于HCR_EL2.RW的读写位
  • 更新当前状态PSTATE,异常等级更改为EL1,执行状态更改为AArch64
  • 内核切换到异常向量表VBAR_EL1的0x680偏移地址处(VBAR_EL1+680),因为这是一个IRQ异常,异常来自较低等级的EL,并且所有较低的EL均是AArch32执行状态
  • 相关定义的寄存器进行压栈,用以维护寄存器上下文
  • IRQ异常处理程序开始执行
  • 一旦IRQ异常处理程序执行完成,控制权交回上级处理程序
  • 上级处理程序恢复以前压栈的寄存器,并执行ERET指令
  • PSTATE从SPSR_EL1中恢复(包括返回到异常等级EL0和目标执行状态AArch32),并且PC更新为ELR_EL1中的值

注意,IRQ异常本身并不区分引起中断的原因(例如定时器或者UART)。假设使用了GIC,可以通过GIC中的中断应答器IAR来识别哪个中断发生了,读取会返回中断ID和标记中断处于活动状态。一旦中断处理后,通过写中断结束寄存器EOIR,将GIC清除到非活动状态。

中断屏蔽和不可屏蔽中断

前面的中断示例只代表了一种非常简单情况,实际上异常处理时有时需要禁用或者屏蔽其他中断。物理和虚拟异步异常都可以暂时屏蔽,并保持挂起状态,直到取消屏蔽并处理,这是通过屏蔽相同类型的中断,直到稍后明确启用来实现的。

当在 AArch64 执行状态运行出现异常时,处理器会自动设置 PSTATE 中断屏蔽位 (PSTATE.DAIF)。 DAIF 代表调试、系统错误 (SError)、IRQ 和 FIQ。 DAIF 字段为 4 位,每一位对应于上述异常类型之一。通过向字段中的某个位写入 1,我们可以屏蔽或忽略相应异常类型。这些异常可以进入挂起状态,但不会得到处理。换句话说,PE 不会切换到异常处理程序,直到该位被取消屏蔽,从而有效地避免处理后面发生的异常。

如果在某个EL上中断正在处理,系统总是会屏蔽了其他中断。但是同步异常无法被屏蔽,因为其是由指令执行直接引起的。

你无法屏蔽路由到更高异常等级的异步异常,软件虽然可以设置较低EL下的PSTATE中断屏蔽位,但这不会阻止异常的发生。例如,如果你将FIQ路由到EL3(SCR_EL3.FIQ=1),在EL1 或 EL2 中将 PSTATE.F 设置为 1 不会阻⽌中断处理。

无论 PSTATE 如何,路由到较低异常级别的异常始终会被屏蔽。例如,如果 IRQ 路由到 EL2 或 EL1(SCR_EL1.IRQ=0),则当 PE 处于 EL3 中时,这个IRQ 始终被隐式屏蔽。

如果软件要支持中断嵌套,例如允许较高优先级异常中断较低优先级异常的处理,软件需要显式地重新启用中断。在相应寄存器被压栈之保存后,异常可以被嵌套。

在2021年,Armv8.8‑A 和 Armv9.3‑A 中添加了不可屏蔽中断 (NMI) 支持。具有超级优先级的中断被归类为 NMI,即使 PSTATE 中异常被屏蔽,其也可以被获取处理。

NMI 有一些限制。在第一次发生中断异常时,所有中断(包括 NMI)都会被屏蔽,这是为了让软件保存需要的状态,以便后续中断处理。

在某些时间点,软件不能处理任何中断,包括 NMI,为了解决这个问题,添加了新的PSTATE屏蔽位ALLINT,这允许软件选择不屏蔽中断、屏蔽大多数中断(不包括NMI)和屏蔽所有中断。

系统控制寄存器用以选择三种 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 中添加了对 NMI 的支持。

您可以在这篇博客文章中了解有关Arm A‑profile 对于 NMI 的支持情况。

检查你的知识

下面的问题将测试你学到的知识。

  1. Armv8‑A 中实现了哪些异常级别?

    EL0和EL1是强制性的。EL2和EL3是可选的,但是大多数设计都实现了。

  2. 执行状态是什么?

    AArch32和AArch64

  3. 异常进入时使用哪个堆栈?

    自动选择SP_ELx来提供安全的异常堆栈。

  4. AArch64 中向量表是如何实现的?

    PE将向量表基地址保存在VBAR_ELx中,这个向量表本身是在内存中。

相关信息

以下是与本文材料相关的一些资源:

以下架构学习指南与本指南中的材料相关:

以下培训课程与本指南中的材料相关:

后续步骤

本指南介绍了 AArch64 异常模型以及异常处理。我们研究了执行状态和安全状态、异常类型和异常处理,包括 AArch64 向量表。

当您想了解有关架构、中断工作原理以及处理器行为流程时,这些知识将非常有用。您可以将您的知识付诸实践,开发嵌入式代码、创建向量表和异常处理程序。

欢迎关注“安全有理”微信公众号。

安全有理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值