编译结果中代码与数据的存储的理解

最近在优化一个项目,想要尽可能地压缩代码,腾出空间写新的业务逻辑。看多了.map文件,有了一些新的理解。我用的编译器是IAR,可能编译出来的.map文件跟keil不一样,但是原理应该是一致的。

先说一些基本的概念。

程序编译后,主要分成三大部分。text,data,bss。

text就是编译后的代码

data就是变量,全局变量,局部的静态变量,而且还是有初始值的,亦即是是初始值不是0。至于为啥要单独存储全局变量和局部的静态变量待会说。

bss也是变量,也是全局变量,局部的静态变量,但是是没有初始值的,亦即是初始值是0。

放几张.map文件的图来看一下。

 

编译结果中代码与数据的存储的理解

可以看出来,.intvec也就是中断向量表,被放在了flash的起始地址,这个是M3内核决定的。

.text也就是代码,kind里也写着了,ro code。只读代码。很重要的信息是size 和object。object告诉你是哪个源文件,size告诉你多大。有了这两个信息,我们就可以知道代码中有哪个源文件占用空间多,可以定点打击,专门优化。

编译结果中代码与数据的存储的理解

.data的kind是inited,已经初始化了,而且放的位置是RAM。

编译结果中代码与数据的存储的理解

bss类型是zero,也可以认为是没有初始值,也放在RAM里。

为啥编译出来的结果,要单独为了全局变量和局部的静态变量专门弄两个段呢。你想啊,全局变量和局部静态变量,是不会因为函数的结束而被会回收的,也就是说RAM里必须有一段空间是要用来存储全局变量和静态变量的,并且在程序运行期间,这些变量的地址不会改的。所以早早地在编译的时候,就确定这些变量存储在RAM的哪个位置也是合理的,可以认为编译代码的时候,遇到全局变量和静态变量,直接就可以用确定的地址,而不是动态分配的地址。

但是我们知道RAM是掉电不保存数据的,那些有初始值的变量(.data里的变量)他们的值是如何事先存储到RAM中的呢。

编译结果中代码与数据的存储的理解

我在map里找到这个,可以看到在存放.text的段里有一个叫Initializer bytes 类型是只读常量,地址是0x57f6,属于flash的范围。并且注明了,for P2-1。而P2-1刚好就是存储.data的段。不言而喻,这些有初始值的变量,初始值被安排存储在flash中,在上电后才拷贝到RAM中。

编译结果中代码与数据的存储的理解

在map中的text段中,我还发现了rodata。类型是常量,按理说这些也是变量,但是并没有被安排放在RAM里,而是被安排在flash中。我想,正是因为是const的,只读,所以安排在可读可写的RAM中占用空间实在浪费。比如说你定义了一个const类型的数组,它就被安排在了flash中。

最后想说的是堆heap和栈stack。

栈用在什么地方呢,比方说你在函数里定义的局部变量,它就是从栈里申请的,可以理解为往栈里PUSH数据,在函数执行完毕后,这些局部变量都要被销毁,这个时候从栈里POP出来就好了。正因如此,局部变量的地址都是动态变化的。栈还用在保存上下文,比如说函数调用的时候,就要保存此时的上下文,把断点和寄存器都入栈保存,然后跳转到调用函数,执行完函数后,就要把栈里的上下文给POP出来,恢复调用前的状态,继续执行代码。执行中断的时候也一样。

堆呢,堆是由用户来分配和销毁的。最常见的就是mallco和free。你mallco一块空间,只要你不调用free这段空间都是不会被销毁重新利用的。

那堆和栈放在哪呢。

编译结果中代码与数据的存储的理解

还是看map,可以看到CSTACK也就是栈,和HEAP都被安排在RAM里。

编译结果中代码与数据的存储的理解

由于我没有给HEAP分配的大小为0,所以编译器没有为HEAP分配空间。这个CSTACK的位置刚好就是在.data和.bss之后。Ram中地址从小到大放置的依次是.data .bss CSTACK。

编译结果中代码与数据的存储的理解

在IAR里,是可以设置堆和栈的大小的。当然这个大小不能超过芯片RAM,编译器也会检查的,它会计算编译后的.data .bss的大小加起来,再加上你的定的堆和栈,一共多大,是否超过了RAM的大小。

最后说一点开发经验,你可以把栈定的很小,比如说只有1k。编译器能检查栈是否太大,但是不能检查是否太小。栈定小了,会怎么样呢。上面说了,栈用在局部变量和上下文切换。如果定得小了,我遇见过这样的情况,你在一个函数里定义了局部变量,经过一些计算给局部变量赋值了,然后你调用另外一个函数,函数后返回,你发现局部变量的值意外改变了,变成0了。原因就跟你把一个8位大小的变量累加到超过255,它就要变回0一样。栈也有一个指针,栈太小,指针指到末尾后再入栈,必然要覆盖原来有效的数据。编译器在编译的时候是没办法帮你发现这种问题的。