[导读] 从这篇文章开始,将会不定期更新关于嵌入式C语言编程相关的个人认为比较重要的知识点,或者踩过的坑。
为什么要深入理解栈?做C语言开发如果栈设置不合理或者使用不对,栈就会溢出,溢出就会遇到无法预测乱飞现象。所以对栈的深入理解是非常重要的。
栈是一种受限的数据结构模型,其数据总是只能在顶部追加,利用一个指针进行索引,顶端叫栈顶,相对的一端底部称为栈底。栈是一种LIFO后入先出的数据结构。
栈就两种操作:
再进一步探讨:
首先将栈与堆分清,从看到这篇文章开始,我建议你不要把堆和栈连在一起叫,栈是栈,堆是堆,这是两回事,别混为一谈!(堆本文不深入讨论)
从C/C++编程语言的角度来看:
相同点:都是一片内存区,在链接时指定栈区/堆区的位置以及大小。
不同点:
栈这个数据模型的应用价值是什么呢?先来看一下单片机内部的可能有哪些栈应用?以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为例,有两种方式可以进行栈大小设置。
为了更加清楚明了,制作了一个GIF操作展示视频,在stack/heap中就可以设置了,其中stack用于设置栈区大小,heap用于设置堆大小。
这个demo中设置了其栈的大小为0x200,堆的大小为0x400,全编译后,检查map文件就印证了栈/堆的大小如预期所修改。
其实对于比较熟悉的开发人员,上一种方式并非推荐用法。用链接配置文件将具有更好的灵活性,比如可以指定一个段的对齐方式,存储位置,某个符号的存储位置等等。这里同样为了直观也做了一个GIF动画,介绍如何通过链接文件进行栈/堆的大小配置。
其最终的效果也一样如预期将栈区的大小设置好了。
这里为了比较容易的展示栈溢出的问题,在main函数利用递归方法计算阶乘,代码如下:
#include
#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 05D8-0x2000 07D8,堆分配在0x2000 07D8-0x2000 09D8。
图为什么没有将0x2000 07D8画在栈区呢?通过调试发现,这个字空间没有用做栈的实际存储。将工程设置成simulation模式,debug进入main.o勾选掉,我们来计算20的阶乘,来具体看一下:
对这个动图解读一下:
__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编译运行,来看一看:
看到了吧,惊喜来了,栈溢出了,程序已经不听话了,完全不知道在干嘛了。所以栈溢出的后果是极端危险的,完全无法预期,程序会带来什么后果。