本文介绍:Armv8-A的异常和特权模型。包括:ARM架构中的不同的异常,以及当处理器接收到异常之后的行为
本文将有助于:
你将学会:
在我们解释Armv8-A异常模型的细节之前,让我们首先介绍特权的概念。现代软件希望被分成不同的模块,每个模块对系统和处理器资源有不同的访问级别。这方面的一个例子是操作系统内核和用户应用程序之间的分离,操作系统内核具有对系统资源的高级访问权限,而用户应用程序配置系统的能力则更有限。
Armv8-A通过实现不同级别的特权来实现这种分割。只有在在处理器接到异常或从异常返回时才能更改特权等级。因此,这些特权级别在Armv8-A体系结构中被称为异常级别。每个例外级别都有编号,级别越高的特权有越高的编号。
如下图所示,异常级别称为EL, x为0 ~ 3之间的数字。例如,最低级别的特权被称为EL0。
大多数应用代码运行在EL0,操作系统运行在EL1,hypervisor运行在EL2。EL3为底层固件和安全代码所保留。
注意:架构不强制要求这个软件模型,但是标准软件采用这种模式。
有两种与本主题相关的类型,第一种是存储系统中的特权,第二种是从访问处理器资源的视角出发的特权。这两种特权都受当前的异常等级影响。
Armv8-A实现了虚拟存储系统。在这个系统中,MMU允许软件指定内存区域的属性。这些属性包括读写权限,可以用两个维度来进行配置。这种使得允许特权访问和非特权访问的权限得以分离。
发生在EL0的内存访问将对照非特权访问权限进行检查。相对的,EL1,EL2和EL3的访问将对照特权访问权限进行检查。
由于这种存储权限配置将用到MMU的转换表,所以在编程这些表格的时候需要考虑到这种特权。MMU的配置存储在系统寄存器中,访问这些寄存器的能力也由当前异常级别控制。
一组系统寄存器存储了Armv8-A处理器的配置设置。系统寄存器的设置组合定义了当前处理器的上下文。对系统寄存器的访问由当前的异常等级决定。
系统寄存器的名称表示可以访问该寄存器的最低异常级别。例如,TTBR0_EL1是保存EL0和EL1使用的转换表的基址的寄存器。这个寄存器不能从EL0访问,如果硬要访问将导致一个异常。
Armv8-A架构有很多组功能相似的寄存器,其命名仅有后缀的异常等级不同。这些寄存器相互之间独立,在指令集中的编码不同,在硬件上单独实现。比如下面的寄存器,针对不同的转换阶段配置MMU。虽然名字相似,但是各自有各自的访问语义:
注意:EL1和EL0共享MMU的配置,且只能由运行在EL1的代码进行控制。因此,没有SCTLR_EL0.
高级别可以访问低级别的寄存器。例如,切换上下文需要保存寄存器状态的时候。
Armv8-A处理器的当前状态取决于异常等级和另外两个重要状态。
当前执行状态定义了通用寄存器的标准宽度和可用的指令集。
执行状态不会更改内存模型和异常的管理。
当前的安全状态控制着哪些异常等级当前有效,哪块内存区域当前可访问,以及这些访问如何在系统内存总线上表示。
下图显示了不同执行状态下使用的异常等级和安全等级:
Armv8-A的两个执行状态:
Armv8-A体系结构允许实现两种安全状态。这允许对软件进行进一步的分区,以隔离和划分受信任的软件:
安全状态的使用请参考:rustZone指南
PE只能在复位或异常等级发生变化时改变执行状态。
复位时的执行状态由IMPLEMENTATION DEFINED的机制决定。一些实现固定了复位时的执行状态。例如,Cortex-A32总是会复位为AArch32状态。在大多数Armv8-A的实现中,复位后的执行状态由复位时采样的信号控制。这允许在片上系统(Soc)级别上控制重置重围状态。
当PE在异常级别之间改变时,也可以改变执行状态。在AArch32和AArch64之间的转换只允许遵循一定的规则。
当从较低的异常级别移动到较高的异常级别时,执行状态可以保持不变或更改为AArch64。
当从较高的异常级别移动到较低的异常级别时,执行状态可以保持不变或更改为AArch32。
将这两个规则放在一起意味着64位层可以承载32位层,但32位层不能承载64位层。例如,64位操作系统内核可以同时承载64位和32位应用程序,而32位操作系统内核只能承载32位应用程序。如下图所示:
EL3总是被认为在安全状态下执行。使用SCR_EL3, EL3代码可以改变所有较低异常级别的安全状态。如果软件使用SCR_EL3改变低异常级别的安全状态,PE只有在改变到低异常级别时才会改变安全状态。
Armv8-A架构允许厂家选择是否实现所有异常级别,以及为每个实现的异常级别选择允许哪些执行状态
EL0和EL1必须实现,EL2和EL3可选。选择不实现EL3或EL2具有重要的含义。
EL3是唯一可以改变安全状态的级别。如果一个实现选择不实现EL3,那么该PE将无法访问单个安全状态。
类似地,EL2包含很多虚拟化功能。目前该体系结构的所有Arm厂家都实现了所有的异常级别,如果没有所有的异常级别,就不可能使用大多数标准软件。
厂家还可以选择每个异常级别有效的执行状态。如果在异常级别允许AArch32,那么所有比它等级低的异常级别都可以允许AArch32。
许多厂家都允许所有的执行状态和所有的异常等级。但是现存的厂家又一定限制。举例,Cortex-A32所有异常级别只允许AArch32。
Cortex-A55只允许EL0上AArch32。
异常:异常将导致当前执行的程序阻塞,当前的状态将会发生改变以执行处理异常的程序。
其它处理器架构可能将其描述为中断。
在Armv8-A架构中,中断是一种外部生成的异常。
Armv8-A体系结构将异常分为两大类:同步异常和异步异常。
同步异常:是由当前指令引起的或与之相关的异常。这意味着同步异常与执行流是同步的。
**同步异常的产生:**试图执行无效的指令,或当前异常等级不允许或已禁止执行的指令。
同步异常也可能由内存访问引起,这可能是地址非对齐的结果,也可能是因为MMU权限检查失败。
因为这种错误是同步的,所以可以在试图访问内存之前接收到异常。内存访问还可以生成异步异常,后面将讨论。
Armv8-A体系结构有一系列异常生成指令**:SVC**、HVC和SMC。这些指令不同于简单的无效指令,因为它们针对不同的异常级别,并且在对异常进行优先级排序时处理不同。这些指令用于实现系统调用接口,以允许特权较低的代码向特权较高的代码请求服务。
调试异常也是同步的。调试异常在Debug overview guide中进行了讨论。
某些类型的异常是在外部生成的,因此与当前指令流不同步。这意味着不可能准确地保证何时接收异步异常。
Armv8-A架构只要求它在有限的时间内发生。异步异常也可以暂时屏蔽。这意味着异步异常可以在异常被接受之前保持挂起状态。
这些异步异常类型是:
物理中断:
虚拟中断:
物理中断是响应PE外部产生的信号而产生的。虚拟中断可以由外部生成,也可以由在EL2上执行的软件生成。
Armv8-A体系结构有两种异常类型,IRQ和FIQ,它们被用于生成外围中断。在Arm架构的其他版本中,FIQ被用作更高优先级的快速中断。这与Armv8-A不同,在Armv8-A中FIQ与IRQ具有相同的优先级。
IRQ和FIQ有独立的路由控制,经常用于实现安全中断和不安全中断,在Generic Interrupt Controller guide中有讨论。
SError是一种异常类型,由内存系统在响应错误的内存访问时产生。
SError的典型用法是以前所说的外部、异步中止,例如,内存访问通过了所有的MMU检查,但是在内存总线上发生了错误。这个错误可能是异步报告的,因为发出的指令已经结束了。
SError中断也可能由一些ram上的奇偶校验或错误纠正码(ECC)检查引起,例如内置缓存中的校验码。
当发生异常时,当前程序流被中断。处理单元(PE)将更新当前状态,并跳转到向量表中的某个地址。通常这个地址包含相应的代码,能够将当前程序的状态保存到堆栈上,然后分支到进一步的代码。如下图:
发现异常时的处理器状态称为异常发生时的状态。PE在异常之后的状态就是异常被带入的状态。
Armv8-A架构有触发异常返回的指令。在这种情况下,执行该指令时PE所处的状态就是异常返回前的状态。异常返回指令执行后的状态就是异常返回到的状态。
每一个异常类型对应着一个异常级别。异步异常可以被路由到不同的异常级别。
当处理异常时,当前的状态必须保存,以便返回。PE将自动地保存异常返回地址和当前的PSTATE。
通用寄存器的数据必须由软件保存。PE将跟新当前的PSTATE为体系结构中相应的异常类型所定义的PSTATE,然后从向量表跳转到相应的异常handler。
异常对应的PSTATE存储在系统寄存器SPSR_ELx,其中是异常被带入的异常级别的编号。异常返回地址储存在ELR_ELx。其中是异常被带入的异常级别。
这三种物理中断类型可以被路由到特权异常级别:EL1、EL2或EL3。下图以IRQs为例:
路由使用SCR_EL3和HCR_EL2配置。使用SCR_EL3进行的路由配置将覆盖使用HCR_EL2进行的路由配置。这些控制允许不同的中断类型被路由到不同的软件。
路由到比正在执行的级别更低的异常级别的异常被隐式屏蔽。该异常将被挂起,直到PE的异常级别等于或低于路由到的异常级别。
异常级别的执行状态由更高级别的异常决定。假设所有异常级别都实现了,下表显示了如何确定执行状态:
去往的异常等级: | 异常等级取决于: |
---|---|
非安全状态的EL1 | HCR_EL2.RW |
安全状态EL1 | SCR_EL3或者HCR_EL2(如果使能安全EL2) |
EL2 | SCR_EL3.RW |
EL3 | EL3的复位状态 |
软件可以通过执行AArch64中的ERET指令来启动异常返回。
这将会导致异常等级根据SPSR_ELx
的值进行返回。
SPSR_ELx
包含返回的目标等级和返回的执行状态。
注意,SPSR_ELx
中指定的执行状态必须与SCR_EL3.RW
或HCR_EL2.RW
中的任何一个配置匹配,否则将非法。
执行ERET指令时,状态将从SPSR_ELx
恢复,程序计数器将更新为ELR_ELx
中的值。这两个更新将以原子和不可分割的方式执行,这样PE就不会处于未定义的状态。
当在AArch64中执行时,架构允许选择两个堆栈指针寄存器;SP_EL0或SP_ELx,其中是当前异常级别。例如,在EL1,可以选择SP_EL0或SP_EL1。
在常规执行期间,所有代码都应该使用SP_EL0。在处理异常时,首先选择SP_ELx。这将维护一个单独的栈来处理异常的前若干步。
这对于在处理由堆栈溢出引起的异常时很有用。
在Armv8-A中,向量表是包含指令的普通内存区域。处理器元素(PE)保存着系统寄存器中表的基址,并且每种异常类型都定义了与该基址的偏移量。
每一个特权异常等级都有着各自的向量表,由VBAR_ELx
(Vector Base Address Register)定义基址。
复位后VBAR寄存器的值是未定义的,在启动中断前寄存器必须配置好。
向量表格式如下:
每种异常类型都可能导致 跳转到四个位置中的一个,这个位置基于异常所在级别的状态。
https://developer.arm.com/architectures/learn-the-architecture/exception-model