当前位置:首页 > 公众号精选 > 嵌入式客栈
[导读]关注、星标嵌入式客栈,干货及时送达 [导读] 从这篇文章开始,将会不定期更新关于嵌入式C语言编程相关的个人认为比较重要的知识点,或者踩过的坑。 为什么要深入理解栈?做C语言开发如果栈设置不合理或者使用不对,栈就会溢出,溢出就会遇到无法预测乱飞现象

关注、星标嵌入式客栈,干货及时送达

[导读] 从这篇文章开始,将会不定期更新关于嵌入式C语言编程相关的个人认为比较重要的知识点,或者踩过的坑。

为什么要深入理解栈?做C语言开发如果栈设置不合理或者使用不对,栈就会溢出,溢出就会遇到无法预测乱飞现象。所以对栈的深入理解是非常重要的。

注:动画如果看不清楚可以电脑看更清晰

啥是栈

先来看一段动画:

没有比这个更直观的啦,栈是一种受限的数据结构模型,其数据总是只能在顶部追加,利用一个指针进行索引,顶端叫栈顶,相对的一端底部称为栈底。栈是一种LIFO后入先出的数据结构。

栈就两种操作:

  • PUSH,压栈,向栈内加入数据,
  • POP,出栈

再进一步探讨:

首先将栈与堆分清,从看到这篇文章开始,我建议你不要把堆和栈连在一起叫,栈是栈,堆是堆,这是两回事,别混为一谈!(堆本文不深入讨论)

从C/C++编程语言的角度来看:

  • 相同点:都是一片内存区,在链接时指定栈区/堆区的位置以及大小。

  • 不同点:

    • 栈:由编译器分配,存放函数的参数值,局部变量,寄存器组(不同的单片机/处理器各有不同)、函数调用参数传递、中断异常产生时须保存处理器状态的寄存器值等
    • 堆:由程序员分配释放,对于C而言,malloc、realloc/free进行分配/释放,对C++而言,由new/delete分配/释放。

为啥用

栈这个数据模型的应用价值是什么呢?先来看一下单片机内部的可能有哪些栈应用?以STM32为例,参考IAR C/C++ Development Guide,P207

处理器模式 建议段名 描述
Supervisor SVC_STACK 操作系统栈
IRQ IRQ_STACK 通用(IRQ)中断处理程序的堆栈。
FIQ FIQ_STACK 用于高速(FIQ)中断处理程序的堆栈。
Undefined UND_STACK 堆栈用于未定义的指令中断。支持硬件协处理器和指令集扩展的软件仿真。
Abort ABT_STACK 用于指令获取和数据访问存储器中止中断处理程序的堆栈。

如果使用RTOS还有任务栈,如果是Linux,其内核线程同样也需要栈的支持,等等这一切的一切栈,其本质上都是利用了栈数据模型的LIFO后入先出的特性,一个典型应用场景就是比如做一件事情做到一半而要转而去做另外一件事,对于芯片编程而言,就需要将当前的工作做个暂存,等另外一件事情做完了,再接着回来继续做。那么怎么做到呢,以一个中断处理为例,要记住当前的工作态有哪些信息需要暂存呢?PC指针,局部变量等就被压入栈,再将中断服务程序地址导入PC指针,进而去执行中断服务程序,待中断处理完毕,在将栈里的内容按照后入先出弹出到对应的寄存器就恢复了原程序的现场,进而继续执行。

怎么用

栈在哪里定义大小,定多大合适?这可能很多刚接触单片机开发的同学不是太清楚,下面就将比较常见的IAR开发环境为例如何定义栈定义栈大小的地方说明一下,这里以IAR8.4.1为例,有两种方式可以进行栈大小设置。

IDE设置

为了更加清楚明了,制作了一个GIF操作展示视频,在stack/heap中就可以设置了,其中stack用于设置栈区大小,heap用于设置堆大小。

这个demo中设置了其栈的大小为0x200,堆的大小为0x400,全编译后,检查map文件就印证了栈/堆的大小如预期所修改。

链接配置文件

其实对于比较熟悉的开发人员,上一种方式并非推荐用法。用链接配置文件将具有更好的灵活性,比如可以指定一个段的对齐方式,存储位置,某个符号的存储位置等等。这里同样为了直观也做了一个GIF动画,介绍如何通过链接文件进行栈/堆的大小配置。


其最终的效果也一样如预期将栈区的大小设置好了。

栈溢出

这里为了比较容易的展示栈溢出的问题,在main函数利用递归方法计算阶乘,代码如下:

#include <stdio.h>
#include "main.h"
static uint32_t spSatte[200];
static uint32_t spIndex = 0;
/*为什么要用浮点数,因为数据非常大整型很快就会溢出*/
float factorial(uint32_t n)
{
    uint32_t sp = __get_MSP();    
    /*记录栈指针的变化情况*/
    spSatte[spIndex++] = sp;
    if(n==0 || n==1)
        return 1;
    else
        return (float)n*factorial(n-1);
}

int main(void)
{
    float  x = 0;
    uint32_t  n = 20;
    printf("stack test:\n");
    x = factorial(n);
    /*打印栈指针变化情况*/
    for(int i = 0;i<spIndex;i++)
        printf("MSP->%08X\n",spSatte[i]);
    
    /*打印阶乘结果*/
    printf("factorial(%d)=%f\n",n,x);    
    while (1)
    {
    }
}

为方便观察,将stm32f407xx_flash.icf 将栈改为256字节

/*stm32f407xx_flash.icf 将栈改为256字节*/
define symbol __ICFEDIT_size_cstack__ = 0x200;
define symbol __ICFEDIT_size_heap__   = 0x200;

全编译后通过map文件来看下栈/堆的分配情况:

"P2", part 3 of 3:                          0x400
  CSTACK                      0x2000'05d8   0x200  <Block>
    CSTACK           uninit   0x2000'05d8   0x200  <Block tail>
  HEAP                        0x2000'07d8   0x200  <Block>
    HEAP             uninit   0x2000'07d8   0x200  <Block tail>
                            - 0x2000'09d8   0x400

直观些,翻译成下图,CSTACK段分配在0x2000 0280-0x2000 0480,堆分配在0x2000 0480-0x2000 0680。

图为什么没有将0x2000 07D8画在栈区呢?通过调试发现,这个字空间没有用做栈的实际存储。将工程设置成simulation模式,debug进入main.o勾选掉,我们来计算20的阶乘,来具体看一下:

对这个动图解读一下:

  • 进入复位是,SP_main为0x200007D8,指向栈底,为空栈。那么这是怎么实现的呢?
__vector_table                ;向量表
DCD sfe(CSTACK) ;这条命令会将程序的CSTACK起始地址装载给SP_main
DCD Reset_Handler ; Reset Handler复位向量
  • 前面说0x200007D8并没有用到,怎么证明呢,在函数进入mian时,第一次压栈的情况如下:

  • 可见STM32栈的增长方向是向下增长的,也即顶在小地址端一侧
  • 栈存储元素是四字节对齐的,因为STM32的字长是字节,如果深入想想,如果不是司字节对齐会怎么样?留给感兴趣的思考一下。
  • 0x200007D8--0x200007DB 这个字存储单元并不是栈的有效存储空间。

栈的变化情况:

stack test:
MSP->200007A8 
MSP->20000790
MSP->20000778
MSP->20000760
MSP->20000748
MSP->20000730
MSP->20000718
MSP->20000700
MSP->200006E8
MSP->200006D0
MSP->200006B8
MSP->200006A0
MSP->20000688
MSP->20000670
MSP->20000658
MSP->20000640
MSP->20000628
MSP->20000610
MSP->200005F8
MSP->200005E0
factorial(20)=2432902023163674771.785700 /*结算结果与用计算器一致*/

每调用一次阶乘函数,栈就压入4个字,由上面还可以看到第20次进入时,栈指针为0x200005E0,如果再压入4个字栈指针会变成0x200005C8,是这样吗,结果还对吗?将n改为21编译运行,来看一看:

看到了吧,惊喜来了,栈溢出了,程序已经不听话了,完全不知道在干嘛了。所以栈溢出的后果是极端危险的,完全无法预期,程序会带来什么后果。

总结一下

  • 栈是一种LIFO后入先出的数据结构模型,是C/C++程序运行时基础,没这个栈,C/C++玩不转
  • 栈在嵌入式编程领域随处可见,比如C栈,中断栈、异常栈、任务栈等等,但其基本工作原理都一样。支持两种基本数据操作:压栈、出栈。
  • 栈溢出程序的结果无法预期,所以合理的设置栈区大小是个永恒的话题,过大则浪费内存,过小则程序会飞。
  • 嵌入式编程递归函数要慎用,个人建议不用。比如IEC61508 功能安全标准中强行规定不可使用递归函数。
  • STM32中__get_MSP可以得到当前栈指针的值,据此可以做一定程度的栈溢出保护措施。防止程序跑飞。
  • 通过上面递归调用测试,还可以得到一个启示,嵌入式编程函数嵌套的层级不宜过深,过深则需要相对较大的栈开销。
  • .......
点击留言/查看留言

END

果喜欢右下点个在看,也会让我倍感鼓舞

往期精彩推荐




▲深度解析U-Boot网络实现(长篇好文)
读U-Boot源码-C语言编程大法总结篇一
读U-Boot源码-C语言编程技巧总结篇二
基于Buildroot的Linux系统构建之根文件系统
手把手教系列之移动平均滤波器C实现
手把手教系列之IIR数字滤波器设计实现
手把手教系列之梳状滤波器设计实现

关注置顶:扫描左下二维码关注公众号加星

讨论加群:扫描右下二维码添加,发送“加群”

关注

加群

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

电磁铁是一种利用电流产生磁场的装置,具有快速响应、易于控制等特点,在工业自动化、电子设备、科学实验等领域有着广泛的应用。STM32是一款功能强大的微控制器,具有高性能、低功耗、易于编程等优点,是控制电磁铁的理想选择。本文...

关键字: 电磁铁 微控制器 STM32

边缘人工智能的实现涉及到三个基本 要素:安全性,连接性、自主性,而其中自主性是AI能力的体现,也是边缘AI有别于其他传统的物联网的关键。而通过ST Edge AI套件,就可以帮助各种不同类型的开发者实现覆盖全硬件平台的全...

关键字: 边缘人工智能 AI STM32

今天,小编将在这篇文章中为大家带来STM32单片机最小系统的有关报道,通过阅读这篇文章,大家可以对它具备清晰的认识,主要内容如下。

关键字: 单片机 单片机最小系统 STM32

学好电子技术基础知识,如电路基础、模拟电路、数字电路和微机原理。这几门课程都是弱电类专业的必修课程,学会这些后能保证你看懂单片机电路、知道电路的设计思路和工作原理;

关键字: 单片机 编程 电路设计

单片机编程需要使用专门的软件工具,这些工具能够帮助程序员编写、调试和烧录程序到单片机中。以下是一些常用的单片机编程软件:

关键字: 单片机 编程 软件工具

STM32是一款由STMicroelectronics生产的微控制器系列,具有高性能、低功耗和丰富的外设资源。其中,串口通信是一种常用的通信方式,可以实现与其他设备之间的数据传输。

关键字: STM32 串口通信 微控制器

STM32是一种广泛使用的微控制器,具有丰富的通信接口。其中,串口通信是STM32与其他设备或系统进行数据交换的重要方式之一。本文将详细介绍STM32串口通信的原理、应用及常见故障。

关键字: STM32 串口通信

由于目前缺乏相应的监测技术,地下电缆线路出现异常运行状态无法被及时发现,久而久之易演变成大故障,最终只能通过更换地下电缆进行修复,耗费大量的人力、物力。鉴于此,开发了一种基于STM32的地下电缆异常状态检测系统,利用热传...

关键字: STM32 地下电缆

Java语言和C语言是两种不同的编程语言,它们在语法、特性和应用领域上有许多差别。下面将详细介绍Java语言和C语言之间的差异以及它们各自的技术特点。

关键字: Java语言 C语言 编程

交通灯控制器是用于控制交通信号灯运行的设备,它可以根据交通流量、行人需求以及其他因素,动态地调整信号灯的变化时间和绿灯时长,以保证交通的流畅和安全。

关键字: 交通信号灯 STM32
关闭
关闭