参考:
《Arm®v7-M Architecture Reference Manual》
ARMv7-M 系列其他文章:
1、ARMv7-M 异常概述
异常(Exception)可以由产生异常的指令触发,也可以由系统行为引起,例如中断请求、内存保护违规、非对齐访问、总线错误或调试事件。故障(Fault)和中断(Interrupt)属于异常体系中的不同类别,架构支持同步 和异步异常。
1.1 系统相关事件
Supervisor Call 用于应用程序向底层操作系统请求服务。应用程序可以执行 SVC 指令,发起需要特权访问的系统服务请求。
Fault 相关事件包括:
- 指令执行过程中发生的错误。
- 加载或存储操作引起的数据访问错误。
- 执行状态相关错误,例如执行未定义指令而产生 UsageFault。
- 可以产生 DebugMonitor 异常的调试事件。
Fault 通常与引发它的指令执行同步。但部分系统错误可能产生不精确异常:处理器报告异常的时刻与引发错误的指令之间没有固定关系,因此无法根据异常发生时的程序位置直接定位故障指令。
- 异步报告的例子:执行 STR 时,写请求先进入写缓冲,处理器继续执行后续指令。如果该请求在后续总线传输中发生错误,处理器可能延迟收到错误响应,从而产生异步且不精确的 BusFault。
- 同步报告的例子:执行 STR 时,处理器在该指令执行过程中检测到总线访问错误,并在该指令处报告 BusFault。此时故障与指令执行同步,属于精确故障,可以根据异常保存的 PC 定位故障指令。
处理器始终将中断 视为相对于程序执行流的异步事件。
ARMv7-M 为这些系统相关事件提供了以下支持:
| 机制 | 作用 |
|---|---|
| SysTick | 系统定时器及其关联中断 |
| PendSV | 延后处理的 Supervisor 服务请求,通过 ICSR 支持挂起控制 |
| NVIC | 管理外部中断的嵌套向量中断控制器 |
| BKPT | 产生调试事件的指令 |
PendSV 通过设置 ICSR.PENDSVSET 发起挂起请求,其实际响应时机由异常优先级模型决定。需要与程序执行同步发生的 Supervisor 调用,则使用 SVC 指令。

架构还提供 SEV、WFE 和 WFI 指令,分别支持发送事件、等待事件和等待中断,供软件进行事件通知、执行等待和功耗管理。
1.2 异常状态(Exception states)
每个异常都处于以下状态之一:
- Inactive:异常未激活且未挂起。
- Pending:异常正在等待处理器处理。来自外设或软件的中断请求可以将相应中断的状态更改为挂起(Pending)。
- Active:处理器正在处理但尚未完成的异常。
- Active and pending:异常正在由处理器处理,并且存在来自同一源的挂起的异常。
注意:异常处理程序可以中断另一个异常处理程序的执行。在这种情况下,两个异常都处于活动状态。
2、ARMv7-M 异常模型
ARMv7-M 在异常进入和退出时,由硬件保存和恢复关键上下文状态,并通过向量表提供各异常处理程序的入口。这些机制构成异常处理的软件可见接口。
以下按照手册顺序,先介绍支持的异常,再定义异常编号,最后说明向量表的组织规则。
2.1 支持的异常概览
ARMv7-M 支持 Reset、NMI、HardFault、MemManage、BusFault、UsageFault、DebugMonitor、SVCall,以及系统级中断和外部中断。这些异常分别承担初始化、错误报告、系统服务和设备事件处理等职责。
Reset:复位
Reset 用于建立处理器的初始运行状态。ARMv7-M 区分两种复位范围:
- 上电复位(Power-on reset):复位处理器、系统控制空间和调试逻辑。
- 局部复位(Local reset):复位处理器和系统控制空间,但部分故障与调试相关资源不随之复位。
复位范围决定了哪些寄存器字段恢复为复位值,因此不能将不同来源的复位一概视为完全相同的状态清除过程。
Reset 异常始终使能,固定优先级为 -3。复位的具体执行过程将在 B1.5.5 中说明。
NMI:不可屏蔽中断
NMI(Non-Maskable Interrupt)是除 Reset 之外优先级最高的异常,始终使能,固定优先级为 -2。
NMI 可以由硬件产生,也可以由软件通过 ICSR (Interrupt Control and State Register)中的相应控制位设置为挂起状态。具体事件如何连接到 NMI,由系统实现决定。

可以看到,通过软件写 ICSR,产生 NMI 中断。
HardFault:通用故障处理
HardFault 为无法通过其他适用异常机制处理的故障提供通用入口,也用于接收由其他故障异常升级而来的错误。
HardFault 通常用于处理严重的系统故障,但架构并不要求所有 HardFault 都不可恢复。其处理策略取决于故障原因及系统设计。
HardFault 始终使能,固定优先级为 -1。故障升级的具体规则将在后续优先级章节展开。
MemManage:内存保护故障
MemManage 用于处理由内存保护单元 MPU 或架构固定保护规则检测到的内存访问违规,适用于指令访问和数据访问。
MemManage 的优先级可配置,软件可以禁止该异常。禁止后,相关保护故障仍然会被检测,其处理将升级为 HardFault。
BusFault:总线访问故障
BusFault 用于处理 MemManage 范围之外的内存相关故障,通常由系统总线检测到的访问错误引起。它可以涉及指令访问或数据访问。
架构允许实现根据具体原因报告同步或异步 BusFault。
BusFault 的优先级可配置,软件可以禁止该异常。禁止后,对应故障升级为 HardFault。
UsageFault:指令使用与执行状态故障
UsageFault 用于处理指令执行引起的非内存类故障,典型原因包括:
- 执行未定义指令。
- 指令执行时处于无效状态。
- 异常返回过程中出现错误。
- 访问未使能或不存在的协处理器。
另外,在配置为报告对应错误时,下列行为也可以产生 UsageFault:
- 某些非对齐的半字或字访问。
- 除零。
UsageFault 的优先级可配置,软件可以禁止该异常。禁止后,对应故障升级为 HardFault。
DebugMonitor:调试监控异常
当暂停调试(Halting debug)关闭、DebugMonitor 异常使能,并满足相应调试条件时,调试事件可以产生 DebugMonitor 异常。
手册将 DebugMonitor 通常归类为同步的故障异常,同时注明调试观察点是异步事件,按中断性质处理。DebugMonitor 的优先级可配置。
SVCall:监督调用
SVCall 用于处理执行 SVC 指令产生的异常,为软件请求系统服务提供入口。
需要与程序执行同步发生的监督调用,应使用 SVC 指令。SVCall 始终使能,优先级可配置。
中断:系统级中断与外部中断
ARMv7-M 定义了两个系统级中断:PendSV 和 SysTick,并在架构层面支持最多 496 个外部中断。实际支持的外部中断数量由具体实现决定。各中断的优先级均可配置。
PendSV
PendSV 提供由软件请求、随后由处理器响应的系统服务入口。软件设置挂起请求后,PendSV 处理程序在处理器接受该异常时执行。
PendSV 始终使能,其挂起状态通过 ICSR 的两个位控制:
| 控制位 | 作用 |
|---|---|
PENDSVSET |
设置 PendSV 挂起状态 |
PENDSVCLR |
清除 PendSV 挂起状态 |
PendSV 的挂起请求由软件设置。
SysTick
SysTick 是由处理器系统定时器产生的中断,可用于周期性系统服务。
SysTick 异常始终使能,其挂起状态通过 ICSR 的两个位控制:
| 控制位 | 作用 |
|---|---|
PENDSTSET |
设置 SysTick 挂起状态 |
PENDSTCLR |
清除 SysTick 挂起状态 |
软件可以抑制定时器产生 SysTick 事件,但上述软件挂起控制接口始终可用。
外部中断
外部中断由处理器之外的外设或系统逻辑提供请求,通常通过 NVIC 接入异常处理体系。其用途和编号分配由具体芯片定义。
软件可以使能或禁止各外部中断,并设置或清除它们的挂起状态。外部中断也可以由硬件设置为挂起状态。
2.2 异常编号定义
每个已定义异常都有一个对应的异常编号。系统异常的编号由架构规定,外部中断从异常编号 16 开始依次排列。

对于外部中断,编号关系为:
text
异常编号 = 16 + 外部中断编号
架构固定了这个编号关系,但并未统一规定每个外部中断对应哪个设备。具体外设与外部中断编号的对应关系,应查阅芯片手册中的中断分配表。
异常编号也确定了相应处理入口在向量表中的位置,下一节将介绍这一映射。
2.3 异常向量表
向量表是异常编号与处理程序入口之间的地址映射表。处理器通过读取相应表项获得入口地址,因此软件需要按照架构规定的布局建立该表。
向量表的组成

向量表包含两类内容:
- 主栈指针 MSP 的初始值(作为硬件复位后默认的 MSP 值)。
- 各异常处理程序的入口地址。
每个表项占一个 32 位字,即 4 字节。第 0 项保存初始 MSP;其余已定义异常的入口按照异常编号排列,保留编号也占据对应位置。
| 表项索引 | 字节偏移 | 内容 |
|---|---|---|
| 0 | 0x00 |
初始 MSP |
| 1 | 0x04 |
Reset 入口 |
| 2 | 0x08 |
NMI 入口 |
| 3 | 0x0C |
HardFault 入口 |
| 4 | 0x10 |
MemManage 入口 |
| 5 | 0x14 |
BusFault 入口 |
| 6 | 0x18 |
UsageFault 入口 |
| 7---10 | 0x1C---0x28 |
保留 |
| 11 | 0x2C |
SVCall 入口 |
| 12 | 0x30 |
DebugMonitor 入口 |
| 13 | 0x34 |
保留 |
| 14 | 0x38 |
PendSV 入口 |
| 15 | 0x3C |
SysTick 入口 |
| 16 | 0x40 |
外部中断 0 入口 |
| 17 | 0x44 |
外部中断 1 入口 |
| ... | ... | ... |
| 16 + n | 0x40 + 4 × n |
外部中断 n 入口 |
第 0 项用于提供复位时的栈指针初始值,并不对应一个异常处理入口。
按异常编号定位入口
设向量表基址为 B,异常编号为 E,则该异常入口表项的地址为:
text
表项地址 = B + 4 × E
对于外部中断 n:
text
E = 16 + n
表项地址 = B + 0x40 + 4 × n
处理器从表项中读取入口指针,再依据异常进入规则开始执行相应处理程序。向量表存放的是入口地址,处理程序的代码可以位于其他适用的存储区域。
例如,若向量表基址为 0x08000000,则 Reset 和 SysTick 的表项分别位于 0x08000004、0x0800003C。这些位置存放的是各自的入口指针,具体指针值由程序的实际代码布局决定。
向量表基址
复位时,处理器将向量表基址初始化为一个实现定义的地址 。软件可以通过向量表偏移寄存器 VTOR(Vector Table Offset Register)查看当前表的位置,或改变其位置。
向量表对齐
向量表必须按 2 的幂自然对齐,且对齐值同时满足两个条件:
- 不小于表覆盖的异常入口范围。
- 不小于 128 字节。
计算表的覆盖范围时,应包含前 16 个位置,即初始 MSP、系统异常入口和保留槽位,以及实现支持的外部中断槽位。
以支持 32 个外部中断槽位的实现为例:
text
表项数量 = 16 + 32 = 48
表的大小 = 48 × 4 = 192 字节
最小合适对齐值 = 256 字节
其他规模的示例如下:
| 外部中断槽位数 | 总表项数 | 表的大小 | 最小对齐值 |
|---|---|---|---|
| 16 | 32 | 128 字节 | 128 字节 |
| 32 | 48 | 192 字节 | 256 字节 |
| 64 | 80 | 320 字节 | 512 字节 |
因此,128 字节是最低要求。具体需要多大的对齐值,应根据实现支持的表规模确定。
异常处理函数入口指针的最低位
用于异常处理入口的有效表项,其 bit0 必须为 1。这一位在异常进入时用于确定 EPSR.T,即 Thumb 执行状态。
因为 ARMv7-M 只支持 Thumb,要求 EPSR.T = 1。如果表项最低位填成 0,就会得到无效的执行状态,执行处理程序的第一条指令时产生 INVSTATE UsageFault。
例如,代码入口(处理函数)位于 0x08001000 时,相应表项中的入口指针应表示为:
text
0x08001001
最低位用于表示 Thumb 状态,实际指令地址仍为 0x08001000。首项保存的初始 MSP 不属于异常入口指针。
注意:对于软件来说,不需要手动给向量表表项中的入口指针 bit0 置一。因为代码在编译、链接过程中,会自动将 code pointer 的最后一位置1(根据 ARM 的 ABI 规则)。
2.4 异常优先级与抢占
异常编号和向量表解决了处理入口的定位问题,优先级模型则规定多个异常之间的响应顺序,以及新异常能否打断当前执行。
理解这一模型,需要先区分两个状态:挂起(Pending) 表示异常请求正在等待处理;活动(Active) 表示处理器已经接受该异常,其处理过程尚未完成。一个处理程序被其他异常抢占后,对应异常仍保持活动状态。
优先级数值与实现位数
ARMv7-M 规定,优先级数值越小,优先级越高。
| 异常 | 优先级 | 是否可配置 |
|---|---|---|
| Reset | -3 | 否 |
| NMI | -2 | 否 |
| HardFault | -1 | 否 |
| 其他异常 | 非负数,取决于实现的优先级位数 | 是 |
负数用于描述固定优先级与可配置优先级之间的相对关系。软件为其他异常设置的优先级从 0 开始,因此无法通过配置使其高于 NMI 或 HardFault。
外部中断的优先级通过 NVIC 的中断优先级寄存器 NVIC_IPRn 设置;具有可配置优先级的系统异常,通过系统处理程序优先级寄存器 SHPR1---SHPR3 设置。两组寄存器中的优先级属于同一套优先级体系,处理器按照统一的优先级比较和分组规则,对系统异常与外部中断进行响应排序及抢占判断。 复位后,可配置优先级恢复为 0,即最高的可配置优先级。



每个可配置优先级字段占 8 位,但具体实现可以仅提供高位中的一部分。ARMv7-M 支持 3---8 个有效优先级位,对应 8---256 个优先级;未实现的低位读为 0,写入被忽略。
| 有效位数 | 可区分的优先级数量 | 优先级字段中的最大有效值 |
|---|---|---|
| 3 | 8 | 0xE0 |
| 4 | 16 | 0xF0 |
| 5 | 32 | 0xF8 |
| 6 | 64 | 0xFC |
| 7 | 128 | 0xFE |
| 8 | 256 | 0xFF |
例如,实现 4 个有效位时,使用的是字段的 bit[7:4],有效编码依次为 0x00、0x10、...、0xF0。若将这 16 个等级按 0---15 编号,则等级 5 对应寄存器字段值 0x50。
本文涉及寄存器编码时使用高位对齐的字段值。CMSIS 的 NVIC_SetPriority() 接口接收未左移的等级值,并依据 __NVIC_PRIO_BITS 完成移位;在上述实现中,应向该接口传入 5。
优先级分组
优先级字段可以分为两部分:
- 组优先级(Group priority) :决定异常之间能否发生抢占,也常称为抢占优先级。
- 子优先级(Subpriority) :在组优先级相同的挂起异常之间,决定响应顺序,并不会抢占。
两部分的分界由 AIRCR.PRIGROUP 控制。下表按照完整的 8 位优先级字段列出划分方式:

实际能区分多少组、每组又有多少子优先级,仍受有效位数限制。未实现的位即使被划入某个字段,也不会增加该字段的有效取值。
例如,某实现提供 4 个有效位,且 PRIGROUP = 5。此时 bit[7:6] 是组优先级,bit[5:4] 是有效的子优先级位:
| 异常 | 优先级字段值 | 组优先级 | 子优先级 |
|---|---|---|---|
| A | 0x50(01 01 0000) |
1 | 1 |
| B | 0x60(01 10 0000) |
1 | 2 |
| C | 0x30(00 11 0000) |
0 | 3 |
A 和 B 同时挂起且都具备响应条件时,A 先得到处理。B 正在执行时,A 不能抢占 B,因为两者的组优先级相同。C 的组优先级更高,在没有屏蔽等限制时,可以抢占 A 或 B。
由此可以得到三层比较规则:
- 先比较组优先级,数值较小者优先。
- 组优先级相同时,比较子优先级,数值较小者先响应。
- 两者均相同时,异常编号较小者先响应。
子优先级和异常编号只用于确定挂起异常的先后顺序,不会使同组异常相互抢占。 Reset、NMI 和 HardFault 的固定组优先级不受 PRIGROUP 影响。
执行优先级
当没有异常处于活动状态(active)时,在 Thread mode 或 Handler mode 中执行的软件,其有效优先级值为 "所支持的最大异常优先级值 + 1"(最大优先级值+1 实际上优先级最小!)。这对应最低的优先级级别。
软件在 Thread mode 下以这一优先级级别执行时,称为处于基础执行优先级(The base level of execution priority)。
执行优先级定义为以下各项所确定的最高优先级:
- 基础执行优先级。
- 所有活动(active)异常中最高的优先级,包括被当前异常抢占的异常。
PRIMASK、FAULTMASK和BASEPRI的值所产生的影响,见下一小节"优先级提升与异常屏蔽"。
"抢占" 判断的是处理器的当前执行优先级(Execution priority) 。对于已使能的中断,只有其组优先级严格高于当前执行优先级,处理器才会接受它并开始异常进入;否则,请求保持挂起,等待执行状态变化后重新参与判断。
根据这一定义,异常处理程序运行时的执行优先级,可能高于其对应异常的优先级(例如,因为异常处理程序设置了 PRIMASK,导致当前执行优先级提升为 0)。
特别是,当某个处理程序降低其对应异常的优先级时,执行优先级不会降到已被抢占的异常中最高的优先级以下。因此,降低当前异常的优先级,绝不会:
- 允许已被抢占的异常抢占当前异常处理程序。
- 颠倒已被抢占异常的优先级顺序。
上面这段话有点拗口。举个例子就明白了。
| 状态 | A 的优先级 | B 的优先级 | 当前执行优先级 |
|---|---|---|---|
| B 抢占 A 后 | 2 | 1 | 1 |
| B 将自身优先级改为 4,修改生效后 | 2 | 4 | 2 |
执行优先级已经从 1 降到了 2,说明修改确实产生了影响。只是 A 仍处于 Active 状态,所以执行优先级不能继续降到 4。
如果只有 B 一个 Active 异常,也没有屏蔽设置的影响,那么 B 的优先级从 1 改为 4 后,执行优先级也会从 1 变为 4。
这个算法规则,有点类似于 OS 中为了解决优先级反转问题而设计的 "优先级继承"。
优先级提升与异常屏蔽
软件可以通过三个特殊寄存器限制异常激活:
PRIMASK:将该掩码位设置为 1 可将执行优先级提升至 0 。这将阻止任何具有 "可配置优先级" 的异常被激活,除非通过后面小节 "优先级升级" 中所述的 "故障升级机制"。此外,这还会对 WFI 指令产生特殊影响。- 注意,这里说的很清楚了,是通过将当前的可执行优先级提升至 0 来阻止其他异常被 active。所以,对于后面 "优先级升级" 章节中提到的,同步异常在此场景下都会被提升到 HardFault。
PRIMASK常被 OS 用来关闭当前核的 "中断嵌套" 功能。例如cpsid指令就是通过改变PRIMASK寄存器来达到关中断的目的。
c
/**
\brief Disable IRQ Interrupts
\details Disables IRQ interrupts by setting special-purpose register PRIMASK.
Can only be executed in Privileged modes.
*/
__STATIC_FORCEINLINE void __disable_irq(void)
{
__ASM volatile ("cpsid i" : : : "memory");
}
FAULTMASK:将该掩码位设置为 1,可将执行优先级提升至 -1。软件仅当执行优先级既非 NMI 也非 HardFault 时,方可将FAULTMASK置为 1;换言之,只有当优先级值大于或等于零时,FAULTMASK才能被置为1。将FAULTMASK置为 1 会将异常处理程序的优先级提升至 HardFault 的级别。除从 NMI 返回之外,任何异常返回都会自动将FAULTMASK清零 。- 通俗地说,
FAULTMASK= 1 是:除 Reset 和 NMI 外,阻止其他所有异常被新激活,连 HardFault 也包括在内。
- 通俗地说,
BASEPRI:软件可以将该寄存器写入一个介于N(最低可配置优先级)与1之间的值。当该寄存器被清零时,对执行优先级不产生任何影响。非零值在满足AIRCR.PRIGROUP字段所规定的条件时,将作为优先级掩码发挥作用(小于等于BASEPRIpriority level 的异常无法产生 active)。当BASEPRI所定义的优先级高于当前正在执行的优先级时,此掩码将影响执行优先级。- 注意,这里说的 "满足
AIRCR.PRIGROUP字段所规定的条件",BASEPRI仅受组优先级影响, 不受子优先级影响。可以理解为,相同组优先级,即使子优先级更高,也无法产生 active。
- 注意,这里说的 "满足
这些操作改变的是异常的激活(active)条件,原有优先级字段保持其配置值。优先级提升只作用于组优先级,子优先级仍仅用于挂起异常的排序。
优先级升级
当当前执行优先级低于 HardFault 时,处理器会在以下情况下将异常优先级提升至 HardFault:
- 当待处理的同步故障(synchronous fault)或 Supervisor Call 的组优先级低于或等于当前正在执行的优先级时,将禁止正常的抢占。此规则适用于所有同步异常(synchronous exceptions),包括故障(fault)和 SVCall。这其中包括因执行 BKPT 指令而引发的 DebugMonitor 异常,但不包括其他所有的 DebugMonitor 异常。
- 如果发生 "可配置优先级的故障(例如 BusFault、UsageFault、MemManage等)" 未被使能。
注意,关于同步、异步 异常,建议通过微架构手册获取。例如 cortex-M7 定义了哪些异常是同步哪些是异步:
例如,UsageFault 处理程序再次执行未定义指令,会产生无法以同一优先级重新进入的 UsageFault;在 PRIMASK = 1 时执行 SVC,SVCall 也无法按原优先级激活。这些情况可以转由 HardFault 处理。
已使能的普通中断不会因为优先级不足而升级为 HardFault,而是保持挂起。异步故障在使能时也按通常的优先级规则等待处理;若对应故障被禁止,则按 HardFault 处理,不精确(imprecise) BusFault 属于这种情况。
如果当前执行优先级已经达到 HardFault 或更高,相关错误需要按不可恢复异常的规则判断,详见后续 B1.5.15。浮点状态保存期间的故障另有规则,见 B1.6。
对软件设计的要求
优先级升级意味着本次异常转由 HardFault 处理,原异常的优先级配置保持不变。软件应将其纳入故障处理流程,不能依赖该机制完成正常的系统服务调用。
- 合理约束 SVC 的调用上下文。 执行 SVC 时,SVCall 必须能够按优先级和屏蔽规则被接受。在高优先级中断处理程序中调用无法抢占当前执行的低优先级 SVCall,属于调用方式与优先级配置不匹配,应调整软件设计。采用低优先级 SVCall 的系统,中断处理程序通常应使用中断专用接口,或提交请求交由 PendSV 等后续处理。
- 明确故障处理策略。 禁止某个可配置故障异常,不会阻止对应错误被检测,而会使其转由 HardFault 处理。系统可以有意采用统一的 HardFault 处理策略;需要分别诊断和处理时,应使能对应故障异常并合理配置其优先级。
利用 SVCall 和 PendSV 组织上下文切换
手册给出了一种通过异常优先级组织操作系统服务的使用模型:
- 将 SVCall 和 PendSV 配置为相同的最低可配置优先级。
- 线程通过 SVC 请求 Supervisor Calls 服务。
- 其他异常处理程序将需要协调的上下文管理工作交给 PendSV,并通过设置 PendSV 挂起(pending)状态提出切换请求。
在这一模型中,SVCall 和 PendSV 不能相互抢占,相关处理可以顺序完成。其他处理程序请求 PendSV 后,可以先完成自身工作;当阻止 PendSV 激活的活动异常结束、屏蔽条件解除时,处理器再执行延后的处理。
若将其他参与该模型的中断配置为更高的组优先级,它们仍可抢占 SVCall 或 PendSV。软件将需要统一协调的数据更新集中在上述两个处理程序中,高优先级处理程序通过约定的方式提交请求,从而可以设计出切换期间仍允许中断响应的实现。
这种使用模型的成立依赖软件的数据访问规则:能够抢占切换过程的处理程序,必须遵循相应的共享数据访问约定。优先级配置提供执行顺序上的保证,完整的上下文管理仍由操作系统实现。
2.5 异常入口行为
当处理器接受一个中断、故障(fault)或监督调用(SVCall)时,需要保存被打断程序的必要状态,并建立异常处理程序的执行环境。手册将这一过程分为 PushStack() 和 ExceptionTaken() 两部分,分别描述上下文入栈与异常激活。
ARM 手册中,是以伪代码形式介绍 PushStack() 入栈的过程,这里就不放完整的伪代码了。有兴趣的可以自行阅读。


以下先介绍正常完成的异常进入过程。进入期间发生的新异常、向量读取错误或入栈故障,后续在 B1.5.11 中讨论。
保存上下文所使用的栈
硬件入栈时,使用异常发生前的执行上下文所选中的栈:
| 异常发生前的状态 | 硬件保存上下文的位置 | 新处理程序使用的栈 |
|---|---|---|
| Thread mode,使用 MSP | MSP 指向的栈 | MSP |
| Thread mode,使用 PSP | PSP 指向的栈 | MSP |
| Handler mode | MSP 指向的栈 | MSP |
因此,从使用 PSP 的线程进入异常时,线程的硬件 栈帧 保存在 PSP 对应的栈中;处理程序开始执行后,普通 SP 访问转而使用 MSP。此后若发生异常嵌套,被抢占处理程序的上下文保存在 MSP 对应的栈中。
ARMv7-M 使用 满递减栈 (Full-descending stack):栈向低地址增长,入栈时先为新栈帧调整栈指针,再向该空间保存上下文。
基本异常栈帧
基本栈帧(Basic frame)包含 8 个 32 位字,共 32 字节。以完成入栈后的栈帧起始地址为基准,布局如下:
| 字节偏移 | 保存内容 | 用途 |
|---|---|---|
0x00 |
R0 | 通用寄存器上下文 |
0x04 |
R1 | 通用寄存器上下文 |
0x08 |
R2 | 通用寄存器上下文 |
0x0C |
R3 | 通用寄存器上下文 |
0x10 |
R12 | 通用寄存器上下文 |
0x14 |
LR(R14) | 被打断程序原有的链接寄存器值 |
0x18 |
ReturnAddress | 异常返回时恢复到 PC 的地址 |
0x1C |
xPSR | 被打断程序的状态信息,并按入栈规则记录对齐信息 |
例如,一个使用 PSP 栈的线程在异常发生前有 PSP = 0x20001000。假定此次使用 "基本栈帧",且不需要额外的对齐填充,则入栈后:
text
PSP = 0x20000FE0 //硬件上会自动为 Basic frame 预留 0x20 栈空间(0x20001000 - 0x20 = 0x20000FE0)
R0 的保存位置 = 0x20000FE0
返回地址的保存位置 = 0x20000FF8
xPSR 的保存位置 = 0x20000FFC
上述 32 字节只表示基本帧本身。若需要对齐填充,或需要为浮点状态保存空间,实际栈消耗会增加。这些布局扩展在 B1.5.7 中说明。
"基本栈帧" 与 AAPCS 调用约定配合,使异常处理程序可以采用遵循该约定的过程。R4---R11 不由上述硬件基本帧保存;处理程序需要按调用约定维护其使用的相关寄存器,操作系统执行任务切换时也需要在硬件栈帧之外保存其余必要上下文。
生成异常返回信息
保存旧 LR 后,硬件 为即将执行的处理程序设置一个新的 LR 值,称为 EXC_RETURN。
EXC_RETURN 编码了异常返回需要的信息,包括返回 Thread mode 还是 Handler mode,以及从哪个栈恢复上下文;存在浮点扩展时,它还区分相应的栈帧类型。处理器随后可依据这一编码执行异常返回,其具体取值与检查规则在 B1.5.8 中展开。

对于熟悉 ARMv7-A 的读者,需要注意两种架构在异常返回机制上的区别。在 ARMv7-A 的传统异常处理机制中,异常返回地址相关的值保存在对应异常模式的 LR 中,返回时可能需要根据异常类型进行偏移修正。而在 ARMv7-M 中,异常进入时,硬件会将返回地址自动保存到异常栈帧,并向 LR 写入 EXC_RETURN 编码,用于指示异常返回时的处理器模式、所用栈及栈帧类型。
注意!!这里讨论的是异常处理机制。对于普通函数调用,ARMv7-M 仍然使用 LR 保存函数返回地址。
这里同时存在两个不同的值:栈帧中的 LR 属于被打断程序,处理程序入口处的 LR 则是硬件生成的 EXC_RETURN。 若处理程序需要长期保留 EXC_RETURN,还应在后续函数调用覆盖 LR 前妥善保存。

