解码 ARM Cortex-Mx 异常入口与退出

发布: (2025年12月27日 GMT+8 22:59)
11 min read
原文: Dev.to

I’m sorry, but I don’t have the ability to retrieve or view the content at the external link you provided. If you can paste the text you’d like translated here, I’ll be happy to translate it into Simplified Chinese while preserving the original formatting.

介绍 – 为什么会有这篇文章

ARM Cortex‑M x 上的中断处理在纸面上看起来很简单,但一打开调试器就会变得令人困惑。

  • PC 值神秘地变化
  • 寄存器出现在堆栈内存中
  • LR 保存奇怪的值,如 0xFFFFFFFD
  • 某些寄存器从未出现在堆栈上

本文通过真实的调试截图和内存检查,拆解硬件实际执行的操作、编译器的行为以及调试器隐藏的细节。

在深入堆栈转储和寄存器之前,必须先了解一件事:
核心 决定何时接受中断保存固定的体系结构上下文,并 自动切换模式和堆栈。软件仅在此之后介入。

什么是 STIR

  • STIR 并不会直接跳转到 ISR。
  • 仅仅设置该中断的挂起位
  • STIR 的行为与硬件中断线拉高完全相同——它仅仅将中断标记为挂起。

Step‑by‑Step Exception Entry

  1. NVICNVIC_ISER 寄存器中设置挂起位。
  2. CPU 完成当前正在执行的指令。
  3. 堆栈压入(将寄存器内容压入堆栈)和 向量获取(从向量表读取处理程序地址)。
  4. CPU:
    • 切换到 Handler 模式
    • NVIC_IABR 中设置 Active 位。
    • 清除 Pending 位。
  5. ISR 开始执行。
  6. MSP 用于处理程序内部的所有堆栈操作。

寄存器的堆栈压入、模式切换以及向量获取都是 在两条指令之间由核心内部完成 的,这就是为什么在源代码级单步调试时看不到这些步骤。

关于 STIR 写入的常见误解

观察实际情况
“向 STIR 写入会直接跳转到 ISR。”写入 设置 pending 位。
“中断会在写入后立即被触发。”核心 必须先完成 当前指令。
“Pending = taken。”Pending 表示可触发taken 表示核心已经进入 ISR。

设置 Pending 位之后

  • Cortex‑M 核心 始终会完成当前正在执行的指令
  • 中断只能在 指令边界 被识别,绝不会在指令执行中途触发。
  • 这保证了 精确且确定的 程序执行。

后果

  • 程序计数器 (PC) 会继续更新已在进行的指令。
  • 调试器可能会在源代码视图中高亮下一条 C 语句,看起来像是执行正常进行。
  • 只有在当前指令结束后,异常入口才会发生。

调试器示例

在下面的截图中,调试器停在了 printf 语句处:

  1. 中断仍然 挂起
  2. CPU 完成当前指令后,PC 更新,然后 发生异常入口

此时:

  • 异常入口已完成。
  • 处理器现在在 Handler 模式 下执行 中断服务例程 (ISR)
  • PC 已从向量表加载。
  • LR 包含一个 EXC_RETURN 值。
  • MSP 处于活动状态。
  • 中断 不再挂起,相应的 NVIC 活动位 已被置位。

Source:

检查堆栈帧

当 ISR 运行时,我们可以查看堆栈内存,以了解处理器自动保存的上下文。

自动入栈的寄存器

xPSR, PC, LR, R12, R3, R2, R1, R0

初始堆栈指针

  • 在中断被服务之前,SP = 0x2001FFE8
  • 堆栈是 全降序(向更低地址增长;SP 始终指向最后入栈的项目)。

硬件入栈顺序

步骤SP 递减后的值入栈寄存器
10x2001FFE4xPSR
20x2001FFE0PC
30x2001FFDCLR
40x2001FFD8R12
50x2001FFD4R3
60x2001FFD0R2
70x2001FFCCR1
80x2001FFC8R0
  • 异常入口后,SP = 0x2001FFC8,指向最后入栈的寄存器(R0)。
  • 示例:R0 = 0x0A —— 在寄存器视图以及内存地址 0x2001FFC8 处均得到验证。
  • 地址 0x2001FFE4 处的值对应 xPSR,确认布局符合 ARM Cortex‑M 规范。

为什么我们只在栈上看到 R0–R3、R12、LR、PC 和 xPSR?

乍一看似乎缺少了什么,但 ARM 会根据 谁负责保存寄存器 来有意地区分寄存器的处理方式。

易失寄存器(调用者保存)

寄存器典型用途
R0–R3、R12函数参数、临时计算、短暂使用的值
  • 这些寄存器预期会经常改变。
  • 如果发生中断,这些值很可能是临时的,因此 硬件必须保存它们
  • 因此 Cortex‑M 核心在异常入口时 会自动保存 这些寄存器。

非易失寄存器(被调用者保存)

寄存器典型用途
R4–R11局部变量、循环计数器、指针、结构体、必须在多条指令之间保持的值
  • 软件负责保存这些寄存器。
  • 编译器会生成代码在 ISR 实际使用它们时才 push/pop R4–R11
  • 如果 ISR 不需要这些寄存器,它们根本不会被压栈,从而节省栈空间和时间。

为什么 ARM 这样设计

  • 低中断延迟 – 自动完成的工作最少。
  • 最小堆栈使用 – 只保存必要的寄存器。
  • 可预测的时序 – 硬件定义的堆栈帧是固定且快速的。
  • 快速上下文切换 – 核心可以在几条指令内进入/退出 ISR。
  • 编译器灵活性 – 编译器处理其余工作,只压栈 ISR 真正需要的内容。

TL;DR

  1. STIR → pending bit(不立即跳转)。
  2. Core 完成当前指令,随后执行 异常入口(硬件堆栈、模式切换、向量获取)。
  3. 硬件自动保存 R0‑R3、R12、LR、PC、xPSR。
  4. 软件(编译器)仅在需要时保存 R4‑R11。
  5. 调试器可能会隐藏这些内部步骤,使得流程看起来怪异,但该顺序是确定的,并在 ARM Cortex‑M 架构手册中有文档说明。

Source:

Cortex‑M x 异常返回

为什么硬件不会在每次异常时保存所有寄存器

如果硬件在每次异常时都保存 所有 寄存器,Cortex‑M x 的速度会大幅下降,且不太适合实时系统。

什么是 EXC_RETURN

  • 在异常入口期间放入 LR(链接寄存器)的特殊值。
  • 将该值写入 PC 会触发异常返回。
  • 不是 普通的返回地址;它告诉处理器如何从异常中返回。

使用 LR 中的值的典型返回指令:

BX   LR
POP {PC}
LDR  PC, [addr]

重要说明 – 与普通的 C 函数调用不同,异常机制会在 LR 中存放特殊值 EXC_RETURN

EXC_RETURN 编码

所有 EXC_RETURN 值的位 [31:5] = 1
只有低几位描述返回行为,处理器会自动解码。

描述值 / 含义
[31:5]EXC_RETURN 标识永远为 1 → 标识这是异常返回值
4浮点上下文1 → 未堆栈浮点上下文
0 → 已堆栈浮点上下文(仅在存在 FPU 时)
3返回模式1 → 返回到 Thread 模式
0 → 返回到 Handler 模式
2堆栈指针选择1 → 使用 PSP(进程堆栈指针)
0 → 使用 MSP(主堆栈指针)
1保留永远为 0
0保留永远为 1

异常入口期间会发生什么

  • Cortex‑M 处理器在硬件中完成 堆栈向量获取
  • 程序计数器 (PC) 看似瞬间改变——你看不到各个堆栈步骤。
  • 内存视图会显示已保存的寄存器,而源码视图则没有。

简而言之,调试器显示的是 异常入口的结果,而不是导致该结果的硬件步骤。

我是如何验证该行为的

方法观察
STIR 触发中断确认硬件发起的异常入口
调试器寄存器视图寄存器被正确保存
堆栈内存检查只保存了固定的异常帧;SP 正好移动 32 字节
编译器输出检查仅在需要时保存 R4–R11

结论: EXC_RETURN 是一个紧凑的、由硬件生成的标记,告诉 Cortex‑M 核心如何展开异常(使用哪个堆栈指针、返回到哪个模式以及是否存在浮点状态)。处理器自动完成所有低层的堆栈/出栈操作,调试器显示的是最终的堆栈状态,而不是每一步硬件操作。

Back to Blog

相关文章

阅读更多 »

纠正代码与文本之间的不匹配

Forem 信息流 !Forem 标志 https://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.co...

Vim

1. 匹配光标下单词的所有出现位置 vim