上一篇文章中,为了提取jlink-ob的bin文件,需要到jlinkARM.dll中获取,因此必须要了解STM32编译后的指令特点及其存储特性,所以这一篇先来看看堆栈和存储,以及SP这个栈顶地址是怎么确定的。
如何获取Jlink-ob的固件
今天来科普一下堆栈的概念,以及SMT32存储器是如何存储代码的。
堆和栈的概念其实是渐进式的来看,首先堆栈是一种数据结构,其次程序运行的时候利用了这样的数据结构,在MCU的内存中营造出了这两个区域来配合程序执行。
一,什么是堆栈和队列
堆栈对应到英文单词就是heap和stack。
这里要注意,在数据结构中,队列和的堆栈是不同类型的数据结构。
我记得初学计算机的时候,有一个很有意思的题目--摞盘子。
我现在每天的生活中都在亲历这个题目,晚饭后,我会把厨房收拾干净,所有的盘子都洗干净摞在一起。
旁边那一摞盘子其实就是我设计的一个栈,他有一个特点,那就是我做饭的时候一定先要从最上面拿起一个盘子乘菜。而当我刷锅洗碗时,我会把洗好的盘子再放回最上面。如果觉察到下面的盘子积灰了,那说明我们的栈深度还足够,否则我会认为最近做的菜有点多了。好了,上面就是一个数据结构 — 栈的问题,下面我们看一下鸡蛋的问题。额,不是鸡蛋,是队列的问题。
我最近买了一个储存鸡蛋的盒子,设计的很巧妙,买来鸡蛋我就从上面一个一个的放进去,然后放在冰箱的最上面。需要鸡蛋的时候,我就从下面取一颗,源源不断。你会发现,我吃的永远是剩的最陈旧的那一颗鸡蛋,这就是队列。
二,STM32 的FLASH与RAM的存储
到了计算机系统中,堆和栈被分别了开来。其中栈保持原有的特性,也可以叫做堆栈,它是一种运算受限的线性表,限制仅允许在表的一段进行插入和删除操作,可以被操作的这一端叫做栈顶,另一端叫做栈底,也就是容易积灰的那一端。栈在程序运行的过程中主要负责存储局部变量,比如我们函数中定义的变量,如果控制器内核的寄存器不够用了,它就会把这个变量放到栈里面先存一会。在操作系统中,任务切换时的现场参数保存也是存放到对应任务的栈(这里的栈其实放在STM32的全局变量里面)里面。上面有点迷糊,这里我们主要讨论STM32裸机程序的堆栈情况。在STM32中,堆是用来给分配动态内存的,系统只有用到malloc的时候才会使用这个区间,后面在讲这个位置。我先看看,每次编译完程序,编译器链接后,会提供一个生成信息。
代码编译后提示信息可以看到的内容
- Code :是代码占用的空间,存储到Flash【ROM】中的程序代码。RO-data:是 Read Only 只读常量的大小,如const修饰的变量。用来存储程序的指令和常量,保存在Flash【ROM】中。RW-data:是(Read Write) RW是可读可写变量,就是初始化时候就已经赋值了的(上电前就已经确定值的),RW + ZI就是你的程序总共使用的RAM字节数。ZI-data:是(Zero Initialize) 没有初始化的可读写变量的大小,就是程序中用到的变量并且被系统初始化为0的变量的字节数。
Total ROM Size (Code + RO Data + RW Data)这样所写的程序占用的ROM的字节总数,这部分在我们进行程序下载的时候,会全部存储在SMT32的Flash中。
当系统上电后,内核会将一部分数据存搬运到RAM当中,因为我们定义的很多变量是可读取的,所以要放到RAM中。这其中包括RW-data和ZI-data。对于RW-data,需要一点一点的搬运过去,既然是搬运,那么在FLASH中本来就会存储一份,所以RW-data会占用Flash空间。而对于ZI-data,由于它没有初始化,所以它的初始值无所谓,所以我们只需要知道它的个数,在RAM中直接画一片地方给他用就行了,因此ZI-data无需单独在Flash中存储一份。我们所说的堆栈,就在上述执行过程中同样的被画了一块自留地。
三,SMT32的堆栈自留地
那么在STM32的RAM中,堆栈是如何量取确定的呢?我们先看一张图:
上图中指示了STM32的RAM区域存储结构,下面是低地址,上面是高地址。编译器在编译和链接后,是可以计算出全局变量,局部变量以及一些未初始化的全局变量所占用空间的,我们把这个总和计算出来,现在RAM上画一片地方来存放,也就是上图的静态存储区域。然后我们再画一片自留地给到定义好的HEAP区域,最后画一片给到STACK,也就是我们说的栈。我们在代码中看一下如何定义的。我手头有一个CW32的例程,用这个来示意一下。在项目中的startupxx.s文件中,用汇编定义了堆和栈的大小。
如果我们不使用malloc进行动态分配内存,那么这里的Heap_Size完全可以定义为0,不过编译器直接把这个事给干了,也就是说,如果它发现你没有使用malloc,编译器会直接移除这部分空间,可以在例程编译生成的map文件中看到。
所以,没必要手贱的去改这个汇编文件。再说RAM空间的堆叠方式,它是先放的静态变量,然后放的堆空间(编译器会优化掉),最后放的栈空间,所以只要我们改变栈空间大小,那么栈顶指针就会变化。实验一下吧。
接下来,我们把栈改大一倍,编译看看,栈顶地址是不是相应的变大了
是不是?童叟无欺!那么,如果我们增加静态存储区的大小,这个栈顶地址应该也会改变。如何增加静态存储区大小呢?定义一个全局变量就好了。记得把编译器的优化等级去掉哦,不然编译器会偷偷把你没用的变量啥的都给你移除掉,实验就没效果了。
我注释掉了一个4字节的数组,栈的大小也改回了0x200,编译后,我的栈顶地址变成了0x20000e60。接下来,我把注释打开,增加一个4字节的数组。
我们可以看到,栈顶地址确实增加了,从原来的0x20000e60增加到了0x20000e68。为什么地址一下增加了8呢?于是我有改小了一下数组,重新编译后,栈顶地址又变回了0x20000e60。
有兴趣的朋友研究下,咱们评论区聊聊。