Amiya · 2021年06月03日

ARM系列 -- 异常和特权

大凡办一事,其中常有曲折交互之处,一处不通,则处处皆窒矣。
-- 曾国藩

异常(exception)和特权(privilege)是在ARMv8-A中定义的两个概念。

现代软件期望被分成不同的模块,每个模块对系统和处理器资源具有不同的访问级别。这方面的一个例子是操作系统内核和用户应用程序之间的分离,前者具有对系统资源的高级别访问权限,后者配置系统的能力更为有限。

Armv8-A通过实现不同级别的特权来实现这种分离。当前特权级别只能在处理器接受异常或从异常返回时更改。因此,在Armv8-A体系结构中,这些特权级别被称为异常级别(exception level,以下简称EL)。每个异常级别都有编号,权限级别越高,编号越高。

1.png
图1  异常等级

关于软件的划分,以及运行的等级,可以参考下表。用户程序运行在EL0,操作系统运行在EL1,虚拟机监控程序(hypervisor)运行在EL2,固件程序(firmware)运行在EL3。这里插一句,以前有VMware的兄弟告诉我,在服务器中,虚拟机程序运行在比OS更低的层次。那时候还不能理解。

2.png

关于特权,有两个方面。第一种是内存系统中的特权,第二种是从访问处理器资源的角度来看的特权。两者都受当前异常级别的影响。

Armv8-A实现了一个虚拟内存系统,其中内存管理单元(MMU)允许软件为内存区域分配属性。根据读/写权限的不同,可以配置为特权访问和非特权访问。对于内存的区域的配置,是由软件通过对MMU的转换表(translation table)的操作来完成的,同时MMU的配置会被保存到系统寄存器中,因此要考虑在当前的特权和异常等级下能否访问转换表和系统寄存器。

在ARM的架构下,这些系统寄存器在不同的异常等级下是独立的寄存器,在指令集中有自己的编码,并在硬件中单独实现。这些系统寄存器可以根据后缀区分出可以访问的等级。例如,TTBR0\_EL1是保存转换表基址的寄存器。如果EL0访问此寄存器,将会触发一个异常。

在高异常等级下可以访问低异常等级的寄存器,虽然大多数情况下不会这么做。但在某些场景下,还是需要的。比如在上下文切换或电源管理操作期间,更多特权级别有时会访问与较低异常级别关联的寄存器,以实现虚拟化功能或读写寄存器集,作为保存和还原操作的一部分。

ARMv8-A里面除了异常等级,还有两个状态:执行状态(execution state)和安全状态(security state)。

ARMv8-A有两种可用的执行状态:

  • AArch32:32位执行状态。此状态下的操作与Armv7-A兼容。有两个可用的指令集:T32和A32。标准寄存器宽度为32位。
  • AArch64:64位执行状态。有一个可用的指令集:A64。标准寄存器宽度为64位。

ARMv8-A架构实现两种安全状态,允许对软件进行进一步的分区,以隔离和划分受信任的软件:

  • 安全状态(secure):在此状态下,处理单元(Processing Element)可以访问安全的和非安全的物理地址空间。在这种状态下,PE可以访问安全和非安全系统寄存器。在这种状态下运行的软件只能响应安全中断。
  • 非安全状态(non-secure):在此状态下,PE只能访问非安全物理地址空间。PE也只能访问允许非安全访问的系统寄存器。在此状态下运行的软件只能响应非安全中断。

3.png
图2  不同安全状态,执行状态下的异常等级

处理器只有在复位和异常等级变换时才能改变执行状态。在ARMv8-A的大多数实现中,复位后的执行状态由复位时采样的信号控制,这样,可以通过片上系统级控制复位改变执行状态。当处理器在异常等级变化时,也可以更改执行状态,在AArch32和AArch64之间转换,但必须遵守某些规则:

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

所以,一个64位的操作系统可以运行64位或32位的应用程序;但是32位的操作系统不能运行64位的应用程序。此处插一句,linux是有用户态和内核态之分的,在这里可以对应EL1和EL0。你看,知识都是相通的。那你知道在linux里面,怎么从用户态切到内核态吗?

4.png
图3  OS和APP的执行状态关系

EL3总是被认为是在安全状态下执行的。ARMv8-A架构里允许实现选择是否实现所有异常级别,并选择每个实现的异常级别允许执行状态。EL0和EL1是唯一必须实现的异常级别。EL2和EL3是可选的。

EL3是唯一可以更改安全状态的异常等级。如果实现选择不实现EL3,则处理器将无法支持两种安全状态。同样,EL2包含了许多虚拟化功能。如果不实现EL2,就不支持虚拟化特性。

在处理器具体实现中,还可以选择对每个异常等级有效的执行状态。如果在某个异常等级下支持AArch32,则必须在所有较低的异常级别下支持AArch32。

许多处理器实现支持所有执行状态和所有异常级别,但也有一些例外。例如,Cortex-A32只支持AArch32。例如,Cortex-A55支持所有异常等级,但只在EL0上支持AArch32,其他异常级别EL1、EL2和EL3必须是AArch64。

异常是任何可能导致当前执行程序被挂起并导致状态更改以执行代码来处理该异常的事件。在ARMv8-A架构中,中断(interrupt)是一种外部生成的异常。Armv8-A体系结构将异常分为两种广泛类型:同步异常和异步异常。

同步异常是由刚刚执行的指令引起的异常。也就是说,同步异常与指令流同步。尝试执行无效指令(当前异常等级不允许的指令或已禁用的指令)可能会导致同步异常。同步异常也可能是由内存访问引起的,这是由于地址未对齐或某个MMU权限检查失败。因为这些错误是同步的,所以可以在尝试访问内存之前执行异常。当然,内存访问还可以生成异步异常,下面会说。

ARMv8-A体系结构有一系列异常生成指令:SVC、HVC和SMC。这些指令与简单的无效指令不同,因为它们针对不同的异常等级,并且在对异常进行优先级排序时会受到不同的处理。这些指令用于实现系统调用接口,以允许特权较低的代码从更特权的代码请求服务。

某些异常是在外部产生的,因此与当前的指令流不同步。这意味着不可能确切的保证何时发生异步异常。ARMv8-A架构只要求它在有限的时间内处理。异步异常也可以临时屏蔽。

异步异常类型包括:

  • 物理中断

•SError(系统错误)
•IRQ
•FIQ

  • 虚拟中断
    •vSError(虚拟系统错误)
    •vIRQ(虚拟IRQ)
    •vFIQ(虚拟FIQ)

IRQ和FIQ是用来产生外设中断的,它们有不同的路由控制。SError是为了响应错误的内存访问,而由内存系统生成的。比如,内存访问虽然通过了MMU检查,但是在总线上产生了错误。SError也可能是由于RAM的奇偶校验或ECC错误引起的。这些错误与指令流不是同步的关系。

当产生异常时,当前的程序被打断。处理器更新当前状态,切换到异常向量表(vector table),随后再切换到异常处理程序。

5.png
图4  异常处理流程

异常处理程序在内存中的存储位置称为异常向量。异常向量存储在一个表中,该表称为异常向量表。在Armv8-A中,处理器在系统寄存器中保存表的基址,并且每个异常类型都有一个定义好的基址偏移量。每个异常等级都有自己的向量表,由向量基地址寄存器VBAR\_ELx定义,其中<x>是1、2或3。异常向量表的格式如下图:

6.png
图5  ARMv8-A异常向量表

在ARMv8-A中,可以通过指令触发异常,从而切换异常等级。

今天主要介绍的是ARMv8-A中的异常概念,其实跟SoC设计相关性并不明显,更多的是跟软件设计相关,但对于整体理解ARM架构还是有帮助的。

作者:老秦谈芯
来源:https://mp.weixin.qq.com/s/mFAiVAQ4gFIrSaVkslaXsQ
作者微信公众号
qrcode_LaoQinTanXin_1.jpg

相关文章推荐

欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区Arm技术博客专栏
推荐阅读
关注数
23360
内容数
883
Arm相关的技术博客,提供最新Arm技术干货,欢迎关注
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息