目录
[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} 压入))
[4.2 第二个 LR](#4.2 第二个 LR)
[位于中断栈帧中的 0x20001FC8(由硬件自动压入)](#位于中断栈帧中的 0x20001FC8(由硬件自动压入))
[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内部还会调用其他函数(如PutDataResult、xSemaphoreTake等),这些调用会覆盖 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后的状态。