ARMv8-A编程指导之异常处理

        严格来讲,一个interrupt会中断软件执行的流程。但是,以ARM术语来看,它实际上是一个exception。Exception为要求特权软件采取措施来保证系统平滑功能的条件或系统event。对于每个exception类型有一个与之相关的exception handler。一旦exception被处理,特权软件让core准备唤醒在exception之前的行为。

        存在以下类型的exception:

Interrupts 存在两种类型的中断:IRQ和FIQ。

        FIQ比IRQ优先级更高。这两种类型的exception通常与core中输入pin对应。假定中断没有禁用,当当前指令完成执行时,外部硬件发出中断请求线并发出对应的exception类型。

        FIQ和IRQ为发往core的物理信号,当发出后,如果当前被使能,core将采取对应的exception。在大多数系统中,各种中断源通过一个中断控制器相连。中断控制器仲裁并提升中断优先级,并提供一个串行单个信号,该信号将连接到core的FIQ或IRQ信号。

        因为在任意时刻IRQ和FIQ中断的产生不会直接与core执行的软件相关,它们被分类为asynchronous exception。

Aborts 指令获取失败或数据访问失败时会产生aborts。它们可以来自于外部内存系统对内存访问给出一个错误回复(表明给定的地址在系统中没有对应真实的内存)。

        另外,core的MMU也可以产生aborts。OS可以使用MMU abort来动态分配内存给应用。

        流水线中的指令当被获取时可以被指标为aborted。仅当core尝试去执行指令时才会产生指令abort exception。在指令执行之前exception产生。如果在aborted指令到达流水线的执行阶段之前流水线被冲刷,abort exception不会产生。由于load或store指令,数据abort exception产生,在尝试数据读或写之后异常考虑产生。

        如果abort的产生是由于执行或尝试执行指令流,该abort被认为synchronous,返回的地址提供指令的详细信息。

        一个asynchronous abort并不是由正在执行的指令产生,且返回的地址通常也不能提供造成abort的细节。在ARMv8-A中,指令和数据abort为synchronous。Asynchronous exception为IRQ/FIQ和SError。

Reset Reset被认为是实现的最高异常级别的特殊vector。它是当发出异常时,ARM处理器跳转到的指令位置。vector使用实现定义的地址。

        RVBAR_ELn包含reset vector地址,其中n表示最高异常级别。

        所有的core都有一个reset输入,在它们复位时立即采用reset exception。它为最高优先级的exception且不能屏蔽。在上电后exception用来执行代码初始化core。

异常产生指令 某些指令的执行能够产生异常。这些指令通常用于来自更高权限级别的软件的请求服务。

  1. SVC指令使能用户模式程序请求一个OS服务
  2. HVC指令使能guest OS请求hypervisor服务
  3. SMC指令使能Normal world请示安全world服务

        如果由于EL0的指令获取产生异常,该异常作为exception被陷入到EL1,如果在非安全状态HCR_EL2.TGE没有被设置。如果被设置,该异常会被陷入到EL2。

        如果由于其他异常级别的指令获取产生异常,异常级别保持不变。

        我们知道ARMv8-A架构有四个异常级别。处理器执行仅通过异常的陷入或返回在不同异常级别之间变化。当处理器从一个更高的异常级别到一个更低的异常级别时,执行状态可以保持不变,或它将从AArch64切换到AArch32。当处理器从一个更低的异常级别到一个更高的异常级别时,执行状态保持不变,或从AArch32切换到AArch64。

         上图描述了当运行应用时产生异常相关的程序流程。处理器跳到包含每个异常类型项的vector table。Vector table包含下发代码,这些代码通常会区分异常产生的原因,选择并调用合适的函数处理它们。代码完成执行然后返回通过执行ERET指令返回到应用。

1 异常处理寄存器

        第4章描述了处理器的当前状态是如何被保存在PSTATE域中。如果异常产生,PSTATE信息被保存在SPSR_ELn中。

        SPSR.M域用于记录执行状态(0表明AArch64,1表明AArch32)。

        异常位屏蔽位DAIF允许异常事件被屏蔽。当这些位被设置时将不产生异常。

D 调试异常屏蔽

A SError中断处理状态屏蔽,比如异常外部abort

I IRQ中断处理状态屏蔽

F FIQ中断处理状态屏蔽

        SPSel域选择当前异常级别Stack Pointer或SP_EL0是否被使用。这可以在任意异常级别,除了EL0。

        IL域,当被设置时,会导致下一条指令的执行会触发一个异常。它在非法执行返回时被使用,比如当它被配置为AArch32时,它尝试作为AArch64返回到EL2。

        SS域在第18章介绍。调试者使用它来执行单个指令并在下列指令时产生调试异常。

        一些单独域(CurrentEL, DAIF, NZCV)在产生异常时会被拷贝到SPSR_ELn。

        当造成异常的event产生时,处理器硬件自动执行某种操作。SPSP_ELn被更新用来保存PSTATE信息,并要求在异常处理的结尾返回。PSTATE被更新反映最新处理器状态。在异常处理结尾使用的返回地址被保存在ELR_ELn。

        记住_ELn结尾的寄存器名表明在不同的异常级别存在不同的寄存器拷贝。比如,SPSR_EL1与SPSR_EL2是不同的物理寄存器。另外,对于synchronous或SError异常,ESR_ELn也被更新表明异常的原因。

        当从异常返回时由软件告诉处理器。这是由ERET指令完成的。这将恢复之前从SPSR_ELn的PSTATE,并通过从ELR_ELn恢复PC来返回到原来的位置。

        我们已经看到SPSR是如何记录异常返回的必要状态信息。我们将看到Link寄存器用于保存程序地址信息。架构为函数调用提供Link寄存器。

        在第6章我们看到A64指令集,寄存器X30用于从子路径返回。当我们使用BL或BLR执行分支时,它的值由指令的地址更新。

        ELR_ELn寄存器用于从异常返回地址。在该寄存器中的值被自动写到一个异常中并写入到PC中,它作为ERET指令的效果用于返回异常。

        ELR_ELn包含某些异常类型的返回地址。对于相同的异常,它为产生异常指令的下一个指令的地址。比如,当执行SVC指令时,我们希望返回到应用的下一个指令。其它情况,我们可能希望重新执行产生异常的指令。

        对于异步异常,ELR_ELn指向还没有执行的第一个指令的地址。如果在产生一个异步异常之后有必要返回指令,处理代码允许修改ELR_ELn。ARMv8-A架构明显比ARMv7-A简单。

        另外对于SPSR和ELR寄存器,每个异常级别有自己的Stack Pointer寄存器。它们被命名为SP_EL0,SP_EL1, SP_EL2, SP_EL3。这些寄存器用于指向stack,用于保存寄存器,因此它们在返回原始代码之前恢复他们的原始值。

2 同步和异步异常

        在AArch64中,异常可能为同步或异步。如果它由于执行或尝试执行指令流而产生异常,该异常被描述为synchronous,且返回地址提供造成异常指令的细节。asynchronous异常并不是由执行指令产生,且返回地址并不能提供造成指令的细节。

        asynchronous异常源为IRQ或FIQ或SError。系统error有很多可能原因,最常见的asynchronous Data Aborts(比如,来自cache line到外部内存的脏数据的回写触发abort)。

        Synchronous异常源有如下:

  1. 来自MMU的指令abort。比如,通过读取标记为Execute Never的内存位置的指令
  2. 来自MMU的数据abort。比如,权限失败或对齐检查
  3. SP和PC对齐检查
  4. Synchronous外部abort。比如,当读取转换表时产生abort
  5. 未分配指令
  6. 调试异常

2.1 Synchronous abort

        Synchronous异常由于一些可能的原因产生:

  1. 来自MMU的abort。比如,权限失败或内存区域标记为Access Flag Fault
  2. SP和PC对齐检查
  3. 未分配指令
  4. SVC, SMC和HVC

        这些异常可能为OS的正常操作的一部分。比如,在LINUX中,当一个task想要请求一个新的内存页分配,这是通过MMU abort机制来处理的。

        在ARMv7-A架构中,prefetch abort,Data abort和未定义异常为分开的事务。在AArch64中,所有的event产生一个synchronous abort。异常处理读取syndrome和FAR寄存器来获取信息。

2.2 处理synchronous异常

        寄存器将synchronous异常的原因提供给异常处理。ESR_ELn寄存器给出异常的原因信息。FAR_ELn寄存器给出所有synchronous指令和Data abort以及对齐fault的虚拟地址。

        ELR_ELn寄存器保持造成数据访问的指令地址。在一个内存fault后进行更新,比如由非对齐的地址分支设置。

        如果异常从使用AArch32的异常级别陷入到使用AArch64的异常级别,异常写目标的异常级别的FAR寄存器,FAR_ELn的top 32位被设置为0。

        对于实现了EL2或EL3时,synchronous异常被陷入到当前或更高异常级别。Asynchronous异常可以被路由到一个更高异常级别,由hypervisor或安全kernel处理。SCR_EL3寄存器指定哪个异常被路由到EL3,HCR_EL2指定哪个异常被路由到EL2。存在单独的位允许控制IRQ, FIQ和SError的路由控制。

2.3 系统调用

        一些指令或系统函数可以被执行在特定的异常级别。如果运行在更低的异常级别的代码需要发出一个权限操作,比如当应用代码从kernel请求功能,一种方法是使用SVC指令。这允许应用产生一个异常。通过寄存器传递参数,或由系统调用编码。

2.4 EL2/EL3的系统调用

        前面我们知道SVC是如何从EL0的用户应用调用到EL1的kernel。HVC和SMC系统调用指令以类似的方式将处理器移到EL2和EL3。当处理运行在EL0时,它不能直接调用到EL2或EL3。这仅可能从EL1或更高级别。应用必须使用SVC调用到kernel,然后允许kernel调用到更高异常级别。

        来自OS kernel, 软件使用HVC指令调用到EL2,或使用SMC指令调用到EL3。如果处理器实现了EL3,需要提供EL2陷入的SMC指令。如果没有EL3,SMC没有分配且在当前异常级别被触发。

        类似的,来自EL2,程序使用SMC指令调用到EL3。如果当在EL2或EL3你使用SVC调用时它将在相同的异常级别上产生synchronous异常,异常级别的处理决定如何回复。

 2.5 未分配的指令

        未分配的指令在AArch64中会导致synchronous abort。当处理器执行以下时产生异常类型:

  1. 没有分配的指令opcode
  2. 请求高于当前异常级别的指令
  3. 被禁用的指令
  4. 当PSTATE.IL被设置时的任何指令

2.6 ESR寄存器

        ESR寄存器ESR_ELn,包含用于决定异常的原因的异常处理的信息。仅在synchronous异常和SError时更新。对于IRQ和FIQ时不会更新,因此这些中断通常由GIC中寄存器获取状态信息。寄存器的位如下:

  1. ESR_ELn的Bit[31:26]表明异常分类,它允许handler区分各种异常原因(如未分配指令,从MCR/MRC到CP15的异常,FP操作的异常,SVC/HVC/SMC执行,Data Abort,和对齐异常)
  2. Bit[25]表明陷入指令的长度(0为16bit指令,1为32bit指令)
  3. Bit[24:0]为ISS域,包含异常类型的特定信息。比如,当系统调用指令SVC/HVC/SMC执行时,该域包含与opcode相关的当前值,比如SVC 0x123456。

由异常引发的执行状态和异常级别的变化

        当异常产生时,处理器可能会修改异常级别或保持相同的执行状态。比如,一个外部源在AArch32模式下执行应用时产生了一个IRQ中断,这里让运行在AArch64模式中OS内核执行IRQ handler。

        SPSR包含执行状态和将返回的异常级别。当异常产生时这自动由处理器设置。但是,每个异常级别的执行状态如下控制:

  1. 最高的异常级别的reset执行状态通常由一个硬件配置输入。这是不固定的,因为在运行时我们有RMR_ELn寄存器来修改最高的异常级别的执行状态。
  2. 对于EL2和EL1,执行状态由SCR_EL3.RW和HCR_EL2.RW位控制。SCR_EL3.RW在EL3中并设置EL2状态。HCR_EL2.RW位可能在EL2或EL3,设置EL1/0的状态。
  3. 不可能在EL0产生异常。

        考虑运行在EL0的一个应用,如图10-5所示被一个IRQ打断。内核IRQ handler运行在EL1。当处理IRQ异常时处理器决定设置哪个执行状态。通过异常级别的控制寄存器的RW位来完成。因此,在这个例子中,当异常到达EL1时,由HCR_EL2.RW控制handler的执行状态。

        我们必须考虑异常进入哪个异常级别。当异常再次产生时,异常级别可能保持相同,或它到达更高异常级别。异常不会到达EL0。

        synchronous异常通常会进入到当前或更高异常级别。但是,asynchronous异常可以被路由到更高异常级别。对于源码,SCR_EL3指定哪个异常被路由到EL3。对于hypervisor代码,HCR_EL2指定哪个异常被路由到EL2。

        对于这两种情况,存在单独的位来控制IRQ/FIQ/SError的控制路由。处理器仅将异常进入到它要路由的异常级别。产生异常不会导致异常级别下降。当中断产生时中断通常在异常级别里被屏蔽。

        当异常从AArch32到AArch64时,这里需要特殊的考虑。AArch64 handler代码可能要求访问AArch32寄存器且架构定义允许访问AArch32寄存器的映射。

        AArch32寄存器R0到R12以X0到X12被访问。在AArch32模式下SP和LR的各种备份版本通过X13到X23访问,备份的R8到R12 FIQ寄存器以X24到X29被访问。这些寄存器的bit[63:32]在AArch32状态无效且包含或0或最后写入的值。没有架构保证是哪个值。通常作为W寄存器访问寄存器。

 4 AArch64异常表

        当异常产生时,处理器必须执行与异常对应的handler代码。handler保存在内存中的位置称为异常向量。在ARM架构中,异常向量被保存在一个称为异常向量表中。每个异常级别存在属于自己的异常表,即EL3/EL2/EL1分别存在一个。该表包含需要执行的指令,而不是一组地址。每个异常的地址在表开始位置的固定偏移处。每个表基地址的虚拟地址由VBAR_ELn设置。

        向量表中每个项为16指令长度。与ARMv7相比,这是一个明显的变化,ARMv7的每个项为4byte。在AArch64中,向量的大小更宽,因此top-level handler可以在向量表中直接写入。

        表10-2显示了一个向量表。基地址由VBAR_ELn指定,然后每个项对于基地址有固定的偏移。每个表有16项,每个项有128byte。该表包含4组4个项。

  1. 异常的类型(SError,FIQ,IRQ或Synchronous)
  2. 如果异常产生在相同的异常级别,使用Stack Pointer(SP0或SPx)
  3. 如果异常产生在更低的异常级别,下一个更低的执行状态

Address

Exception type

Description

VBAR_ELn + 0x000

Synchronous

Current EL with SP0

         +0x080

IRQ/vIRQ

         +0x100

FIQ/vFIQ

         +0x180

SError/vSError

VBAR_ELn + 0x200

Synchronous

Current EL with SPx

         +0x280

IRQ/vIRQ

         +0x300

FIQ/vFIQ

         +0x380

SError/vSError

VBAR_ELn + 0x400

Synchronous

Lower EL using AArch64

         +0x480

IRQ/vIRQ

         +0x500

FIQ/vFIQ

         +0x580

SError/vSError

VBAR_ELn + 0x600

Synchronous

Lower EL using AArch32

         +0x680

IRQ/vIRQ

         +0x700

FIQ/vFIQ

         +0x780

SError/vSError

        考虑一个例子可能使这些更容易理解。

        如果内核代码执行在EL1且产生了IRQ中断,产生一个IRQ异常。这个中断并不与hypervisor或安全环境相关,且它也在内核中被处理,在SP_EL1,SPSel被设置,因此你使用SP_EL1。异常因此产生于地址VBAR_EL1+0x280。

        在ARMv8-A架构中缺少LDR PC, [PC, #offset],你必须使用更多指令使能将要读取的目的地址。向量空间大小的选择设计是为了避免未使用的向量的cache line造成的cache污染。Reset地址是一个完全独立的地址,它为实现定义,通常core中的硬接线配置决定。这个地址在RVBAR_EL1/2/3寄存器可见。

        每个异常有一个单独的异常向量,或来自当前异常级别,或来自更低异常级别,由OS或Hypervisor来决定更低异常级别的AArch64和AArch32状态。SP_ELn用于更低级别产生的异常。但是,软件在handler中能够切换到SP_EL0。

5 中断处理

        ARM通常使用中断来表示中断信号。在ARM-A或ARM-R处理器中,这意味着外部的IRQ或FIQ中断信号。架构不会指定如何使用这些信号。FIQ通常为安全中断源保留。在早期的版本中,FIQ和IRQ被用于表明高和标准中断优先级,但在ARMv8-A中并不是这样。

        当处理器将一个异常带到AArch64执行状态时,PSTATE中断屏蔽自动设置。这意味着禁用了更多的异常。如果软件支持nested异常,比如允许一个更高优先级中断来中断一个低优先级源,软件需要明确重新使能中断。

        对于下列指令:

MSR DAIFClr, #imm

        该immediate value实际上为4bit域,因为它也可以屏蔽:

  1. PSTATE.A
  2. PSTATE.D

6 通用中断控制器

        ARM对ARMv8-A系统提供了标准的中断控制。中断控制器的编程接口在GIC架构中定义。GIC支持多Core系统中软件产生中断,私有中断和共享外设中断在core之间的路由。

        GIC架构提供寄存器用于管理中断源和行为对单个Core进行中断的路由。它使能软件进行屏蔽,使能和禁用中断,提高单独源的优先级并产生软件中断。GIC接受系统级产生的中断,并将它们发送给每个core,并导致IRQ或FIQ异常。

        从软件角度看,一个GIC主要有两个功能模块:

Distributor 系统中所有的中断源都连接到distributor上。它有寄存器控制单个中断的属性,如优先级,状态,安全性,路由信息和使能状态。Distributor决定中断通过CPU接口被发送到哪个core上。

CPU Interface core通过CPU Interface接受中断。CPU接口有寄存器用于屏蔽,区分和控制中断的状态。在系统中每个core有一个独立的CPU接口。

        在软件中中断通过interrupt ID区分中断。一个interrupt ID唯一对应一个中断源。软件可以使用interrupt ID来区分中断源并调用对应的handler来处理中断。Interrupt ID由系统设计决定。

        中断可以分为如下几类型:

SGI 它是由软件写Distributor寄存器GICD_SGIR产生。通常用于core间通信。SGI可以产生到所有core,或系统中一组特定的core。Interrupt ID 0-15保留用于SGI。软件设置interrupt ID产生中断。

PPI 它为全局外部中断,Distributor可以将它路由到一个特定的core。Interrupt ID 16-31保留用于PPI。这些中断源对于core是私有的,对于每个core它并不是相同的中断源,比如,基于core的timer。

SPI 它由外设产生,GIC可以将其路由到超过一个core。Interrupt ID 32-1020用于SPI。SPI用于从各种外设发出中断。

LPI 它为基于message的中断,可以被路由到特定的core。GICv2或GICv1不支持LPI。

        中断或为边沿中断,或为电平中断。

        一个中断有如下不同状态:

  1. Inactive:这意味着中断当前没有发出
  2. Pending:这意味着中断源已发出,但等待core进行处理。Pending中断将被发往CPU接口后面将发给core
  3. Active:这意味着core acknowledge中断,当前正在处理
  4. Active and pending:core正在处理中断,这里也有pending中断产生

        中断被传递到的Core的优先级和列表被配置在Distributor中。发往Distributor的中断处于pending状态。Distributor决定最高优先级pending中断,这些中断被传递给core并将其传递给core的CPU interface。对于CPU interface,中断被发往core,这时core产生FIQ或IRQ异常。

        core执行异常handler。Handler必须从CPU接口寄存器查询interrupt ID并开始处理中断。当完成处理,handler必须写CPU接口寄存器来报告处理结束。

        对于给定的中断,典型的时序为:

Inactive -> pending

当外设发出中断时

Pending -> active

当handler acknowledge 中断

Active -> inactive

当完成中断处理

        Distributor提供寄存器报告不同中断ID的当前状态。

        在多core/多处理器系统中,单个GIC可能被多个core共享。GIC提供寄存器控制SPI发往哪个core。这个机制使OS能够在不同core间共享和分发中断。

6.1 配置

        GIC可以以内存映射外设被访问。所有core可以访问公共Distributor,每个core使用相同的地址访问它自己私有的CPU interface。一个core不能访问其他core的CPU interface。

        Distributor存在一组寄存器,你可能使用它们配置单个中断的属性。这些配置属性:

  1. 中断优先级GICD_IPRIORITY<n>。Distributor使用它来决定哪个中断下次发往CPU interface
  2. 中断配置GICD_ICFGR<n>。它决定中断是电平中断还是边沿中断。不适用于SGI。
  3. 中断目标GICD_ITARGETSR<n>。它决定中断发往哪个core。仅适用于SPI。
  4. 中断使能和禁用状态GICD_ISENABLER<n>和GICD_ICENABLER<n>。当它们变成pending时,仅distributor中使能的这些中断能够发送。
  5. 中断安全性GICD_IGROUPR<n>决定中断是分配给安全还是非安全
  6. 中断状态

        Distributor也提供中断优先级屏蔽,中断低于某个优先级被阻止发送到core。Distributor使用它决定pending中断是否发送给某个core。

        每个core的CPU interface帮助该core上的中断控制和处理。

6.2 初始化

        在reset时Distributor和CPU interface都是disable的。在reset之后GIC必须在它传递中断给core之前进行初始化。

        在Distributor中,软件必须配置优先级,目标core,安全性和使能单个中断。Distributor必须通过它的控制寄存器GICD_CTLR被使能。对于每个CPU interface,软件必须编程优先级屏蔽和抢占设置。

        每个CPU interface必须通过它的控制器GICD_CTLR被使能。这为GIC传递中断给Core作准备。

        在中断传递给core之前,软件通过设置vector table中的有效中断vector,并清PSTATE中中断屏蔽,并设置路由控制。

        整个中断机制可以通过disable Distributor被禁用。中断的传递也可以 通过disable CPU interface被禁用。单个中断也可以被disable。

        对于到达core的中断,单个中断,Distributor和CPU interface必须被使能。同时中断也需要有足够的优先级,要高于core的优先级屏蔽。

6.3 中断处理

        当core获取到中断,它会跳转到vector table中top-level中断向量,并开始执行。

        Top-level中断handler读取CPU interface的IAR寄存器来获取interrupt ID。

        在返回interrupt ID同时,该读取会导致中断被标记为active。一旦知道interrupt ID,top-level handler可以发出设备特定的handler处理中断。

        当设备特定的handler完成执行,top-level handler写interrupt ID给EOI寄存器,表明完成中断处理。

        除了移除active状态,这会将最后中断状态标记为inactive,或pending,这使得CPU interface将更多pending中断发往core。这总结了单个中断的处理流程。

        也有可能在相同的core上超过一个中断等待被处理,但CPU interface一次仅发出一个中断。Top-level中断handler重复上述时序直到它读取到特定的interrupt ID 1023,这表明在这个core上没有其他的pending中断。这个特定的interrupt ID被称为spurious interrupt ID。

        spurious interrupt ID为保留值,在系统中可以被赋值给任何设备。当top-level handler读取到spurious interrupt ID时,它可以完成执行,并在处理中断前准备让core唤醒task。

        GIC管理多个中断源的输入并将他们传递给IRQ或FIQ请求。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值