栈的简单理解
个人根据韦东山老师的课的一些笔记和个人理解,分享交流
栈:也是一块内存空间 cpu的sp寄存器指向它 可以用在函数调用 局部变量 多任务系统里
栈 (Stack):它像自动传送带。由 CPU 的 SP 寄存器控制,函数一调用,变量自动入栈;函数一结束,变量自动出栈。它不需要你写 free,非常快,但空间通常比较小。
SP 全称是 Stack Pointer,即 栈指针寄存器。
它是一个 CPU 内部的专用寄存器,里面存储的是一个内存地址。
这个地址指向当前栈空间的“最顶端”



a函数去调用b和c函数,其本质就是去调用BL指令
BL指令最终实现两个功能,一个是LR,是返回地址,就是下一条指令的地址
然后PC等于函数a的地址,这里就是去调用函数a,去调用函数b函数c也是同理


涉及三个问题,首先就是LR被覆盖了怎么办
第二就是局部变量在栈中如何去分配
为何每个rtos任务都有自己的栈
Q1:首先就是LR被覆盖了怎么办
问题一:以调用函数a为例,在转到函数a的时候,知道需要去修改LR,所以先提前去保存LR,在入口处就去保存LR
划出自己的栈,保存LR进栈里,保存局部变量

注意的是这个b和c是a去调用,同时去调用两个,先b搞完后就出栈了,然后就排到c去用
这些R0等就是寄存器
| 阶段 | SP 状态 | 栈内存动作 |
|---|---|---|
| 进入 a_func | SP = A - offset | PUSH {LR},保存 main 的返回地址。 |
| 调用 b_func | SP 进一步减小 | 划出 b 的栈帧,存 b 的局部变量和 LR。 |
| b_func 结束 | SP 增加(弹回) | b 的栈帧被逻辑销毁,SP 回到 a_func。 |
| 调用 c_func | SP 再次减小 | 复用之前 b 所在的地址空间。 |
| a_func 结束 | SP 回到 A | POP {PC},直接把栈里存的 LR 弹给 PC,跳回 main。 |
Q2:局部变量

由这个图可以看出栈是如何给局部变量分配空间的
首先i没有加volatile,它在栈给你分配了空间,然后那些没加volatile的它就优先使用寄存器表示,这就是局部变量在栈的分配
相当于没有优化的这些是有写入内存里的,而且是有给它分配地址的,然后那些没有进内存的,就只在寄存器里
编译器为了追求效率,并不会给所有的局部变量都在栈(内存)里开辟空间:
-
优先使用寄存器:对于
ch、uch、buf这些没有加volatile的变量,编译器认为它们是“临时”的。直接用 R5 (ch)、R6 (uch)、R4 (buf) 这种快速的寄存器来存放,读写速度最快。 -
被迫进入栈空间:对于加了
volatile的变量i,编译器被禁用了优化逻辑。它必须在栈内存中给i找个位置(图中sp = sp - 20划出的空间之一),每次用到i都要通过STR或LDR真真实实地去读写内存,而不是只在寄存器里转悠。
1. 那些只在寄存器里的变量:没有“固定地址”
正如你观察到的,像 ch、uch 这种变量,如果编译器觉得寄存器够用,它们可能自始至终都不会进入内存。
-
无地址性:在 C 语言层面你虽然可以定义它们,但在底层,它们没有一个具体的内存地址(比如
0x2000...),它们的名字就是 R5 或 R6。 -
速度极快:CPU 访问寄存器只需要 1 个时钟周期,而访问内存(即便有缓存)通常也要快得多。
-
风险:如果你尝试对这种变量取地址(比如
&ch),编译器就会“被迫”给它在栈上分配一个空间,因为它必须得有个地址给你。
2. 写入内存的变量:拥有“身份证号”
对于你图中加了 volatile 的 i,或者是因为函数调用需要被 PUSH 的寄存器:
-
分配地址:它们在栈内存中有了明确的坐标(即
SP + 偏移量)。 -
可见性:因为有了内存地址,其他的硬件单元(比如 DMA)或者中断函数(通过地址指针)才能找得到它们。
-
确定性:即便 CPU 现在很忙,这个值也会稳稳地存在内存条/SRAM 里,不会因为寄存器被挪作他用而消失。
. 局部变量与寄存器的“分配”
编译器会计算你函数里有多少个活跃的局部变量:
-
变量少时:如果你只有 2 个局部变量(如
ch和uch),编译器可能只选择PUSH {r4, r5, lr}。 -
变量多时:如果你定义了 10 个局部变量,寄存器 R4-R11 全被占满了,你会看到
PUSH {r4-r11, lr}。 -
寄存器不够用时:如果寄存器全用完了变量还没存完,编译器就会通过
SUB sp, sp, #offset在栈上划出更多的空间
3. 栈管理的“本质”总结
你可以把栈想象成一个伸缩尺:
-
向下拨动尺子:就是在内存里占位。尺子划过的这段空间,现在属于当前函数,别人不能用。
-
向上收回尺子:就是宣布这段内存“我用完了”。虽然内存里的数据还在(并没有被抹除),但因为 SP 已经移走了,下次调用别的函数时,SP 再次向下移动,就会直接覆盖掉旧数据。这就是为什么你理解的“自动释放”是如此高效。
4. 你的理解在 RTOS 中的应用
你提到过“多任务系统”,在 FreeRTOS 中,所谓的“切换任务”,本质上就是:
-
把当前的 SP 指针保存起来。
-
把 CPU 的 SP 寄存器强制改为另一个任务上次保存的地址。 只要 SP 移动了,CPU 面对的就是一套全新的局部变量和返回地址。
Q3:为何每个RTOS任务都要有自己的栈
假设现在有两个任务
rtos本质就一个任务运行一阵子然后去切换到另一个任务,然后不断去重复这个过程
这个过程它在切换时会保存前面任务,然后去恢复要开始的这个任务
保存和恢复的是现场,就是那些寄存器什么的
可以根据这个生活中看书的例子去辅助理解

恢复A的时候,先找到A的结构体,然后得到A的SP,那么这些保存的现场是存在内存里的,现在地址也找到了,直接把这些寄存器的值写入硬件,然后再返回pc的值,那不就可以接着往下运行了,然后那些寄存器的值也都已经恢复
综上所述,每个任务都要有自己的栈,每个任务都有自己的调用关系,自己的局部变量,即便是调用同一个函数也是保存在自己的栈,现场这些也是保存在自己的栈内
RTOS 任务切换与栈管理笔记总结
1. 核心本质:RTOS 是时间的“分时复用”
-
运行逻辑:RTOS 并不是让多个任务真正同时运行,而是让每个任务运行一小段时间,通过极快的切换频率(Tick),在宏观上产生“并行”的错觉。
-
切换核心:切换的本质就是现场的保存与恢复。当任务 A 被挂起时,CPU 必须记住它此刻的所有状态,以便下次无缝接轨。
2. 现场 (Context) 到底是什么?
-
寄存器集合:现场就是 CPU 内部那一套正在变动的数据,包括通用寄存器(R0-R12)、栈指针(SP)、链接寄存器(LR)和程序计数器(PC)。
-
生活类比:就像看书看到一半去接电话,你必须记住“读到了哪一页(PC)”以及“刚才想到了什么(寄存器数据)”,而书签就是记录这些信息的载体。
3. 为什么必须“每个任务都有自己的栈”?
-
独立领地:每个任务都有自己的调用关系(栈帧)和局部变量。如果共用一个栈,任务 A 压入的返回地址会被任务 B 的局部变量踩掉,导致程序崩溃。
-
私有化存储:
-
局部变量:保存在各任务栈中。
-
现场状态:在切换瞬间,所有的寄存器值都会
PUSH进入当前任务的私有栈。 -
栈顶地址 (SP):每个任务的 SP 最终记录在它的任务控制块 (TCB) 中。
-
4. 切换过程的底层三部曲
-
保存:当前任务执行
PUSH,将 R0-R15 的状态全部存入其私有栈内存。 -
转场:从当前任务的 TCB 记录下 SP 地址;然后找到下一个任务的 TCB,取出它上次保存的 SP 值并赋给 CPU 的 SP 寄存器。
-
恢复:CPU 根据新的 SP 指针执行
POP,将栈内的旧数据还原回硬件寄存器。当 PC 寄存器被弹出时,程序瞬间“闪现”回另一个任务断点处。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)