11.Exception Handling
异常是指任何需要核心暂停正常执行而转而执行与每种异常类型关联的专用软件例程(称为异常处理程序)的情况。异常是通常需要特权软件采取补救措施或更新系统状态的条件或系统事件,以确保系统的平稳运行。这被称为处理异常。当异常处理完成后,特权软件会准备核心恢复到异常发生前的状态。其他架构可能将ARM所称的异常称为陷阱或中断,但在ARM架构中,这些术语保留用于特定类型的异常。
所有微处理器必须响应外部异步事件,例如按下按钮或时钟达到某个值。通常,专用硬件会激活核心的输入线。这会导致核心暂时停止当前程序序列并执行特权处理程序。核心响应此类事件的速度可能是系统设计中的关键问题,称为中断延迟。实际上,在许多嵌入式系统中,并没有主程序——系统的所有功能都由从中断运行的代码处理,而为这些中断分配优先级是设计的关键领域。系统通过生成中断通知核心发生了某些事情,而不是让核心不断测试系统不同部分的标志以查看是否有待处理的任务。复杂的系统有许多中断源,具有不同的优先级和嵌套中断处理的要求,高优先级中断可以中断低优先级中断。
在正常程序执行中,程序计数器在地址空间中递增,程序中的显式分支修改执行流程,例如用于函数调用、循环和条件代码。当异常发生时,这一预定的执行顺序会被中断,并暂时切换到处理异常的例程。
除了响应外部中断,还有许多其他因素可能导致核心发生异常,包括外部因素,如复位、来自内存系统的外部中止,以及内部因素,如MMU生成的中止或使用SVC指令的操作系统调用。你可能还记得在第三章中,处理异常会导致核心在不同模式之间切换,并将一些寄存器的内容复制到其他寄存器。
11.1 Types of exception
ARMv7-A和ARMv7-R架构包括六种特权模式:FIQ、IRQ、超级用户、异常、未定义和系统模式,以及非特权的用户模式。如果实现了虚拟化扩展和安全扩展,Hyp模式和监控模式可以被添加到列表中。当前模式可以在特权软件控制下改变,或者在发生异常时自动改变。
非特权用户模式无法直接影响核心的异常行为,但可以生成SVC异常来请求特权服务。这就是用户应用程序请求操作系统代表其完成任务的方式。
当发生异常时,核心会保存当前状态和返回地址,进入特定模式,并可能禁用硬件中断。对特定异常的执行处理从一个固定的内存地址开始,称为该异常的异常向量。特权软件可以将一组异常向量的位置编程到系统寄存器中,并在发生相应异常时自动执行。
存在以下类型的异常: 中断:在ARMv7-A核心上提供了两种类型的中断,称为IRQ和FIQ。FIQ的优先级高于IRQ。由于在向量表中的位置以及FIQ模式中可用的更高数量的寄存器,FIQ在速度上也有一些潜在优势。这可能在处理程序中节省将寄存器推入栈的时钟周期。这两种异常通常与核心上的输入引脚相关——外部硬件激活中断请求线,当当前指令执行完成时,相应的异常类型会被触发,前提是中断未被禁用。
FIQ和IRQ都是对核心的物理信号,当激活时,核心会在当前启用的情况下发生相应的异常。在几乎所有系统中,各种中断源将通过中断控制器连接。中断控制器负责仲裁和优先级排序中断,并提供一个序列化的单一信号,然后将其连接到核心的FIQ或IRQ信号。有关更多信息,请参见相关内容
由于IRQ和FIQ中断的发生与核心在任何给定时间执行的软件没有直接关系,因此它们被归类为异步异常。
中止:中止可以在指令获取失败(预取中止)或数据访问失败(数据中止)时生成。它们可能来自外部内存系统,在内存访问时给出错误响应(这可能表示指定的地址与系统中的实际内存不对应)。另外,中止也可以由核心的内存管理单元(MMU)生成。操作系统可以使用MMU中止动态分配内存给应用程序。
当指令在管道中被获取时,可以标记为已中止。预取中止异常仅在核心尝试执行该指令时发生。异常发生在指令执行之前。如果在中止指令到达管道的执行阶段之前,管道被刷新,则不会发生中止异常。数据中止异常在加载或存储指令执行时发生,被认为发生在尝试读取或写入数据之后。
如果中止是由于执行或尝试执行指令流而生成的,并且返回地址将提供导致中止的指令的详细信息,则该中止被描述为同步中止。异步中止则不是通过执行指令生成的,同时返回地址可能并不总是提供导致中止的原因的详细信息。
ARMv7架构区分精确和不精确的异步中止。由MMU生成的中止始终是同步的。该架构并不要求特定类别的外部中止访问是同步的。例如,在某个特定实现中,报告在翻译表查找时发生的外部中止可能被视为精确的,但这并不是对所有核心的要求。对于精确的异步中止,中止处理程序可以确定是哪条指令导致了中止,并且在该指令之后没有执行其他指令。这与不精确的异步中止形成对比,后者是外部内存系统在无法识别的访问上报告错误时的结果。在这种情况下,中止处理程序无法确定是哪条指令引起了问题,也无法确定在生成中止的指令之后是否执行了其他指令。
例如,如果一个缓冲写入从外部内存系统接收到错误响应,那么在存储操作之后将会执行其他指令。这意味着中止处理程序无法修复该问题并返回到应用程序。它所能做的就是终止引发问题的应用程序。因此,设备探测需要特别处理,因为对不存在区域的读取报告的外部中止即使在该内存标记为强序列化或设备时也会生成不精确的同步中止。
异步中止的检测由CPSR的A位控制。如果A位被设置,来自外部内存系统的异步中止将被核心识别,但不会生成中止异常。相反,核心将保持中止挂起,直到A位被清除,此时会触发异常。内核代码将使用屏障指令确保挂起的异步中止能够与正确的应用程序对应。如果由于不精确中止必须终止一个线程,它必须是正确的那个!
重置:所有核心都有一个重置输入,并将在重置后立即发生重置异常。这是最高优先级的异常,无法屏蔽。该异常用于在上电后在核心上执行初始化代码。
生成异常的指令:某些指令的执行可以生成异常。这些指令通常用于请求以更高特权级别运行的软件提供服务:
**超级用户调用(SVC)**指令使用户模式程序能够请求操作系统服务。
**虚拟机监控调用(HVC)**指令(如果实现了虚拟化扩展)使来宾操作系统能够请求虚拟机监控服务。
**安全监控调用(SMC)**指令(如果实现了安全扩展)使正常世界能够请求安全世界的服务。
任何尝试执行核心未识别的指令都会生成未定义(UNDEFINED)异常。
当发生异常时,核心会执行与该异常对应的处理程序。处理程序存储在内存中的位置称为异常向量。在ARM架构中,异常向量存储在一个表中,称为异常向量表。因此,单个异常的向量可以位于表开始处的固定偏移量位置。表的基址由特权软件编程到系统寄存器中,以便核心在发生异常时能够定位相应的处理程序。
可以为安全PL1、非安全PL1、安全监控和非安全PL2特权级配置单独的向量表。核心用来查找处理程序的表取决于当前的执行特权,以及其被配置为进入的特权或安全状态。
你可以用ARM或Thumb代码编写异常处理程序。CP15 SCTLR.TE位用于指定异常处理程序将使用ARM还是Thumb。在处理异常时,必须保留核心的先前模式、状态和寄存器,以便在处理完异常后能够恢复程序的执行。
11.1.1 Exception priorities
当同时发生异常时,每个异常会依次处理,然后返回到原始应用程序。所有异常不能同时发生。例如,未定义指令(Undef)和监控调用(SVC)异常是互斥的,因为它们都是通过执行指令触发的。
注意
ARM架构并未定义何时处理异步异常。因此,异步异常相对于其他异常(包括同步和异步异常)的优先级由具体实现决定。
所有异常都会禁用IRQ,只有FIQ和重置会禁用FIQ。这是通过核心自动设置CPSR的I(IRQ)和F(FIQ)位来实现的。因此,除非处理程序显式禁用它,FIQ异常可以中断数据中止处理程序或IRQ异常。如果同时发生数据中止和FIQ,优先级较高的数据中止会优先处理。这让核心能够记录数据中止的返回地址。但由于数据中止并不会禁用FIQ,因此核心会立即处理FIQ异常。在FIQ处理结束后,返回到数据中止处理程序。
虽然可能会同时生成多个异常,但某些组合是互斥的。预取中止会将指令标记为无效,因此不能与未定义指令或SVC同时发生(当然,SVC指令也不能是未定义指令)。这些指令无法引起任何内存访问,因此无法导致数据中止。架构并未定义何时必须处理异步异常、FIQ、IRQ或异步中止,但处理IRQ或数据中止异常不会禁用FIQ异常,这意味着FIQ的执行会优先于IRQ或异步中止处理。
核心上的异常处理通过一个称为向量表的内存区域进行控制。默认情况下,该向量表位于内存映射的底部,从0x00到0x1C的字对齐地址。大多数缓存核心允许将向量表从0x0移动到0xFFFF0000。
对于具有安全扩展的核心,情况更为复杂。此时有三个向量表:非安全、保护和安全监控。对于具有虚拟化扩展的核心,则有四个,增加了一个Hypervisor向量表。对于具有MMU的核心,所有这些向量地址都是虚拟的。
11.1.2 Exception mode summary
下表列出了在进入异常处理程序时CPSR中IRQ和FIQ位的中断禁用状态。
11.1.3 The Vector table
本篇文章的第一个表的第一列给出了与特定类型异常相关的向量表中的向量偏移。这是一个指令表,ARM核心在引发异常时跳转到这些指令。这些指令位于内存的特定位置。默认的向量基地址是0x00000000,但大多数ARM核心允许将向量基地址移动到0xFFFF0000(或HIVECS)。所有Cortex-A系列处理器都允许这样做,并且这是Linux内核选择的默认地址。实现安全扩展的核心还可以使用CP15向量基地址寄存器分别设置安全状态和非安全状态的向量基地址。
你会注意到,每种异常类型都与一个单词地址相关联。因此,每个异常在向量表中只能放置一条指令(尽管理论上可以使用两条16位的Thumb指令)。因此,向量表条目几乎总是包含以下两种跳转形式之一:
B
LDR PC, [PC, #offset]
这从一个相对于异常指令地址的内存位置加载PC。这允许异常处理程序放置在整个32位内存空间中的任何任意地址(但相对于简单分支需要额外的时钟周期)。
当核心处于Hyp模式时,它使用Hyp模式向量条目,这些条目来自于专门属于虚拟机监控程序的向量表。通过一种特殊的异常过程(称为Hyp陷阱入口)进入虚拟机监控程序模式,该过程利用了向量表中先前保留的0x14地址。一个专用寄存器(Hyp综合寄存器)向虚拟机监控程序提供有关异常或进入虚拟机监控程序的其他原因的信息(例如,被捕获的CP15操作)。
11.1.4 FIQ and IRQ
FIQ保留给一个单一的高优先级中断源,需要保证快速响应时间,而IRQ则用于系统中的所有其他中断。
由于FIQ是向量表中的最后一个条目,FIQ处理程序可以直接放置在向量位置,并从该地址顺序执行。这避免了分支指令和相关延迟,从而加快了FIQ的响应时间。相较于其他模式,FIQ模式中额外的银行寄存器使得在调用FIQ处理程序之间能够保留状态,从而提高了执行速度,可能减少了在使用寄存器前需要推送某些寄存器的需求。
IRQ和FIQ之间的另一个关键区别在于,FIQ处理程序不应生成其他异常。因此,FIQ保留给那些具有完全内存映射且无需进行SVC调用来访问内核功能的特殊系统设备(因此FIQ只能被不需要使用内核API的代码使用)。
FIQ通常不被Linux使用。由于内核是架构无关的,它没有多种中断形式的概念。虽然一些运行Linux的系统仍然可以利用FIQ,但由于Linux内核从不禁用FIQ,因此它们在系统中的优先级高于其他任何中断,因此需要谨慎处理。
11.1.5 The return instruction
链接寄存器(LR)用于存储处理完异常后PC的适当返回地址。其值必须根据发生的异常类型进行修改,如下表所示。《ARM架构参考手册》定义了适当的LR值(这些定义源自于早期硬件实现时方便的值)。下表还提供了一个包含此调整的异常返回指令示例。
11.2 Exception handling
当发生异常时,ARM核心会自动执行以下操作:
将CPSR复制到SPSR_,这是特定于(非用户)操作模式的寄存器。
在新模式的链接寄存器(LR)中存储返回地址。
修改CPSR模式位为与异常类型相关的模式。
这使得异常始终在ARM或Thumb状态下以小端或大端格式运行,而不管核心在发生异常前的状态如何。
将PC设置为指向异常向量表中的相关指令。在新模式下,核心将访问与该模式关联的寄存器。
几乎总是需要异常处理程序软件在异常进入时立即将寄存器保存到堆栈中。FIQ模式具有更多的银行寄存器,因此简单的处理程序可能可以编写成不需要使用堆栈的方式。
提供了一条特殊的汇编语言指令来帮助保存必要的寄存器,称为SRS(保存返回状态)。该指令将LR和SPSR压入任何模式的堆栈;要使用的堆栈由指令操作数指定。
11.2.1 Exit from an exception handler
要从异常处理程序返回,必须原子性地执行两个独立操作:
从保存的SPSR恢复CPSR。
将PC设置为返回地址偏移。
在ARM架构中,可以使用RFE指令或任何标志设置的数据处理操作(带有S后缀),将PC作为目标寄存器,例如:SUBS PC, LR, #offset
(注意S)。返回异常(RFE)指令从当前模式堆栈中弹出链接寄存器和SPSR。
实现这一点有多种方法:
如果异常处理程序入口代码使用堆栈保存必须在处理异常时保留的寄存器,可以使用带有^修饰符的多重加载指令返回。例如,异常处理程序可以使用以下指令在一条指令中返回:
LDMFD sp! {pc}^
LDMFD sp!, {R0-R12, pc}^
在这个例子中,^修饰符表示SPSR同时复制到CPSR。
为此,异常处理程序必须将以下内容保存到堆栈中:
调用处理程序时正在使用的所有工作寄存器。
链接寄存器,修改后产生与数据处理指令相同的效果。
注意:不能使用16位Thumb指令从异常中返回,因为这些指令无法恢复CPSR。
RFEFD sp
11.3 Other exception handlers
这一部分简要描述了处理程序如何处理中止、未定义指令和SVC异常,并考虑了Linux内核如何处理中断。
11.3.1 Abort handler
中止处理程序代码在不同系统之间可能有显著差异。在许多嵌入式系统中,中止表示意外错误,处理程序将记录任何诊断信息,报告错误,并使应用程序(或系统)正常退出。
在支持虚拟内存的系统中,使用内存管理单元(MMU)的中止处理程序可以将所需的虚拟页面加载到物理内存中。实际上,它尝试修复原始中止的原因,然后返回到中止的指令并重新执行。
CP15寄存器提供了导致中止的内存访问地址(故障地址寄存器)和中止原因(故障状态寄存器)。原因可能是缺乏访问权限、外部中止或地址转换故障。此外,链接寄存器(根据中止是由指令获取还是数据访问引起,可能需要调整-8或-4)提供了导致中止异常的指令地址。通过检查这些寄存器、最后执行的指令以及系统中的其他信息,例如转换表条目,中止处理程序可以确定采取何种行动。
11.3.2 Undefined instruction handling
如果核心尝试执行一个在ARM架构规范中被描述为“未定义”的操作码的指令,或者当一个协处理器指令被执行但没有任何协处理器识别它为可执行的指令,就会产生未定义指令异常。
在某些系统中,代码可能包含协处理器(例如VFP协处理器)的指令,但系统中没有相应的VFP硬件。此外,VFP硬件可能无法处理特定指令,并希望调用软件进行仿真。或者,VFP硬件可能被禁用,因此会触发异常,以便启用它然后重新执行指令。
这些仿真程序通过未定义指令向量调用。它们检查导致异常的指令操作码,并确定采取何种行动(例如,在软件中执行适当的浮点操作)。在某些情况下,这些处理程序可能需要串联在一起(例如,可能有多个协处理器需要仿真)。
如果没有软件使用未定义或协处理器指令,异常的处理程序必须记录适当的调试信息,并终止因该意外事件而失败的应用程序。
在某些情况下,未定义指令异常的额外用途是实现用户断点。
11.3.3 SVC exception handling
超级调用(SVC)通常用于使用户模式代码能够访问操作系统功能。例如,如果用户代码想要访问系统的特权部分(例如执行文件I/O),它通常会使用SVC指令来实现。
参数可以通过寄存器传递给SVC处理程序,也可以(较少见)通过操作码中的注释字段传递。
异常处理程序可能需要确定异常发生时核心处于ARM状态还是Thumb状态。尤其是,SVC处理程序可能需要读取指令集状态。这是通过检查SPSR的T位来完成的。该位在Thumb状态时被置位,在ARM状态时被清除。
ARM和Thumb指令集都有SVC指令。当从Thumb状态调用SVC时,必须考虑以下几点:
关于如何在Linux内核中使用SVC的示例代码见以下示例。
SVC #0指令使ARM核心触发SVC异常,这是访问内核功能的机制。寄存器R7定义了您想要的系统调用(在此情况下为sys_write)。其他参数通过寄存器传递;对于sys_write,R0指示要写入的位置,R1指向要写入的字符,R2给出字符串的长度。
11.4 Linux exception program flow
Linux利用跨平台的异常处理框架,在处理异常时不区分不同的特权核心模式。因此,ARM实现使用异常处理程序存根,使内核能够在SVC模式下处理所有异常。除了SVC和FIQ之外,所有异常都使用存根切换到SVC模式,并调用正确的异常处理程序。
11.4.1 Boot process
在启动过程中,内核将分配一个4KB的页面作为向量页面。它将此页面映射到异常向量的位置,虚拟地址为0xFFFF0000或0x00000000。这是通过位于文件arch/arm/mm/mmu.c中的devicemaps_init()完成的,该函数在ARM系统启动的早期阶段被调用。之后,trap_init(位于arch/arm/kernel/traps.c中)将异常向量表、异常存根和kuser帮助程序复制到向量页面中。显然,异常向量表必须复制到向量页面的起始位置,异常存根复制到地址0x200(而kuser帮助程序复制到页面顶部,即0x1000 - kuser_sz),这通过一系列memcpy()操作完成,如下面的示例所示。
当复制完成后,内核异常处理程序处于其运行时动态状态,准备处理异常。
11.4.2 Interrupt dispatch
有两个不同的处理程序,__irq_usr和__irq_svc。这些处理程序保存所有核心寄存器,并使用宏get_irqnr_and_base来指示是否有待处理的中断。处理程序在这段代码中循环,直到没有中断剩余。如果有中断,代码将分支到arch/arm/kernel/irq.c中的do_IRQ。
此时,所有架构的代码都是相同的,并调用用C语言编写的适当处理程序。
然而,还有一个额外的考虑点。当中断完成后,通常需要检查处理程序是否执行了需要调用内核调度程序的操作。如果调度程序决定切换到另一个线程,则最初被中断的线程将保持休眠状态,直到再次被选中运行。
(广告时间)
Arm架构类课程:
安全热销大课程:
安全类经典课程:
其它课程:
铂金VIP课程介绍
之最介绍
招牌课程:Truszone标准版、Trustzone高配版
销量前三课程:ARM三期、Secureboot、Android15安全架构
持续更新的课程:ARM三期、铂金VIP
非常好非常好但又被忽视的课程:CA/TA开发
近期更新/力推的课程/重点课程:optee系统架构从入门到精通
说点心里话:
1、不要再说课贵了,你看看咱这是啥课?别家的能比不?请不要拿通用的linux、android、python、C语言和咱这专业课比。
2、咱们的VIP是数十门课程的集合。不要拿别人一门课程的价格对标咱这20门课程价格。
3、这些知识很多人都会,但能拿出来讲的有多少人? 愿意拿出来讲的有多少人?会讲的又有多少人?
4、价格都是认真计算的,并非随意定价。都是根据内容质量、核心知识点、时长和节数计算而来。从来不无缘无故涨价(涨价是需要理由的,如课程内容增加了....)。咱靠的是内容质量和长期服务,而不是运营和营销(无脑涨价)。
5、如果你刷到此处,可能是老粉/铁粉,记得点赞、评论哦。感谢您的支持。