本学习基于arm官网文档:learn_the_architecture_-_aarch64_exception_model
ARM架构不会规定哪个EL等级运行什么软件,但是一般用法都是按照上面的用法来用的。
EL等级只会在下面几种情况改变:
A核会提供MMU来做这种特权访问,EL0等级会做非特权检查,EL1、EL2、EL3会做非特权检查。
MMU的配置也受EL等级控制
AArch64 处理器的配置设置保存在一系列称为系统寄存器的寄存器中。
访问这些寄存器也是需要EL等级来控制的。比如VBAR_EL1这种后缀的寄存器,需要在EL1等级以上的上下文中访问才行。
不同后缀的寄存器是独立的、单独的寄存器(虽说功能相同,但是权限不同),在指令集中有自己的编码,并将在硬件中单独实现。
ARM-A核有两种Execution states和最高四种Security states
当前的执行状态决定了寄存器的位宽和可用的指令集。还会影响内存模型的各个方面以及异常的管理方式。
Armv8-A 和 Armv9-A支持以下两种运行时:
AArch32:
AArch64:
只有在重启的时候,或者EL等级改变的时候,运行时状态才会被改变,并且需要遵循下面的条件:
AArch64 允许实现多种安全状态。 这允许进一步划分软件以隔离和划分可信软件。
大多数A核处理器支持两种安全模式:
**安全模式:**这种模式可以访问安全地址和非安全地址(物理地址),和被隔离的寄存器
**非安全模式:**这种模式只能访问非安全物理地址,和允许非安全访问的寄存器。
每当您想要从一种安全状态切换到另一种安全状态时,您必须通过 EL3切换。
每个EL等级,PE都可能会处于安全模式或者非安全模式。时通过寄存器来确定的。
Armv9-A引入了RME模式,在RME模式下,它额外支持两种安全模式:
Realm state:在此模式下,PE可以访问非安全地址和Realm域地址空间
Root state:在此模式下,PE可以访问所有的物理地址,此模式只能在EL3下使用
RME把EL3视作特殊的安全模式,与其他EL等级不一样,也称做Root。EL3模式运行平台代码和初始化启动代码,默认这些代码是绝对安全的。
EL0和EL1等级是是必须要实现的,EL2和EL3等级是可选的。
如果没有用到EL2和EL3等级,那么需要考虑下面几件事情:
在Armv8.0-A,EL2只能运行在非安全模式下,因为虚拟化实现不支持安全模式,但是Armv8.4-A添加了S.EL2寄存器,所以可以运行在两种模式下;在Armv9-A,你如果在EL2下必须支持两种安全模式,即使disable位还存在。
AArch32兼容了老的32位架构,并且从Armv9-A开始,就限制了它只能在EL0下使用,AArch64可以在所有的EL等级下使用。
异常会中断当前正在运行的程序,并转去执行发生异常的处理代码。每个异常都有自己的回调函数。
对于AArch64来说,中断也是一种由外部触发的特殊的异常。
异常会被应用到如下几种场景中:
同步异常由当前被执行的指令引起,或者与当前被执行的指令相关。比如,当试图写一块被MMU定义为只读的内存区域时,就会发生同步异常。
如果以下全部适用,则异常是同步的:
Arm存在很多同步异常,甚至一条指令会引起多条异常,它为不同的同步异常提供了不同的优先级顺序。
这个异常引起的原因可能有:
陷阱异常举例:
kernel运行在EL1层,它会禁用EL0层的浮点运算单元用来节省进程切换的时间,当一个FP或者SIMD指令运行的时候,它会引起陷阱异常并跳转到EL1层处理,在这里打开SIMD/FP单元并且标记已经使能了这个单元。
陷阱异常在虚拟化应用中非常重要,可以参考官网文档:AArch64 virtualization guide
内存访问导致的同步异常可能是MMU执行检查的结果,也可能是由于内存系统返回的错误。比如:访问了没有权限的内存地址或者写了只读区域的地址。
内存访问也可能会引起异步异常(SError)。
通常是低权限的软件用来请求高权限软件的服务,也就是系统调用。
ARM提供了三条指令来产生指令异常:
EL0级的服务不能直接调用HVC和SMC,只有通过SVC进入kernel之后,才能调用SVC和SMC调用相关服务。
各种指令的路由关系:
调试异常会跳转到debugger所在的异常等级,包括下面几种异常:
异步异常也叫物理中断,外界通过中断告诉PE发生了外部事件,然后通过指定的回调来处理,主要有下面几种:
通常是内存系统用来响应非预期的事务,包括:
SErrors are treated as a separate class of asynchronous exception because you would normally have separate handlers for these cases. SError generation is IMPLEMENTATION DEFINED。
硬件中断,区分于SError,这个中断用来标记外部硬件的中断,比如timer或者外围硬件,他们通过GIC来管理。
当支持虚拟化时,在hypervisor中或者VM中处理的中断就是虚拟中断,包括外部设备产生的中断和软件中断,AArch64支持虚拟中断:
虚拟中断可以在hypervisor中产生,也可以通过中断控制器产生。hypervisor通过配置HCR.EL2寄存器来产生异常,产生的异常将物理异常路由到EL2,并将虚拟异常 signaling到EL1。
以下是AArch64处理中断时的术语:
当异常发生之后,处理器保存当前PE的状态和异常返回地址,转去处理异常。
当处理异常时,会进行下面的操作:
当异常发生之后,PE会跳转到异常向量表中处理异常,对于AArch64来说,这个向量表不止是地址,还包含有最多32条指令。
需要注意,提升ELx等级唯一的方法就是产生异常,降低EL等级唯一的办法就是从异常中返回,每种异常都有自己对应的EL等级目标。
因为有可能AArch32的程序可以通过异常跳转到高等级的EL等级,从而执行AArch64的代码,因此AArch64回调处理函数可能会处理AArch32的寄存器,因此两者有一个对应关系:
AArch64有一个处理器状态的概念,称为PSTATE。它保存了当前的EL等级和ALU状态等,包括:
每个异常类型的EL等级由下面两种控制:
举例:
3. 像SVC、HVC、SMC这些指令是自带EL等级的
4. 像是IRQ、FIQ、SError等异常,可以通过寄存器配置他们是在哪个EL等级。
需要注意的是,当SCR寄存器和HCR寄存器发生冲突时,SCR寄存器的配置会覆盖HCR寄存器的配置。
进入和返回到一个未定义的EL等级都会引起错误。
路由到当前异常级别的异常可以被当前级别屏蔽。路由到较低异常级别的异常始终被屏蔽。异常将挂起,直到PE更改为与路由到的异常级别相等或更低的异常级别。这符合一条规则,即您永远不会因为执行异常而失去特权。如下图:
每个EL等级都有属于自己的中断向量表,基地址存放在寄存器VBAR_ELx中(Vector Base Address Register),但是EL0没有自己的中断向量表。所有的向量表格式都是一样的,每个异常类别都有一个异常向量,位于向量基地址的固定偏移处。
VBAR寄存器地址复位之后会被清空,所以需要在使能中断之前配置这个寄存器。
异常向量表偏移对应的向量入口:
从上表可以看出,表格被分为2个组,每个组又被分为两个小组,分别是:
需要注意的是,运行时状态是取决于发生异常时要跳到的EL等级,而不是当前运行时的EL等级,举例:当从EL0到EL1时,向量表入口取决于EL1的运行时状态;并且,当从EL0到EL2时,向量表也是取决于EL1的运行时状态,这是因为我们一般希望hypervisor运行在EL2,而VM运行在EL1和EL0,hypervisor需要知道kernel运行在EL1时的运行时状态,而不仅仅是应用的运行时状态(因为AArch32的应用可能运行在AArch32的OS上,也可能运行在AArch64的OS上)。
异常发生时,栈指针选择遵循下面的规则:
当从异常返回时,会执行下面两个步骤:
ERET指令会从SPSR寄存器恢复处理器状态,并从ELR寄存器中拿到程序返回地址并执行。SPSR_ELx寄存器记录了程序返回时的EL状态及运行时,并且运行时状态必须和SCR_ELx或者HCR_ELx中的RW bit位相匹配,不然会报错。
执行ERET指令时,会将SPSR_ELx寄存器恢复到PSTATE,ELR_ELx寄存器的值恢复到程序计数器中,并且这个过程时自动的、原子的,防止PE进入一个未定义的状态。
可以通过下面几个寄存器来查看异常信息:
举例说明:
当一个AArch32的软件运行在EL0,需要通过SVC指令进行系统调用时,此时OS以AArch64状态运行在EL1,会进行以下操作:
4. 将当前PSTATE写入寄存器SPSR_EL1中
5. 将异常返回地址写入寄存器ELR_EL1中
6. 通过HCR_EL2.RW bit判断应该运行在哪个运行时(AArch64)
7. 更新当前PSTATE,同时EL等级切换到EL1,运行时切换到AArch64
8. 处理器转去VBAR_EL1 + 0x600地址去执行,因为根据前面所述规则,从低等级到高等级,并且低等级运行时都是AArch32
9. Defined registers are stacked to maintain register context
10. The type of asynchronous exception is identified from ESR_EL1, in this case SVC
11. 执行指定的SVC回调代码
12. Once the SVC-specific handler code completes, control returns to the high-level handler
13. 恢复之前压栈的寄存器,并执行ERET指令
14. 恢复PSTATE寄存器,以及返回到EL0,运行时切换到AArch32,ELR_ELx寄存器装载到程序计数器继续运行
一个更复杂的例子是,EL0下的非安全的APP请求一个EL1安全OS的服务,这时候,需要从EL0到EL3,再从EL3到安全的EL1,因为EL0不能直接请求EL3的服务,所以这时候需要EL0下通过svc指令进入非安全的EL1,非安全的EL1再通过smc指令进入EL3然后再请求安全的EL1下OS的服务,具体步骤可以参考:TrustZone for AArch64
当程序运行在EL0时,一个中断打断执行,并进入EL1(HCR_EL2和SCR_EL3配置中断路由到EL1),会执行下面的步骤:
需要注意的是,处理器不知道当前发生了哪个中断,它时通过读取GIC的IAR(Interrupt Acknowledge Registers)寄存器来判断当前中断ID的。读取IAR寄存器会返回当前的中断ID并标记这个中断为active,然后在处理完这个中断之后,需要回写GIC的EOIR(End of Interrupt Register)寄存器来标记完成了这个中断。