本章重点分析处理器产生异常的原因、在发生异常时的动作以及异常返回时的动作。ARMv8有四种异常等级EL0/1/2/3,以及两种安全模式,这部分的内容我会在下一章节详细讲解,本章针对于异常的通用概念。
1、AArch64异常和中断处理的定义
异常是指需要特权软件(异常处理程序)执行某些操作以确保系统顺利运行的条件或系统事件。它们会导致执行流程中断。
区分异常和中断的一种方法是:异常是一个事件(分支或跳转指令除外),它会导致指令的正常顺序执行被修改;而中断是一种非直接由程序执行引起的异常,通常用处理器内核外部的硬件发出中断信号,例如按下按钮。
每个异常类型都有与之关联的异常处理程序。当异常被处理后,特权软件会让内核在恢复异常处理之前的现场。
通常,中断用于表示中断信号。在ARM-A系列处理器上,中断信号可以是IRQ或FIQ中断信号。ARM体系结构将异常分为两组,同步和异步。同步异常类型可能有许多原因,但它们的处理方式类似。同步异常是指处理器需要等待异常处理的结果,然后继续执行后面的指令。异步异常类型细分为三种中断类型:IRQ、FIQ和SError(系统错误)。异步异常于处理器当前处理的指令和中断时完全没有关系的。
中断属于异常的一种!!!
以下类型的操作可能导致异常:
中止
中止包含指令中止(获取指令失败)和数据中止(数据访问失败)。它们可能来自外部内存系统,在内存访问时给出错误响应(可能表示指定的地址与系统中的实际内存不对应)。处理器中的MMU可以捕获这些错误并且报告给处理器,MMU中止内存分配。
指令中止时当处理器尝试执行某条指令发生错误时,会发生指令中止。数据中止异常是由加载或存储指令引起的,发生在处理器尝试读取或写入外部存储器数据之后。
如果中止是通过直接执行指令生成的,并且由返回地址判断导致中止的指令,则中止被描述为同步中止。其他情况为异步中止。
在AArch64中,同步中止会导致同步异常。异步中止会导致系统错误(SError)。
复位
复位操作是优先级最高的一种异常处理,复位有自己专有的向量,该向量使用IMPLEMENTATION DEFINED的地址,该地址通常由配置输入信号设置。
可以从重置向量基址寄存器RVBAR_ELn读取该地址,其中n是实现的最高异常级别的编号。
所有核心都有一个复位输入,复位后会出现复位异常。它是最高优先级的异常,不能被屏蔽。此异常用于在系统通电后在内核上执行代码以对其进行初始化。让CPU复位引脚产生复位信号,CPU进入复位状态,并重新启动。
软件产生的异常
ARMv8架构提供了三种软件产生的异常,这些异常通常是指软件想尝试进入更高的异常等级而造成的错误。
- SVC指令:允许用户模式的程序请求操作系统服务。
- HVC指令:允许客户机(Guest OS)请求主机的服务。
- SMC指令:允许普通世界中的程序请求安全监控服务。
中断
中断有三种类型:IRQ、FIQ和SError。与SError相比,IRQ和FIQ是通用的,后者专门与外部异步数据中止相关。因此,“中断”通常仅指IRQ和FIQ。
FIQ的优先级高于IRQ,这两种中断通常与每个核心的单个输入引脚相关。在芯片内部,分别有分别有连接到处理器内部的IRQ和FIQ两根中断线。在几乎所有系统上,各种中断源都使用中断控制器连接。中断控制器对中断进行仲裁和优先排序,并依次提供一个串行化的单一信号,该信号随后连接到内核的FIQ或IRQ信号。连接关系如下所示:
由于IRQ和FIQ中断在任何给定时间都与内核上运行的软件没有直接关系,因此它们被归类为异步异常。
2、同步异常和异步异常
在Aarch64中分为同步异常和异步异常两种:
- 如果异常是通过直接执行指令生成的,并且返回地址指示导致异常的指令,则将异常描述为同步异常。否则,中止则被描述为异步的。
- 同步异常是指处理器需要等待异常处理的结果,然后继续执行后面的指令,如果数据中止时我们知道发生数据异常的地址,并且在异常处理函数中修复这个地址。
异步异常的来源是IRQ、FIQ或SError(系统错误)。SError有几个可能的原因,最常见的是异步数据中止(例如,从缓存线向外部内存回写脏数据时触发的中止)。
2.1、处理同步异常
ESR_ELn和FAR_ELn寄存器包含了用于向异常处理程序提供有关同步异常原因的信息。ESR_ELn提供有关异常原因的信息,而FAR_ELn保留所有同步指令和数据中止以及对齐故障的故障虚拟地址。
ELR_ELn保存了导致中止数据访问(用于数据中止)指令的地址。这些在内存故障后更新。但在其他情况下设置,例如,通过分支到未对齐的地址。
如果使用AArch64将异常从AArch32中的异常级别带入异常级别,并且该异常写入与目标异常级别关联的故障地址寄存器,则FAR_ELn的前32位均设置为零。
对于实现EL2(虚拟机监控程序)或EL3(安全内核)的系统,同步异常通常在当前或更高的异常级别中获取。异步异常可以(如果必要时),路由到更高的异常级别,由虚拟机监控程序或安全管理程序处理内核SCRYEL3登记器指定哪些异常路由到EL3,并且类似地,HCR_EL2指定将哪些异常路由到EL2。有单独的位允许对IRQ、FIQ和SError路由的单独控制。
2.2、Exception Syndrome Register
ESR_ELn包含允许异常处理程序确定异常原因的信息。它仅针对同步异常和错误进行更新。由于这些中断处理程序通常从通用中断控制器(GIC)中的寄存器获取状态信息,因此不会对IRQ或FIQ进行更新。
Bits [31:26] (ESR_ELn.EC):它表示异常类别,使处理程序能够区分各种可能的异常原因(例如未分配指令、从MCR或MRC到CP15的异常、从FP操作、执行SVC、HVC或SMC的异常、数据中止和对齐异常)。例如,EC=101111是一个SError中断。
Bit [25] (ESR_ELn.IL):表示捕获指令的长度(16位指令为0,32位指令为1),并为某些异常类别设置。
Bits [24:0] (ESR_ELn.ISS) :形成指令特定综合征(ISS)字段,其中包含特定于该异常类型的信息。例如,当执行系统调用指令(SVC、HVC或SMC)时,该字段包含与操作码(如SVC的0x123456)关联的立即值。
2.3、未分配的指令
未分配的指令会导致AArch64中的同步中止。当处理器执行以下操作之一时,将生成此异常类型:
- 未分配的指令操作码;
- 需要比当前异常级别更高权限级别的指令;
- 已禁用的指令;
- 设置PSTATE.IL字段时的任何指令。
2.4、系统调用
某些指令或系统功能只能在特定的异常级别执行。例如,如果以较低的异常级别运行的代码必须执行特权操作,例如当应用程序代码从内核请求功能时。一种方法是使用SVC指令。这允许应用程序生成异常。参数可以在寄存器中传递,也可以在系统调用中编码。
例如:EL0处的应用程序代码使用malloc()请求内存
2.5、对EL2/EL3的系统调用
SVC指令可用于从EL0的用户应用程序调用EL1的内核。HVC和SMC系统调用指令以与EL2和EL3类似的方式移动处理器。当处理器在EL0(应用程序)上执行时,它不能直接调用虚拟机监控程序(EL2)或安全监视器(EL3)。这只能在EL1及以上版本中实现。因此,应用程序必须使用SVC调用内核,并允许内核代表它们调用更高的异常级别。
从操作系统内核(EL1),软件可以使用HVC指令调用虚拟机监控程序(EL2),或者使用SMC指令调用安全监视器(EL3)。如果处理器是用EL3实现的,则提供了从EL1获取EL2陷阱SMC指令的能力。如果没有EL3,则SMC未分配,并在当前异常级别触发。
类似地,从虚拟机监控程序代码(EL2),程序可以使用SMC指令调用安全监视器(EL3)。如果在EL2或EL3中进行SMC调用,它仍然会在相同的异常级别上导致同步异常,并且该异常级别的处理程序可以决定如何响应。
3、异常发生后的处理
ARMv8-A体系结构有四个异常级别:EL0、EL1、EL2和EL3。处理器执行只能通过获取异常或从异常返回来在异常级别之间切换。
- 当处理器从较高的异常级别移动到较低的异常级别时,执行状态可以保持不变,也可以从AArch64切换到AArch32。
- 当从较低的异常级别移动到较高的异常级别时,执行状态可以保持不变或从AArch32切换到AArch64。
异常会导致程序流的更改。从与所采取异常对应的异常向量开始,在所采取异常的异常级别重新启动执行。也就是说,异常向量保存异常处理程序的第一条指令。
当发生导致异常的事件时,CPU会感知异常发生,而且会对应生成一个目标异常等级,处理器硬件会自动执行以下操作:
-
把PSTATE寄存器的值保存到SPSR_ELn寄存器。更新SPSR_ELn(其中n是异常发生的异常级别),以存储在异常结束时正确返回所需的PSTATE信息。
-
PSTATE被更新以反映新处理器状态(这可能意味着异常级别被提升,或者可以保持不变)
-
把返回地址保存在对应目标异常等级的ELR中。 注:寄存器名称上的_ELn后缀表示这些寄存器有多个副本存在于不同的异常级别。例如,这意味着SPSR_EL1是与SPSR_EL2不同的物理寄存器。
-
处理器转到向量表,向量表其中包含每个异常类型的条目。向量表包含一系列代码,该代码通常标识异常的原因,并选择和调用相关函数来处理异常。
当处理程序代码完成执行时,它返回到高级处理程序,然后高级处理程序执行ERET指令以返回到应用程序。
异步异常的目标执行状态(即AArch32或AArch64)和异常级别使用系统寄存器SCR_EL3和HCR_EL2进行配置。*(具体配置我也会在专门文章中讲解!!!)
4、AArch64异常向量表
发生异常时,处理器必须执行与异常对应的处理程序代码。内存中存储处理程序的位置称为异常向量。在ARM体系结构中,异常向量存储在一个表中,称为异常向量表。
每个异常级别都有自己的向量表,即EL3、EL2和EL1各有一个向量表。该表包含要执行的指令,而不是一组地址。这些通常是将核心指向完整异常处理程序的分支指令。
例如,EL1的异常向量表包含处理EL1上可能发生的所有类型的异常的指令,各个异常的向量表的开头有固定的偏移量。每个表基地址的虚拟地址由向量基地址寄存器VBAR_EL3、VBAR_EL2和VBAR_EL1设置。
向量表中的每个条目都有16条指令长(在ARMv7-A和AArch32中,每个条目只有4个字节)。这意味着在AArch64中,顶级处理程序可以直接写入向量表。
基址由VBAR_ELn给出,每个条目与该基址之间有一个定义的偏移量。每个表有16个条目,每个条目的大小为128字节(32条指令)。该表实际上由4组4个条目组成。使用哪个条目取决于几个因素:
- 异常类型(SError IRQ FIQ 同步异常)
- 如果在同一异常级别执行异常,则使用堆栈指针(SP0或SPn)
- 如果在较低的异常级别执行异常,则下一个较低级别(AArch64或ARCH32)的执行状态。
典型的异常向量表如下所示:
如果内核代码在EL1处执行,并且发出IRQ中断信号,则会发生IRQ异常。这个特定的中断与hypervisor或安全环境无关,也在内核中处理,并且设置了SPSel位,因此使用SP_EL1。因此,从地址VBAR_EL1+0x280执行。
在ARMv8-A体系结构中没有LDR PC、[PC、#offset]的情况下,必须使用更多指令才能从寄存器表中读取目标。向量的间距旨在避免未使用的向量对典型大小的指令缓存线造成缓存污染。重置地址是一个单独的地址,由实现定义,通常由内核内的硬连线配置设置。该地址在RVBAR_EL1/2/3寄存器中可见。
为每个异常提供单独的异常向量,使操作系统或虚拟机监控程序能够灵活地确定较低异常级别的AArch64和AArch32状态。SP_ELn用于从较低级别生成的异常。但是,软件可以切换到在处理程序中使用SP_EL0。使用此机制时,它有助于从处理程序中的线程访问值。
5、从异常返回
必须通过软件告知处理器何时从异常返回。这是使用ERET指令在代码中完成的。这将从SPSR_ELn恢复到异常之前的状态,并通过从ELR_ELn恢复PC将程序执行返回到原始位置。
ARMv8A为子例程和异常返回提供单独的链接寄存器。
在A64指令集中,寄存器X30(与RET指令一起)用于从子程序返回。每当执行带链接的分支指令(BL或BLR)时,它的值都会用要返回的指令的地址更新。
ELR_ELn寄存器用于存储异常的返回地址。该寄存器中的值在进入异常时自动写入,并作为执行用于从异常返回的ERET指令的效果之一写入PC。
从异常返回时,如果SPSR中的值与系统寄存器中的设置冲突,则会出现错误。
ELR_ELn包含返回地址,该地址取决于特定的异常类型。通常,这是生成异常的指令之后的指令地址。
例如,当执行SVC(系统调用)指令时,希望返回到应用程序中的以下指令。但是,在其他情况下,可能需要重新执行生成异常的指令。
对于异步异常,ELR_ELn指向由于中断而未执行或未完全执行的第一条指令的地址。例如,如果在中止同步异常后需要返回指令,则允许处理程序代码修改ELR_En。ARMv8-A AArch64模型比AArch32或ARMv7-A中使用的模型更简单,在AArch32或ARMv7-A中,出于向后兼容性的原因,当从某些类型的异常返回时,有必要从链接寄存器值中减去4或8。
除了SPSR和ELR寄存器外,每个异常级别都有自己的专用堆栈指针寄存器。它们是SP_EL0、SP_EL1、SP_EL2和SP_EL3。这些寄存器用于指向专用堆栈。例如,堆栈可用于存储被异常处理程序损坏的寄存器,以便在返回原始代码之前将其还原为原始值。
处理程序代码可以从使用SP_ELn切换到SP_EL0。例如,SP_EL1可能指向一块内存,该内存包含一个内核始终可以保证有效的小堆栈。SP_EL0可能指向更大的内核任务堆栈,但不能保证不会溢出。此切换通过写入SPSel寄存器来控制。
总结:在异常返回执行ERET指令,处理器会自动完成:
- 从ELR_ELx寄存器中恢复PC指针。
- 从SPSR_ELx寄存器恢复处理器的状态。
注意:中断处理过程是关中断进行的,在中断处理完成后通过恢复PSTATE寄存器把中断打开。
后续会介绍处理器模式、异常等级、状态的切换以及异常向量表和中断处理函数怎么写。