栈的概念(韦东山学习笔记)
栈核心问题 (针对 LR 覆盖、局部变量分配、RTOS 任务栈 )
一、知识总览
这部分聚焦栈在函数调用、RTOS 任务中的核心逻辑,解决 3 个关键问题:LR 被覆盖如何处理、局部变量在栈里咋分配、RTOS 为啥每个任务要有独立栈。理解这些,才能搞懂程序执行流程、任务切换的本质,尤其是 RTOS 上下文管理的底层逻辑。
二、核心问题分步拆解
(一)问题 1:LR 被覆盖了怎么办?
在函数嵌套调用场景里,内层函数执行
BL
指令(函数调用指令 )会覆盖外层函数的 LR
(Link Register,存储返回地址 ),若不处理,函数返回时执行流向会混乱。结合 上图 拆解解决逻辑:
LR 的作用:
LR
存储函数执行完后要返回的地址。比如main
调用a_func
时,执行BL a_func
(图 中0x080001ba: BL a_func ; 0x8000154
),会把main
中下一条指令地址存入LR
,保证a_func
执行完能回到main
继续运行。LR 被覆盖的原因:
函数嵌套调用(如main
→a_func
→b_func
)时,内层函数的BL
指令会覆盖外层函数的LR
。像a_func
调用b_func
执行BL b_func
,会把a_func
里 “返回给main
的地址” 覆盖成 “返回给a_func
的地址”,若不处理,b_func
执行完就无法正确回到main
。解决方法:栈保存与恢复:
- 压栈保存(函数入口 ):
函数开头用PUSH
指令存LR
到栈。以a_func
为例(图 ),PUSH {r0, lr}
会把当前R0
(通用寄存器 )和LR
(返回地址 )压入栈。这样即便内层函数覆盖LR
,栈里仍留存外层函数正确的返回地址。 - 出栈恢复(函数出口 ):
函数结尾用POP
指令恢复LR
到PC
(Program Counter,控制程序执行流向 )。如a_func
的POP {r3, pc}
,从栈中取出之前保存的LR
值并赋给PC
,让程序回到正确调用点(如main
)。
总结:通过 “函数入口
PUSH
存LR
+ 函数出口POP
恢复PC
”,借助栈的 “后进先出” 特性,解决多层函数调用时LR
被覆盖问题,确保函数嵌套调用后执行流能正确回归。图直观呈现了PUSH
保存LR
、POP
恢复执行流的汇编指令与栈操作对应关系,是理解该机制的关键 。- 压栈保存(函数入口 ):
(二)问题 2:局部变量在栈中如何分配?
函数内的局部变量(如
main
里的 char ch = 65; int i = 99;
),依托栈指针(SP
)调整和内存布局规则分配,结合 上图 解析:
栈指针调整:预留空间:
函数执行前,编译器生成指令调整SP
(栈指针 )。以main
函数为例(上图 ),汇编可能有SP = SP - 20
操作,向 低地址方向扩展栈帧(ARM 栈通常向低地址增长 ),为局部变量、保存的寄存器预留内存。变量填充:规则与对齐:
编译器按 “声明顺序 + 内存对齐” 把局部变量填入栈帧。先声明的变量地址更靠近SP
原位置(地址 “高” ),后声明的更远离(地址 “低” );同时满足内存对齐(如int
占 4 字节,按 4 字节对齐存储 )。
比如上图中,char ch = 65
(1 字节 )、int i = 99
(4 字节 ),编译器会严格安排它们在栈里的位置,保证访问时能通过SP + 偏移
(如LDR R0, [SP, #4]
)正确读取。总结:局部变量分配依托 “调整
SP
预留空间 → 按规则填栈帧 →SP + 偏移
访问” 实现。上图清晰展示main
函数局部变量在栈中的分配逻辑,是理解该过程的核心参考 。
(三)问题 3:为什么 RTOS 任务都有自己的栈?
RTOS 多任务切换时,需保存 / 恢复任务执行现场(寄存器、局部变量等 ),每个任务配置独立栈是实现稳定切换的基础。结合上图 解析:
任务与执行现场:
RTOS 中任务(如Task_A
、Task_B
)是独立执行流,需随时暂停、恢复。任务执行现场包含 CPU 寄存器(PC
、LR
、R0 - R15
)、局部变量、函数返回地址,统称 “上下文”。独立栈的必要性:
- 隔离性:任务切换时,不同任务的栈空间独立,避免数据干扰。比如
Task_A
执行到一半被打断,其局部变量、返回地址存在自己的栈里;Task_B
运行时用自己的栈存数据,互不影响。 - 保存现场:任务切换时(第 2 张图 ),需把当前任务的 “现场” 存到自己的栈,再恢复下一个任务的 “现场”。若共用一个栈,现场会被覆盖,切换后无法恢复执行。
举例:
Task_A
执行b_func
时被切换,栈里存着PC = 0x0800017a
、R0 = 2
等现场(第 3、4 张图 );切换到Task_B
后,Task_B
用自己的栈运行;切回Task_A
时,从其栈恢复现场继续执行。总结:每个任务独立栈是为实现 “执行现场隔离存储与恢复”,保障多任务切换后能正确续跑。上图直观呈现任务切换时栈对现场的保存 / 恢复逻辑,是理解多任务栈设计的关键 。
- 隔离性:任务切换时,不同任务的栈空间独立,避免数据干扰。比如
三、知识串联(从函数调用到 RTOS 任务 )
- 函数调用:用
LR
存返回地址,嵌套调用时靠 “压栈保存LR
+ 出栈恢复”。解决覆盖问题;局部变量依托SP
调整、栈帧规则分配。 - RTOS 任务:每个任务用独立栈保存执行现场(寄存器、返回地址、局部变量 ),切换时存 / 恢复现场,保障任务暂停、续跑的正确性。
四、易错点 & 补充说明
(一)易错点
- 栈溢出:函数嵌套深、局部变量大,或 RTOS 任务栈配置小,会耗尽栈空间,覆盖代码段、堆,引发程序崩溃。需合理规划栈大小。
- 局部变量地址无效:函数返回后,局部变量栈空间释放,若返回其指针(如
return &ch;
),会因访问 “无效地址” 出错。 - 任务栈未对齐:RTOS 任务栈需满足 ARM 架构对齐要求(如 4 字节对齐 ),否则存 / 取数据会因硬件不支持非对齐访问出错。
(二)补充拓展
- 栈帧优化:编译器会优化栈帧(复用空间、局部变量存寄存器 ),调试时需注意优化可能让栈帧 “不直观”。
- RTOS 上下文切换细节:除栈外,还涉及
PSP
(进程栈指针 )和MSP
(主栈指针 )切换(如 Cortex - M 系列 ),复杂 RTOS 会区分内核栈和任务栈,保障系统调用安全。