基础知识——ARM M核入栈出栈流程

目录

[1. 前提:Cortex-M 的栈和模式](#1. 前提:Cortex-M 的栈和模式)

[2. 任务函数执行时的栈布局(使用 PSP)](#2. 任务函数执行时的栈布局(使用 PSP))

[2.1 push {r3, r4, r5, lr} 的作用](#2.1 push {r3, r4, r5, lr} 的作用)

[2.2 ARM 调用约定(AAPCS)规定](#2.2 ARM 调用约定(AAPCS)规定)

[3. 中断发生时的硬件自动压栈](#3. 中断发生时的硬件自动压栈)

[4. 栈中两个LR的区别](#4. 栈中两个LR的区别)

[4.1 第一个 LR](#4.1 第一个 LR)

[位于 0x20001FE0(由 push {r3, r4, r5, lr} 压入)](#位于 0x20001FE0(由 push {r3, r4, r5, lr} 压入))

(1)它是什么?

(2)它有什么用?

(3)总结

[4.2 第二个 LR](#4.2 第二个 LR)

[位于中断栈帧中的 0x20001FC8(由硬件自动压入)](#位于中断栈帧中的 0x20001FC8(由硬件自动压入))

(1)它是什么?

(2)它有什么用?

(3)注意区分

[5. POP弹出返回值](#5. POP弹出返回值)


1. 前提:Cortex-M 的栈和模式

  • 双栈指针:MSP(主栈)和 PSP(进程栈)

  • 双模式

    • 线程模式 (Thread Mode):运行普通任务代码,可使用 PSP 或 MSP(RTOS 任务通常用 PSP)

    • 处理模式 (Handler Mode) :运行中断/异常代码,必须使用 MSP

2. 任务函数执行时的栈布局(使用 PSP)

假设任务 test_Thread 已经运行了一段时间,PSP 指向任务栈的当前栈顶(最低地址)。

以子函数 test_saveDataResult 为例,看如何出入栈的。一般test_saveDataResult开头可能有类似 push {r3, r4, r5, lr}这样的语句,这会让 PSP 减小 16 字节(4 个寄存器)。

此时任务栈内容(简化):

复制代码
高地址 (初始栈底)
...
[ 跳转前的其他栈帧数据 ]
[ 保存的 lr ]
[ 保存的 r5 ]
[ 保存的 r4 ]
[ 保存的 r3 ]   <-- 这是 push 后的 PSP 位置(最低地址)
低地址

PSP 指向这个最低地址(即保存 r3 的位置)。

2.1 push {r3, r4, r5, lr} 的作用

push {r3, r4, r5, lr} 出现在一个函数的开头,意思是 当前函数(即被调用者) 将会使用 R4、R5、R6 这三个寄存器,并且它内部还会调用其他函数(因此需要保存 LR)。这里的"被调用者"指的是 这个函数本身

这条指令将 4 个寄存器 压入当前栈(PSP 或 MSP):

  • r3:通常是一个通用寄存器,在函数内部可能被用作临时变量或保存某个值。

  • r4, r5 :属于 被调用者保存的寄存器(callee‑saved)。函数如果修改它们,必须在退出前恢复原值,所以入口处先压栈保存。

  • lr (链接寄存器):保存函数的 返回地址(即调用该函数的下一条指令地址)。因为函数内部可能还会调用其他函数(会覆盖 lr),所以必须先将 lr 保存到栈中,以便函数结束时能正确返回。

执行 push 后,栈指针(SP)减小 16 字节(4×4),函数主体代码可以使用 r3、r4、r5 而不用担心破坏调用者的数据。函数末尾通常有 pop {r3, r4, r5, pc}pop {r3, r4, r5, lr} ; bx lr 来恢复寄存器并返回。

2.2 ARM 调用约定(AAPCS)规定

类型 寄存器 谁负责保存
参数/临时 r0‑r3, r12 调用者保存(caller‑saved)
被调用者保存 r4‑r11 被调用者保存(callee‑saved)
链接寄存器 r14 (LR) 特殊:若函数内再调用其他函数,需保存
栈指针 r13 (SP) 专用
程序计数器 r15 (PC) 专用

规则:

  • 被调用者保存的寄存器(r4‑r11):如果函数要修改它们,必须在入口处压栈,出口处恢复。

  • 链接寄存器(LR):如果函数内部会调用其他函数(非叶子函数),则 LR 会被 bl 覆盖,所以必须把原始的返回地址压栈保存;如果是叶子函数(不调用任何函数),则可以直接使用 LR 返回,不需要压栈。

3. 中断发生时的硬件自动压栈

中断到来时,CPU 硬件基于 当前使用的 SP (这里是 PSP)执行以下步骤:

  • PSP = PSP - 32(为 8 个寄存器腾出空间)。

​​​​​​​将 8 个寄存器按顺序写入 从新 PSP 开始的地址。

  • 按照 Cortex-M 的硬件设计,压栈顺序为:

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

其中 R0 保存在最低地址(即新 PSP 指向的位置),xPSR 保存在最高地址(PSP+28)。

压栈完成后,PSP 指向保存 R0 的位置(最低地址)。

  • 硬件自动将 SP 切换为 MSP (进入处理模式),并将 LR 设置为 EXC_RETURN 值(如 0xFFFFFFFD),然后跳转到中断向量。

此时:

  • PSP 的值被冻结(指向任务栈中那 8 个寄存器的栈帧底部)

  • MSP 指向系统栈(独立空间),后续中断函数使用 MSP

内存布局变为:

复制代码
高地址
+--------------------------------+  <- 0x20002000 (栈底)
|    调用者 task 的栈帧           |
|   (局部变量、保存的寄存器等)     |
+--------------------------------+
|  (可能空闲)                     |
+--------------------------------+  <- 0x20001FE0
|  返回地址 (LR 被 push 保存)      |  ← test_saveDataResult 的返回地址
+--------------------------------+  <- 0x20001FDC
|  R5                            |
+--------------------------------+  <- 0x20001FD8
|  R4                            |
+--------------------------------+  <- 0x20001FD4
|  R3 (函数保存的,与中断无关)      |  ← 中断前的 PSP 指向这里
+--------------------------------+
|  (可能的局部变量空间)            |
+--------------------------------+  <- 0x20001FB4 (中断后的 PSP)
|  R0 (中断时的值)                |
+--------------------------------+  <- 0x20001FB8
|  R1                            |
+--------------------------------+  <- 0x20001FBC
|  R2                            |
+--------------------------------+  <- 0x20001FC0
|  R3                            |  
+--------------------------------+  <- 0x20001FC4
|  R12                           |
+--------------------------------+  <- 0x20001FC8
|  LR (中断时的 LR)               |
+--------------------------------+  <- 0x20001FCC
|  PC (中断时的 PC)               |  ← 中断返回地址
+--------------------------------+  <- 0x20001FD0
|  xPSR                          |
+--------------------------------+
低地址

4. 栈中两个LR的区别

4.1 第一个 LR

位于 0x20001FE0(由 push {r3, r4, r5, lr} 压入)

(1)它是什么?

这是 test_saveDataResult 函数入口处 通过 push {r3, r4, r5, lr} 压入栈的 返回地址

这个 LR 的值等于 调用 test_saveDataResult 的上层函数(调用者)中的下一条指令的地址

也就是说,当 test_saveDataResult 执行完毕后,需要返回到调用者的这个地址继续执行。

(2)它有什么用?

  • 函数返回 :在 test_saveDataResult 的末尾,会执行 pop {r3, r4, r5, pc}pop {r3, r4, r5, lr}; bx lr

    如果是 pop {r3, r4, r5, pc},则栈中保存的这个 LR 值会直接被弹出到 PC ,实现函数返回。

    如果是先弹出到 LR 再 bx lr,效果一样。

  • 保存原因 :因为 test_saveDataResult 内部还会调用其他函数(如 PutDataResultxSemaphoreTake 等),这些调用会覆盖 LR 寄存器的值。所以必须在函数入口就把原始返回地址保存到栈中,以便最后恢复。

(3)总结

这个 LR 是 当前函数(被调用者)的返回地址,由函数序言压栈,函数尾声弹出,用于返回到调用者。


4.2 第二个 LR

位于中断栈帧中的 0x20001FC8(由硬件自动压入)

(1)它是什么?

这是 中断发生时刻 ,CPU 硬件自动压入任务栈的 当时的 LR 寄存器值

当中断到来时,CPU 正在执行 ins_saveDrResult 函数(例如在 ldr r3, [pc, #36] 之后,ldr r0, [r3] 之前)。

此时 LR 寄存器中保存的是 当前函数(ins_saveDrResult)内部某个位置的值 ,具体取决于当前执行点。

通常,在函数执行过程中,LR 可能已被修改(例如在调用子函数时被覆盖,或者被用作临时变量),但在中断发生那一刻,硬件会将 LR 的当前值原样压栈。

(2)它有什么用?

  • 中断返回 :当中断服务程序执行完毕,硬件执行中断返回时,会从栈中弹出这个 LR 值到 LR 寄存器。

    但是,中断返回后并不是直接用这个 LR 返回(因为返回地址已经在 PC 栈帧中),而是需要依靠 EXC_RETURN 机制以及栈中的 PC 值来恢复被中断的指令地址。

    实际上,硬件自动压栈的 LR 主要作用是 在嵌套中断时保存被中断代码的 LR,以便在中断返回后,被中断的代码能够继续正确执行(尤其是当被中断的代码本身也是一个函数,且它有自己的返回地址需要保持时)。不过对于单层中断,这个 LR 在返回后通常不被直接使用,它的存在是为了维持栈帧的完整性和可能的异常处理(比如 Fault 发生时,可以通过 LR 回溯调用链)。

  • 调试/异常分析:在 MemManage Fault 处理函数中,可以通过查看压栈的 LR 值,知道中断发生时正在执行哪个函数的哪条指令附近,有助于定位问题。

(3)注意区分

中断栈帧中的 LR 不是 函数的返回地址,而是 被中断那一刻的 LR 寄存器值

而函数返回地址保存在前面第一个 LR 中(由 push 保存的)。中断发生后,ins_saveDrResult 尚未返回,所以它的返回地址依然安全地保存在栈的高地址处(0x20001FE0),不会被中断破坏。

5. POP弹出返回值

函数入栈一开始push了返回地址,那么处理器SP有可能因为局部变量的存在,SP还会减小,那么POP时,它怎么直到原来push的位置在哪里?从而拿来lr到pc返回继续执行??

假如初始 SP = 0x20001000

  • push {r3, r4, r5, lr} → SP = 0x20000FF0(存放了 lr 在 0x20000FFC
  • sub sp, #8 → SP = 0x20000FE8(局部变量区)
  • ... 函数体 ...
  • add sp, #8 → SP = 0x20000FF0(回到 push 后的位置)
  • pop {r3, r4, r5, pc} → 依次弹出:
    • 0x20000FF0 弹出到 r3

    • 0x20000FF4 弹出到 r4

    • 0x20000FF8 弹出到 r5

    • 0x20000FFC 弹出到 pc(返回地址)

      SP 最终变为 0x20001000

因此,只要步骤 4 的 add sp, #8 与步骤 2 的 sub sp, #8 匹配,pop 就能正确工作。

push压栈,存返回地址 -> 如果还需要栈空间,则栈空间分配(减小 SP)-> 函数体就涉及到局部变量的数据写入(向栈内存地址赋值,SP+偏移递增使用)-> 执行完毕,SP加回分配的大小,这样SP就是push完的地址 -> pop弹出就会对应上LR。

补充

栈 分配后通常是向高地址方向(地址增加)使用。

因为:

  • 分配栈空间,SP自减后 指向 最低地址(栈顶)。

  • 访问局部变量时,编译器生成的代码通常使用 正偏移,例如:

    • str r0, [sp, #0] → 写入地址 SP + 0

    • str r1, [sp, #4] → 写入地址 SP + 4(更高地址)

    • str r2, [sp, #8] → 写入地址 SP + 8

  • 所以随着偏移增大,访问的地址逐渐 增加(向栈底方向,即原来栈的高地址)。

因此,从栈顶向栈底(地址增加方向)使用这片空间, 注意是 SP加偏移使用**,**SP本身没变,这样才能pop时加当时分配时减的值,回到入栈push后的状态。

相关推荐
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第77题】【Mysql篇】第7题:回表查询与全表扫描的区别?
java·开发语言·数据库·mysql·面试
水木流年追梦3 小时前
大模型入门-大模型分布式训练2
开发语言·分布式·python·算法·正则表达式·prompt
_kerneler3 小时前
arm虚拟机实时性优化总结
arm开发
罗超驿4 小时前
5.Java线程创建全攻略:5种写法 + 高频面试题解析
java·开发语言·java-ee
Simon523144 小时前
反射------5.26学习小计
java·开发语言·spring boot
ComputerInBook4 小时前
C++ 23 相比 C++ 20 新增之特征
开发语言·算法·c++23
一知半解仙4 小时前
Claude Code的跨平台安装教程
java·开发语言·人工智能·开源
代钦塔拉4 小时前
C++ auto
开发语言·c++
csdn_aspnet4 小时前
java 算法 LeetCode 编号 70 - 爬楼梯
java·开发语言·算法·leetcode