FreeRTOS学习(六):通过实例深入理解栈的作用(不学FreeRTOS也值得看,对程序运行机制的理解很有帮助!)
文章目录
前言
看本章之前请先看->ARM架构基础:寄存器、栈操作与指令详解
在程序执行过程中,栈(Stack)扮演着至关重要的角色。本文将通过一个简单的C语言例子,结合其反汇编代码,来深入理解栈的作用。
一、简单的函数对应的反汇编
首先看一个简单的C语言函数:
void add_val(int *pa, int *pb) {
volatile int tem; // 声明一个临时变量
tem = *pa; // 获取第一个指针指向的值
tem = tem + *pb; // 将两个值相加
*pa = tem; // 存储结果
}
就是简单的a=a+b的加法
与之对应的反汇编是
void add_val(int *pa, int *pb) {
volatile int tem; // 声明一个易变的临时变量
PUSH {r3,lr}
tem = *pa; // 获取第一个指针指向的值
LDR r2,[r0,#0x00]
STR r2,[sp,#0x00]
tem = tem + *pb; // 将第二个指针指向的值加到临时变量上
LDR r2,[r1,#0x00]
LDR r3,[sp,#0x00]
ADD r2,r2,r3
STR r2,[sp,#0x00]
*pa = tem; // 将结果存回第一个指针指向的位置
LDR r2,[sp,#0x00]
STR r2,[r0,#0x00]
}
现在汇编看着很多很乱,没关系!
下面我们一步步分析!
首先如上图所示
pa和pb的值会存进R0和R1,因为在调用C语言函数的时候,第一个参数会保存在R0第二个参数会保存在R1
下面讲PUSH
保存r3和lr寄存器到栈上,为函数调用做准备
这里的r3是一个通用寄存器
这条指令会将r3和lr的值都压入栈
每个寄存器占4字节,总共压入8字节
这样就保证了栈的8字节对齐
实际上r3的值在这个函数中并没有特别用途
它主要是为了满足栈对齐的要求
LR保存的是返回地址
如果add_val函数内还要调用其他函数
新的函数调用会覆盖LR的值
所以需要先把LR保存到栈上
这就是为什么有PUSH {r3,lr}这个指令
LR中保存的是函数调用指令的下一条指令的地址,这确保了函数能正确返回到调用点继续执行。
1.tem=*pa; 获取第一个指针指向的值,那它是如何实现的
LDR r2,[r0,#0x00] ; ->将pa指向的值加载到R2r0在这里就是pa,前面有讲
👇
R2寄存器取址&pa,去pa的地址上去读内存也就是存值,相当于R2=pa
STR r2,[sp,#0x00]; ->将R2的值存储到栈上的tem变量
👇
那R2的值存到哪里呢,存到[sp+0]的位置
栈是向下生长的,所以sp=sp-4的位置就是sp,把R3替换掉
也就是pa,刚好就是tem
2.tem=tem+*pb; 的实现:
680A LDR r2,[r1,#0x00] ; 将pb指向的值加载到r2
9B00 LDR r3,[sp,#0x00] ; 将栈上的tem值加载到r3
441A ADD r2,r2,r3 ; r2 = r2 + r3
9200 STR r2,[sp,#0x00] ; 将结果存回栈上的tem
LDR r2,[r1,#0x00] -> R2=[R1+0]=[&pb]=pb
这里的R1等于pb是因为pb的值已经传给R1了,前面已经说了
LDR r3,[sp,#0x00] -> R3=[sp+0]=tem重新给R3赋值
ADD r2,r2,r3 -> R2=R2+R3=pb+tem
STR r2,[sp,#0x00] -> R2=[sp+0]把R2的值存进sp+0
那就是tem
现在不就相当于tem=tem+pb了吗
我们继续
3.pa = tem;
9A00 LDR r2,[sp,#0x00] ; 将栈上的tem值加载到r2
6002 STR r2,[r0,#0x00] ; 将r2的值存储到pa指向的位置
LDR r2,[sp,#0x00] ->R2读取sp的值,也就是R2=[sp+0]=tem
STR r2,[r0,#0x00] ->把R2的值存到[R0+1],也就是pa,所以这里就是pa=pb=tem
二、现场的保存
假如现在我这里读到R2的值,R2=pa=1是吧
我辛辛苦苦去读内存,我得到的值保存在R2这里,我R2的值还没来得及用呢,我本来在下面要把R2的值又写到内存里面去,还没来得及STR r2,[sp,#0x00]; 在这里的时候我就被中断出去了,你要去做其他各种乱七八糟的事情去了。那好,被中断的时候怎么保护现场,R2的值你是得保存下来,如果你不保存的话,下一次我们箭头这里继续执行的时候,R2的值有可能就不是1了
那我们要怎么保存现场呢?
什么叫现场?
就是
被打断的瞬间,这所有寄存器的值
cpu
👇
那我们把寄存器的值保存在哪里?
保存在栈里
任务就是,一个函数对应它的现场也就是栈(上图对应的ram),我们称之为运行中的函数
事实上,被打断的现场中,不是所有寄存器的值都需要被保存
void a(int bb){
b(bb);
}
在c语言的函数调用中,R0,R1,R2,R3,这些寄存器是用来传递参数给子函数的,也就是a函数传参数给b函数,既然是传参数,那就不需要被保存
- R0-R3本来就是用来传参的,被调用函数可以随意修改
- 如果a函数后续还需要这些参数值,它需要自己提前保存
- 这样的设计提高了函数调用的效率
这就是为什么不是所有寄存器都需要保存 - 这是ARM调用约定(calling convention)的一部分。
如果是硬件中断(按键触发) 的话
硬件自动保存:
- R0-R3 (参数寄存器)
- R12 (IP)
- R14 (LR)
- SPSR (保存被中断时的CPSR)
- 自动切换到相应的处理模式(如IRQ模式)
软件需要保存:
- 如果中断处理程序会使用到R4-R11,需要手动保存
- 如果中断处理程序会调用其他函数,需要保存LR
- 如果会发生嵌套中断,需要保存SPSR
总之就是保存会用到的寄存器
而在任务切换的时候需要保存所有的寄存器原因是
- 函数调用是"自愿"的程序行为
- 编译器知道调用约定
- 知道哪些寄存器需要保存(R4-R11)
- 知道哪些可以不保存(R0-R3)
- 而任务切换是"被动"的中断行为
- 任务不知道什么时候会被切换
- 切换可能发生在任何指令执行点
- 当前任务寄存器的所有值都可能还需要用到
- 不能依赖调用约定
所以:
- 任务切换时必须保存全部寄存器状态(R0-R15)和CPSR
- 这些值构成了任务的完整上下文(context)
- 恢复时也要恢复所有寄存器的值
总结
栈的主要作用可以总结为以下几点:
- 保存函数调用时的上下文
- 保存返回地址(LR)
- 保存调用者需要保护的寄存器
- 保存函数的局部变量
- 传递函数参数
- 当参数超过4个时(ARM中R0-R3装不下)
- 参数通过栈传递给被调用函数
- 中断和任务切换时保存现场
- 保存被中断任务的寄存器状态
- 保存处理器状态(CPSR)
- 用于后续恢复任务执行
- 动态分配临时存储空间
- 分配局部数组等自动变量
- 函数返回时自动释放
- 避免使用堆的开销
- 支持程序递归
- 每次递归调用都有独立的栈帧
- 保存每层递归的局部变量和返回信息
栈的特点:
- 后进先出(LISP)的访问方式
- 自动管理的内存空间
- 空间效率高,分配/释放快
- 但大小固定,需要预先规划
栈对于程序执行非常重要,是实现函数调用、中断处理等基本机制的基础。