函数栈帧与压栈出栈
栈模型
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-R3R12LRPCxPSR
这组自动压栈的现场,就是异常返回时要恢复的最小上下文。
所以你说的“把当前 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 才会把MSP和PSP分开:MSP给内核和中断用,PSP给线程用。
这样线程跑线程的栈,中断跑中断的栈,任务切换的时候只要保存和恢复各自的栈指针就行。
PC负责跳转,向量表第 1 个字是初始栈顶,第 2 个字是入口地址;在 STM32 里,程序一般从0x08000000开始,栈顶一般在0x20000000 + SRAM 大小。
函数调用和中断现场保存,配合看 CPU 通用寄存器组