函数栈帧与压栈出栈

← 返回 Cortex-M4内核原理 | ← 主页

← CPU 通用寄存器组


栈模型

Cortex-M 用的是 Full-Descending(满递减栈)

  • SP 永远指向当前栈顶的有效数据位置
  • 递减:压栈时 SP 往低地址走,比如 0x20001234 -> 0x20001230

压栈就是先把 SP 往下挪,再把数据写进去;出栈就是先读数据,再把 SP 往上挪。


栈帧

每次函数调用,CPU 都会在栈里开一块新的空间,这块空间就叫 栈帧(Stack Frame)

  • SP 负责跟着压栈、出栈移动
  • BP / R7 负责固定住当前函数栈帧的基准位置
  • 局部变量、返回地址、临时保存的寄存器,都会放到这块栈帧里

编译器在函数入口通常会先做一次 push,把要保留的东西压到栈里,再把 R7 之类的基准寄存器设好。


函数调用为什么能返回

函数调用时,上一层函数的“下一条指令地址”会先被保存起来,最常见的是放到 LR

流程可以理解成:

调用函数 -> 保存返回地址到 LR -> 执行函数体 -> 函数结束 -> 跳回 LR 里的地址
  • 非叶子函数:通常要先把 LR 压栈,因为它还会再调用别的函数
  • 叶子函数:如果不再调用别的函数,常直接 BX LR 返回

压栈和出栈

压栈

  • 保存函数返回地址
  • 保存当前函数要继续用的寄存器
  • 保存局部变量或临时量

出栈

  • 恢复之前保存的寄存器
  • 恢复 SP
  • 非叶子函数通过恢复出来的返回地址跳回去

你可以把它理解成:

进函数时先把现场收起来,出函数时再原样放回去。


为什么中断里会牵扯到 MSP 和 PSP

你这段理解可以整理成一句更准的话:

如果只有一个 SP,中断一来,当前线程正在用的栈和中断现场就会混在一起;恢复的时候虽然也能压栈、出栈,但线程栈和中断栈不分开,不适合操作系统做任务切换。

所以 Cortex-M 设计了两个栈指针:

  • MSP:主栈指针,给内核和异常/中断用
  • PSP:进程栈指针,给线程/任务用

这样就变成:

  • 线程自己跑的时候,走 PSP
  • 中断进来以后,硬件和内核走 MSP
  • 任务切换时,只需要保存/恢复各自的栈指针,就能把线程现场记住

这就是为什么操作系统里要有两个栈指针。


中断进入时硬件到底干了什么

异常/中断进来时,Cortex-M 会自动把一部分现场压栈,主要是:

  • R0-R3
  • R12
  • LR
  • PC
  • xPSR

这组自动压栈的现场,就是异常返回时要恢复的最小上下文。

所以你说的“把当前 CPU 的通用寄存器组和特殊寄存器压在那个线程栈里,然后把这个 SP 跳转到主栈里”,可以更准确地理解成:

  • 线程的运行现场要保存
  • 中断要有自己的栈空间
  • 任务切换时,要能把线程现场从 PSP 对应的栈里恢复回来

PC 和向量表

PC程序计数器(Program Counter),里面存的是下一条要执行的指令地址

  • CPU 执行完当前指令后,就去 PC 指向的地址取下一条
  • 所以往 PC 里写一个地址,本质上就是跳转

复位时,CPU 会去向量表里取两个关键值:

  • 第 1 个字:初始 MSP,也就是栈顶指针
  • 第 2 个字:Reset_Handler 地址,也就是程序入口

在 STM32 里,程序通常放在 0x08000000,栈顶通常放在 0x20000000 + SRAM 大小 的末尾。

所以你可以记成:

  • Flash 里放程序
  • SRAM 末尾放栈顶
  • 向量表前两个字决定“先把栈指到哪,再从哪开始跑”

你的这段原话,整理后可以直接记成

如果只有一个 SP,中断一来,线程现场和中断现场就混在一起了。
所以 Cortex-M 才会把 MSPPSP 分开:MSP 给内核和中断用,PSP 给线程用。
这样线程跑线程的栈,中断跑中断的栈,任务切换的时候只要保存和恢复各自的栈指针就行。
PC 负责跳转,向量表第 1 个字是初始栈顶,第 2 个字是入口地址;在 STM32 里,程序一般从 0x08000000 开始,栈顶一般在 0x20000000 + SRAM 大小


函数调用和中断现场保存,配合看 CPU 通用寄存器组