FreeRTOS学习(六):通过实例深入理解栈的作用(不学FreeRTOS也值得看,对程序运行机制的理解很有帮助!)

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函数,既然是传参数,那就不需要被保存

  1. R0-R3本来就是用来传参的,被调用函数可以随意修改
  2. 如果a函数后续还需要这些参数值,它需要自己提前保存
  3. 这样的设计提高了函数调用的效率

这就是为什么不是所有寄存器都需要保存 - 这是ARM调用约定(calling convention)的一部分。

如果是硬件中断(按键触发) 的话
硬件自动保存:

  • R0-R3 (参数寄存器)
  • R12 (IP)
  • R14 (LR)
  • SPSR (保存被中断时的CPSR)
  • 自动切换到相应的处理模式(如IRQ模式)

软件需要保存:

  • 如果中断处理程序会使用到R4-R11,需要手动保存
  • 如果中断处理程序会调用其他函数,需要保存LR
  • 如果会发生嵌套中断,需要保存SPSR
    总之就是保存会用到的寄存器

而在任务切换的时候需要保存所有的寄存器原因是

  1. 函数调用是"自愿"的程序行为
  • 编译器知道调用约定
  • 知道哪些寄存器需要保存(R4-R11)
  • 知道哪些可以不保存(R0-R3)
  1. 而任务切换是"被动"的中断行为
  • 任务不知道什么时候会被切换
  • 切换可能发生在任何指令执行点
  • 当前任务寄存器的所有值都可能还需要用到
  • 不能依赖调用约定

所以:

  • 任务切换时必须保存全部寄存器状态(R0-R15)和CPSR
  • 这些值构成了任务的完整上下文(context)
  • 恢复时也要恢复所有寄存器的值

总结

栈的主要作用可以总结为以下几点:

  1. 保存函数调用时的上下文
  • 保存返回地址(LR)
  • 保存调用者需要保护的寄存器
  • 保存函数的局部变量
  1. 传递函数参数
  • 当参数超过4个时(ARM中R0-R3装不下)
  • 参数通过栈传递给被调用函数
  1. 中断和任务切换时保存现场
  • 保存被中断任务的寄存器状态
  • 保存处理器状态(CPSR)
  • 用于后续恢复任务执行
  1. 动态分配临时存储空间
  • 分配局部数组等自动变量
  • 函数返回时自动释放
  • 避免使用堆的开销
  1. 支持程序递归
  • 每次递归调用都有独立的栈帧
  • 保存每层递归的局部变量和返回信息

栈的特点:

  • 后进先出(LISP)的访问方式
  • 自动管理的内存空间
  • 空间效率高,分配/释放快
  • 但大小固定,需要预先规划

栈对于程序执行非常重要,是实现函数调用、中断处理等基本机制的基础。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值