严格地说,中断是指中断软件执行的流程。然而,在ARM术语中,这通常称为异常。异常是指需要特权级(异常处理程序)执行某些操作以确保系统顺利运行的条件或系统事件。每个异常都有一个相关的异常处理程序。一旦异常得到处理,特权级软件会将core恢复到处理异常前的位置,以继续处理它正在做的事情。
存在的异常类型如下:
中断:
有两种类型的中断称为IRQ和FIQ。FIQ的优先级高于IRQ。这两种类型的异常是于外部的输入引脚有关。外部硬件触发一个中断请求并在当前指令完成执行时触发相应的异常类型若中断未被禁用。
FIQ和IRQ都是发送给内核的物理信号,当断言时,如果当前已启用,内核将接受相应的异常。在几乎所有系统上,各种中断源都使用中断控制器连接。中断控制器对中断进行仲裁和优先排序,并依次提供一个串行化的单一信号,该信号随后连接到内核的FIQ或IRQ信号。有关更多信息,请参阅第10-17页的通用中断控制器。
由于IRQ和FIQ中断的发生可以发生在内核运行的任意时间段,因此它们被归类为异步异常。
中止:
中止来源于指令取指失败(指令中止)和数据访问失败(数据中止)。它们可以来自外部内存系统,在内存访问时给出错误响应(可能表示指定的地址与系统中的实际内存不对应)。另外,中止可以由内核的内存管理单元(MMU)生成。操作系统可以使用MMU中止来为应用程序动态分配内存。
预取一条指令时,可以在指令流水线中中将其标记为已中止。仅当内核尝试执行它时,才导致预取中止异常。异常发生在指令执行之前。如果标记为中止的指令到达指令流水线的执行阶段之前刷新了指令流水线,则不会发生中止异常。数据中止异常发生在加载或存储指令执行时,并且是在尝试读取或写入数据之后发生的。
如果中止是由于指令流的执行或尝试执行而产生的,则中止被描述为同步的,并且返回地址将提供导致该中止的指令的详细信息。异步的中止不是由执行指令生成,异步中止的返回地址可能不提供导致中止的原因的信息。
在ARMv8-A中,指令和数据中止是同步的。异步异常是IRQ/FIQ和系统错误(SError)。参见第10-7页的同步和异步异常。
复位:
复位作为一个特殊向量拥有最高的异常等级。当ARM处理器异常时跳转到的指令位置。此向量使用IMPLEMENTATION DEFINED地址。RVBAR_ELn包含此复位向量地址,其中n是实现的最高异常级别的编号。
所有核心都有复位输入,复位后立即发生复位异常。它是最高优先级的异常,不能被屏蔽。此异常用于在上电后在内核上执行初始化代码。
异常生成指令:
执行某些指令可能会产生异常。执行此类指令通常是为了从以更高权限级别运行的软件请求服务:
如果生成的异常是由于EL0处的指令获取而生成的,则将其视为EL1的异常,除非HCR_EL2.TGE位设置为非安全状态,在这种情况下,它被带到EL2。
如果异常是在任何其他异常级别上获取指令的结果,则异常级别保持不变。
在本书前面,我们看到ARMv8-A体系结构有四个异常级别。处理器执行只能通过获取异常或从异常返回来在异常级别之间切换。当处理器从较高的异常级别移动到较低的异常级别时,执行状态可以保持不变,也可以从AArch64切换到AArch32。同样,当从较低的异常级别移动到较高的异常级别时,执行状态可以保持不变或从AArch32切换到AArch64。
图10-1展示了运行应用程序时发生的异常相关的程序流。处理器跳转到一个包含每个异常类型的向量表。向量表根据各个异常的标识来调用到各个异常的处理函数。此代码完成执行,然后返回到高级处理程序,高级处理程序随后执行ERET指令以返回到应用程序。
第4章描述了处理器的当前状态如何存储在单独的PSTATE字段中。如果发生异常,PSTATE信息将保存在程序状态存储寄存器(SPSR_ELn)中,该寄存器分为SPSR_EL3、SPSR_EL2和SPSR_EL1。
SPRSR.M域(bit4)用于记录指令状态,0表示AArch64状态,1表示AArch32状态。
异常位掩码位(DAIF)允许屏蔽异常事件。当设置位时,不会发生异常。
D:调试异常掩码
A:错误中断进程状态掩码,例如,异步外部中止
I:IRQ中断进程状态掩码
F:FIQ中断进程状态掩码
SPSel字段选择应使用当前异常级别的堆栈指针还是使用SP_EL0。可以在任何异常级别完成,EL0除外。本章后面将对此进行讨论。
设置IL字段时,会导致下一条指令的执行触发异常。它用于非法执行返回,例如,在为AArch32配置EL2时,尝试将其作为AArch64返回到EL2。
第18章讲述(Software Stepping) SS位,调试器使用它执行一条指令,然后在接下来的指令执行调试异常。
其中一些单独的字段(CurrentEL、DAIF、NZCV等)在发生异常时(返回时则相反)被复制到SPSR_ELn中的缩略格式中。
当发生导致异常的事件时,处理器硬件会自动执行某些操作。更新SPSR_ELn(其中n是异常发生的异常级别),以存储在异常结束时正确返回所需的PSTATE信息。PSTATE更新以反映新处理器状态(这可能意味着异常级别提高,或者保持不变)。异常结束时使用的返回地址存储在ELR_ELn中。
请记住,寄存器名称上的_ELn后缀表示这些寄存器的多个副本存在于不同的异常级别。例如,SPSR_EL1是与SPSR_EL2不同的物理寄存器。此外,在同步或错误异常的情况下,ESR_ELn也会用一个指示异常原因的值进行更新。
必须通过软件告知处理器何时从异常返回。这是通过执行ERET指令来完成的。这将从SPSR_ELn恢复预异常PSTATE,并通过从ELR_ELn恢复PC将程序执行返回到原始位置。
我们已经看到了SPSR如何记录异常返回所需的状态信息。现在我们来看一下用于存储程序地址信息的链接寄存器。该体系结构为函数调用和异常返回提供了单独的链接寄存器。
正如我们在第6章A64指令集中看到的,寄存器X30(与RET指令一起)用于从子程序返回。每当我们使用链接指令(BL或BLR)执行分支时,它的值都会随着要返回的指令的地址而更新。
ELR_ELn寄存器用于存储异常的返回地址。该寄存器中的值(如我们所见,实际上是几个寄存器)在进入异常时自动写入,并作为执行用于从异常返回的ERET指令的效果之一写入PC。
ELR_ELn包含特定异常类型的返回地址。对于某些异常,这是生成异常的指令之后的下一条指令的地址。例如,当执行SVC(系统调用)指令时,我们只希望返回应用程序中的下一条指令。在其他情况下,我们可能希望重新执行生成异常的指令。
对于异步异常,ELR_ELn指向由于中断而未执行或未完全执行的第一条指令的地址。
例如,如果在中止同步异常后需要返回指令,则允许处理程序代码修改ELR_En。
ARMv8-A模型比ARMv7-A中使用的模型要简单得多,在ARMv7-A中,出于向后兼容性的原因,当从某些类型的异常返回时,需要从链接寄存器值中减去4或8。
除了SPSR和ELR寄存器外,每个异常级别都有自己的专用堆栈指针寄存器。它们被命名为SP_EL0、SP_EL1、SP_EL2和SP_EL3。这些寄存器用于指向一个专用堆栈,例如,该堆栈可用于存储被异常处理程序损坏的寄存器,以便在返回原始代码之前将其恢复为原始值。
处理程序代码可以从使用SP_ELn切换到SP_EL0。例如,SP_EL1可能指向一块内存,该内存包含一个内核可以保证始终有效的小堆栈。SP_EL0可能指向更大的内核任务堆栈,但不能保证不会溢出。此切换通过写入[SPSel]位来控制,如以下代码所示:
MSR SPSel, #0 // switch to SP_EL0
MSR SPSel, #1 // switch to SP_ELn
在AArch64中,异常可以是同步的,也可以是异步的。如果异常是由于指令流的执行或尝试执行而生成的,并且返回地址提供了导致异常的指令的详细信息,则将异常描述为同步异常。异步异常不是通过执行指令生成的,而返回地址可能并不总是提供导致异常的详细信息。
异步异常的来源是IRQ(正常优先级中断)、FIQ(快速中断)或SError(系统错误)。系统错误有许多可能的原因,最常见的是异步数据中止(例如,将脏数据从缓存线写回外部内存而触发的中止)。
同步异常的来源有很多:
发生同步异常的原因有很多:
此类异常可能是操作系统正常操作的一部分。例如,在Linux中,当任务希望请求分配新内存页时,这是通过MMU中止机制处理的。
在ARMv7-A体系结构中,预取指中止、数据中止和未定义异常是相互独立的项。在AArch64中,所有这些事件都会生成同步中止。然后,异常处理程序可以读取综合寄存器和FAR寄存器,以获得必要的信息来区分它们(稍后将详细描述)
寄存器用于向异常处理程序提供有关同步异常原因的信息。异常综合寄存器(ESR_ELn)提供了有关异常原因的信息。故障地址寄存器(FAR_ELn)保存所有同步指令和数据中止以及对齐故障的故障虚拟地址。
异常链接寄存器(ELR_ELn)保存导致中止数据访问(用于数据中止)的指令的地址。这通常在内存故障后更新,但在其他情况下设置,例如,通过分支到未对齐的地址。
如果使用AArch32将异常从异常级别提取到使用AArch64的异常级别,并且该异常写入与目标异常级别关联的故障地址寄存器,则FAR_ELn的前32位都设置为零。
对于实现EL2(虚拟机监控程序)或EL3(安全内核)的系统,同步异常通常在当前或更高的异常级别中进行。异步异常可以(如果需要)路由到更高的异常级别,由虚拟化程序或安全内核处理。SCR_EL3寄存器指定将哪些异常路由到EL3,同样,HCR_EL2指定将哪些异常路由到EL2。有单独的位允许单独控制IRQ、FIQ和SError的路由。
某些指令或系统功能只能在特定的异常级别执行。如果以较低的异常级别运行的代码需要执行特权操作,例如,当应用程序代码从内核请求功能时。一种方法是使用SVC指令。这允许应用程序生成异常。
参数可以在寄存器中传递,也可以在系统调用中编码。
我们在前面看到了如何使用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中进行SVC调用,它仍然会在相同的异常级别上触发同步异常,并且该异常级别的处理程序可以决定如何响应。
未分配的指令会导致AArch64中的同步中止。当处理器执行以下操作之一时,将生成此异常类型:
异常综合寄存器ESR_ELn包含允许异常处理程序确定异常原因的信息。它仅针对同步异常和错误进行更新。由于这些中断处理程序通常从通用中断控制器(GIC)中的寄存器获取状态信息,因此不会对IRQ或FIQ进行更新。(参见第10-17页的通用中断控制器。)寄存器的位编码为:
当发生异常时,处理器可能会更改执行状态(从AArch64更改为AArch32)或保持相同的执行状态。
例如,外部源可能在执行以AArch32模式运行的应用程序时生成IRQ(中断)异常,然后在以AArch64模式运行的OS内核中执行IRQ处理程序。
SPSR包括要返回的执行状态和异常级别。当发生异常时,处理器会自动设置该值。但是,每个异常级别中异常的执行状态控制如下:
考虑一个运行在EL0中的应用程序,它被IRQ中断,如图10-5所示。内核IRQ处理程序在EL1上运行。
处理器确定在接受IRQ异常时要设置的执行状态。它通过查看控制寄存器的RW位来实现这一点,该位的异常级别高于处理异常的级别。因此,在示例中,在EL1中发生异常的地方,它是由HCR_EL2.RW控制处理程序的执行状态。
当发生异常时,异常级别可能会保持不变,也可能会变得更高。正如我们已经看到的,EL0永远不会出现异常。
同步异常通常在当前或更高的异常级别中进行。但是,异步异常可以路由到更高的异常级别。对于安全代码,SCR_EL3指定将哪些异常路由到EL3。对于虚拟机监控程序代码,HCR_EL2指定要路由到EL2的异常。
在这两种情况下,都有单独的位来控制IRQ、FIQ和SError的路由。处理器仅将异常放入其路由到的异常级别。异常级别永远不会因为发生异常而降低。中断总是在发生中断的异常级别被屏蔽。
当从AArch32到AArch64执行异常时,有一些特殊的注意事项。AArch64处理程序代码可能需要访问AArch32寄存器,因此体系结构定义了允许访问AArch32寄存器的映射。
AArch32寄存器R0到R12作为X0到X12访问。各种AArch32模式下的SP和LR的临时缓存通过X13至X23访问,而存储R8至R12 FIQ寄存器则通过X24至X29访问。这些寄存器的位[63:32]在AArch32状态下不可用,并且是0或者是AArch64中写入的最后一个值。没有体系结构保证它的值。因此,通常以W寄存器的形式访问寄存器。
当异常发生时,处理器必须执行与异常对应的处理程序代码。内存中存储处理程序的位置称为异常向量。在ARM体系结构中,异常向量存储在一个表中,称为异常向量表。每个异常级别都有自己的向量表,即EL3、EL2和EL1各有一个向量表。该表包含要执行的指令,而不是一组地址。个别异常的向量位于表开头的固定偏移处。每个表基的虚拟地址由基于向量的地址寄存器VBAR_EL3、VBAR_EL2和VBAR_EL1设置。
向量表中的每个条目都有16条指令长。这本身就代表了与ARMv7相比的重大变化,ARMv7中的每个条目都是4字节。ARMv7向量表的这种间距意味着每个条目几乎总是内存中其他位置的实际异常处理程序的某种形式的分支。在AArch64中,向量的间距更大,因此处理程序可以直接写入向量表。
表10-2显示了其中一个矢量表。基址由VBAR_ELn给出,然后每个条目都有一个与该基址的已定义偏移量。每个表有16个条目,每个条目的大小为128字节(32条指令)。该表实际上由4组4个条目组成。
使用哪个条目取决于许多因素:
ARM通常使用中断来表示中断信号。在ARM A系和R系处理器上,这意味着外部IRQ或FIQ中断信号。体系结构没有指定如何使用这些信号。FIQ通常用于安全中断源。在早期的体系结构版本中,FIQ和IRQ用于表示高中断优先级和标准中断优先级,但在ARMv8-A中并非如此。
当处理器对AArch64执行状态发生异常时,所有PSTATE中断掩码将自动设置。这意味着将禁用其他异常。如果软件要支持嵌套异常,例如,允许高优先级中断中断低优先级的处理,则软件需要显式重新启用中断。
如下:MSR DAIFClr, #imm
该立即数值实际上是一个4位字段,因为还有以下掩码:PSTATE.A (for SError)、PSTATE.D (for Debug)
示例汇编语言IRQ处理程序可能如下所示:
但是,从性能角度来看,以下顺序可能更可取:
嵌套处理程序需要一些额外的代码。它必须在堆栈上保留SPSR_EL1和ELR_EL1的内容。我们还必须在确定(并清除)中断源后重新启用IRQ。但是(与ARMv7-A不同),由于子例程调用的链接寄存器与异常的链接寄存器不同,我们避免了对LR或运行模式进行任何特殊处理。
ARM提供了一个标准中断控制器,可用于ARMv8-a系统。该中断控制器的编程接口在GIC体系结构中定义。GIC架构规范有多个版本。本文档主要介绍版本2(GICv2)。ARMv8-A处理器通常连接到GIC,例如GIC-400或GIC-500。通用中断控制器(GIC)支持在多核系统的核心之间路由软件生成的、专用的和共享的外围中断。
GIC体系结构提供寄存器,可用于管理中断源和行为,以及(在多核系统中)将中断路由到各个核。它使软件能够屏蔽、启用和禁用来自各个源的中断,对各个源(在硬件中)进行优先级排序,并生成软件中断。GIC接受在系统级断言的中断,并可以向其连接的每个核心发送信号,这可能会导致IRQ或FIQ异常。
从软件角度来看,GIC有两个主要功能块:
分派器:
系统中的所有中断源都与之连接。分发服务器有寄存器来控制各个中断的属性,如优先级、状态、安全性、路由信息和启用状态。分发服务器通过连接的CPU接口确定将哪个中断转发到内核。
CPU接口:
内核通过它接收中断。CPU接口承载寄存器,用于屏蔽、识别和控制转发到该内核的中断状态。系统中的每个核心都有一个单独的CPU接口。
中断在软件中由一个称为中断ID的数字标识。中断ID唯一地对应于中断源。软件可以使用中断ID来识别中断源,并调用相应的处理程序为中断提供服务。提供给软件的确切中断ID由系统设计决定,中断可以有多种不同类型:
中断可以是边缘触发(当GIC检测到相关输入上的上升沿时被认为是断言的,并保持断言直到清除)或电平敏感(仅当GIC的相关输入为高时被认为是断言的)。
中断可以处于多种不同的状态:
中断可以传送到的优先级和核心列表都在分发服务器中配置。
外围设备向分发服务器断言的中断处于挂起状态(如果已处于活动状态,则为活动和挂起)。分发服务器确定可传送到内核的最高优先级挂起中断,并将其转发到内核的CPU接口。在CPU接口处,中断依次被发信号给内核,此时内核接受FIQ或IRQ异常。
核心执行异常处理程序作为响应。处理程序必须从CPU接口寄存器查询中断ID,并开始为中断源提供服务。完成后,处理程序必须写入CPU接口寄存器以报告处理结束。
对于给定的中断,典型的顺序是:
分发服务器提供报告不同中断ID当前状态的寄存器。
在多核/多处理器系统中,单个GIC可由多个核共享(GICv2中最多八个)。GIC提供寄存器来控制SPI的目标核心。此机制使操作系统能够跨内核共享和分发中断,并协调活动。
有关GIC行为的更多详细信息,请参见各处理器类型的TRMs和ARM通用中断控制器体系结构规范。
GIC作为内存映射外围设备进行访问。所有内核都可以访问公共分发服务器,但CPU接口是有备份的,也就是说,每个内核使用相同的地址访问自己的专用CPU接口。一个核心不可能访问另一个核心的CPU接口。
分发服务器承载许多寄存器,您可以使用这些寄存器配置各个中断的属性。这些可配置属性包括:
分配器还提供优先级掩蔽,通过该掩蔽防止低于某个优先级的中断到达内核。分发服务器在确定是否可以将挂起的中断转发到特定的内核时使用此选项。
每个内核上的CPU接口有助于微调该内核上的中断控制和处理
分配器和CPU接口在复位时都被禁用。GIC必须在复位后初始化,然后才能将中断传送到核心。
在分发服务器中,软件必须配置优先级、目标、安全性并启用个别中断。分配器随后必须通过其控制寄存器(GICD_CTLR)启用。对于每个CPU接口,软件必须编程优先级掩码和抢占设置。
每个CPU接口块本身必须通过其控制寄存器(GICD_CTLR)启用。这为GIC向核心提供中断做好了准备。
在内核中预期中断之前,软件通过在向量表中设置有效的中断向量,清除PSTATE中的中断掩码位,并设置路由控制来准备内核接受中断。
通过禁用分发服务器,可以禁用系统中的整个中断机制。可以通过禁用CPU接口来禁用向单个内核的中断传递。还可以在分发服务器中禁用(或启用)单个中断。
为了使中断到达内核,必须启用各个中断、分配器和CPU接口。中断还需要具有足够的优先级,即高于内核的优先级掩码。
当内核接受中断时,它跳转到从向量表获得的顶级中断向量并开始执行。
顶级中断处理程序从CPU接口块读取中断确认寄存器,以获取中断ID。
除了返回中断ID外,读取还会导致在分发服务器中将中断标记为激活。一旦中断ID已知(识别中断源),顶级处理程序现在可以分派特定于设备的处理程序来服务中断。
当特定于设备的处理程序完成执行时,顶级处理程序将相同的中断ID写入CPU接口块中的中断结束(EoI)寄存器,表示中断处理结束。
除了删除活动状态(使最终中断状态为非活动或挂起(如果状态为活动和挂起))之外,这还使CPU接口能够将更多挂起的中断转发到内核。这就结束了对单个中断的处理。
同一个内核上可能有多个中断等待服务,但CPU接口一次只能发送一个中断信号。顶级中断处理程序可以重复上述顺序,直到它读取特殊中断ID值1023为止,这表明在这个核心上没有更多的挂起中断。这个特殊的中断ID称为伪中断ID。
伪中断ID是保留值,不能分配给系统中的任何设备。当顶级处理程序读取伪中断ID时,它可以完成其执行,并准备内核在执行中断之前恢复其正在执行的任务。
通用中断控制器(GIC)通常管理来自多个中断源的输入,并将其分配给IRQ或FIQ请求。
注:1020到1023为特殊目的的中断保留。1023-伪中断,在pending状态中没有使能的INTID,或在pending状态中所有的INTID都不足。当轮询IAR,该值表明没有有效的中断来应答。