C语言+单片机-内存分布详解,全网最全,值得收藏保存
目录
2. 为什么数据段还需要分 .data、.bss、.rodata 这么麻烦?有什么区别?
本篇主要讲在C语言和单片机中内存分布情况。
一、C语言内存分区
C语言内存分区示意图如下:
1. 代码区
-
程序执行代码存放在代码区,其值不能修改(若修改则会出现错误)。
-
字符串常量和define定义的常量也有可能存放在代码区。
2. 常量区
-
字符串、数字等常量存放在常量区。
-
const修饰的全局变量存放在常量区。
-
程序运行期间,常量区的内容不可以被修改。
3. 全局(静态)区
全局(静态)区介绍
-
编译器编译时即分配内存,全局变量和静态变量的存储是放在一块的。C语言中,已初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
-
全局区有 .bss段 和 .data段组成,可读可写。
.bss段
-
未初始化的全局变量和未初始化的静态变量存放在.bss段。
-
初始化为0的全局变量和初始化为0的静态变量存放在.bss段。
-
.bss段不占用可执行文件空间,其内容由操作系统初始化(清零)。
.data段
-
已初始化的全局变量存放在.data段。
-
已初始化的静态变量存放在.data段。
-
.data段占用可执行文件空间,其内容由程序初始化。
4. 堆区(heap)
堆区介绍
-
堆区由程序员分配内存和释放。
-
堆区按内存地址由低地址到高地址增长,用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。
注意:堆内存牢记不忘释放,避免内存泄漏的情况。
调用函数
-
用malloc等函数实现动态分布内存。
void *malloc(size_t);
参数size_t是分配的字节大小。
返回值是一个void型的指针,该指针指向分配空间的首地址。
(void *型指针可以任意转换为其他类型的指针)
-
用free函数进行内存释放,否则会造成内存泄漏。
void free(void * /ptr/);
参数是开辟的内存的首地址。
5. 栈区(stack)
栈区介绍
-
栈区由编译器自动分配释放,由操作系统自动管理,无须手动管理。
-
栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。
-
栈区按内存地址由高地址到低地址方向增长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。
-
栈区是先进后出原则,即先进去的被堵在屋里的最里面,后进去的在门口,释放的时候门口的先出去。
存放内容
-
临时创建的局部变量和const定义的局部变量存放在栈区。
-
函数调用和返回时,其入口参数和返回值存放在栈区。
二、STM32存储器分配
1. 随机存储器—RAM
-
RAM是与CPU直接交换数据的内部存储器,也叫主存(内存)。
-
它可以随时读写,而且速度很快,通常作为操作系统或其他正在运行中的程序的临时数据存储媒介。
-
当电源关闭时RAM不能保留数据(掉电数据消失哦)如果需要保存数据,就必须把它们写入一个长期的存储设备中(例如硬盘)。
2. 只读存储器—ROM
-
ROM所存数据,一般是装入整机前事先写好的,整机工作过程中只能读出,而不像随机存储器那样能快速地、方便地加以改写。
-
ROM所存数据稳定,断电后所存数据也不会改变。
本文使用STM32F103芯片,keil V5环境下默认的内存配置见下图:
-
ROM区域是0x8000000开始,大小是0x10000,这片区域是只读区域,不可修改,存放代码区和常量区。
-
RAM区域是0x20000000开始,大小是0x5000,这片区域是可读写区域,存放的是全局(静态)区、堆区和栈区。
该芯片的内部分区如下图所示:
三、基于STM32代码验证
1. 详细代码如下
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
//全局区
int q1; //未初始化全局变量
static int q2; //未初始化静态变量
const int q3; //未初始化只读变量
int m1 = 1; //已初始化全局变量
static int m2 = 2; //已初始化静态变量
//常量区
const int m3 = 3; //已初始化只读变量
int main(void)
{
SystemCoreClockUpdate();
while(1)
{
//栈区
int mq1; //未初始化局部变量
int *mq2; //未初始化局部指针变量
int mq3=3; //已初始化局部变量
char qq[10] = "hello"; //已初始化局部数组
const int mq4; //未初始化局部只读变量
const int mq5=3; //已初始化局部只读变量
//堆区
int *p1 = malloc(4); //已初始化局部指针变量p1
int *p2 = malloc(4); //已初始化局部指针变量p2
//全局区
static int mp1; //未初始化局部静态变量
static int mp2 = 2; //已初始化局部静态变量
//常量区
char *vv = "I LOVE YOU";//已初始化局部指针变量
char *mq = "5201314";
printf("\n栈区-变量地址\n");
printf("未初始化局部变量 :0x%p\r\n",&mq1);
printf("未初始化局部指针变量 :0x%p\r\n",&mq2);
printf("已初始化局部变量 :0x%p\r\n",&mq3);
printf("已初始化局部数组 :0x%p\r\n", qq );
printf("未初始化局部只读变量 :0x%p\r\n",&mq4);
printf("已初始化局部只读变量 :0x%p\r\n",&mq5);
printf("\n堆区-动态申请地址\r\n");
printf("已初始化局部int型指针变量p1 :0x%p\r\n", p1);
printf("已初始化局部int型指针变量p2 :0x%p\r\n", p2);
printf("\n全局区-变量地址\n");
printf("未初始化全局变量 :0x%p\r\n",&q1);
printf("未初始化静态变量 :0x%p\r\n",&q2);
printf("未初始化只读变量 :0x%p\r\n",&q3);
printf("已初始化全局变量 :0x%p\r\n",&m1);
printf("已初始化静态变量 :0x%p\r\n",&m2);
printf("未初始化局部静态变量 :0x%p\r\n",&mp1);
printf("已初始化局部静态变量 :0x%p\r\n",&mp2);
printf("\n常量区地址\n");
printf("已初始化只读变量 :0x%p\r\n",&m3);
printf("已初始化局部指针变量 :0x%p\r\n",vv );
printf("已初始化局部指针变量 :0x%p\r\n",mq );
printf("\n代码区地址\n");
printf("程序代码区main函数入口地址 :0x%p\n", main);
delay_ms(1000);
free(p1);
free(p2);
}
}
2. 运行结果如下
栈区-变量地址
未初始化局部变量 :0x20000654
未初始化局部指针变量 :0x20000650
已初始化局部变量 :0x2000064c
已初始化局部数组 :0x20000640
未初始化局部只读变量 :0x2000063c
已初始化局部只读变量 :0x20000638
堆区-动态申请地址
已初始化局部指针变量p1 :0x20000060
已初始化局部指针变量p2 :0x20000068
全局区-变量地址
未初始化全局变量 :0x20000014
未初始化静态变量 :0x20000018
未初始化只读变量 :0x2000001c
已初始化全局变量 :0x20000020
已初始化静态变量 :0x20000024
未初始化局部静态变量 :0x20000028
已初始化局部静态变量 :0x2000002c
常量区地址
已初始化全局只读变量 :0x080011a4
已初始化局部指针变量 :0x08000e78
已初始化局部指针变量 :0x08000e84
代码区地址
程序代码区main函数入口地址 :0x08000d6d
四、单片机中的内存分布
在keil中编译程序完成后,在最下面的“Build Output”输出栏,可以看到有这样一行信息“Program Size: Code=321676 RO-data=35972 RW-data=4788 ZI-data=126284”,这句话提供了程序在编译后的大小信息,包括Code、RO-data、RW-data和ZI-data的大小。这些大小信息对开发人员和嵌入式系统设计者来说具有以下作用:
-
内存优化:通过了解不同部分的大小,可以评估程序对内存的占用情况,帮助进行内存优化。例如,可以通过减少代码大小、优化数据存储方式或优化算法来减少内存的使用,从而节约系统资源。
-
内存规划:了解程序的Code、RO-data、RW-data和ZI-data大小,可以帮助进行内存规划。在嵌入式系统设计中,内存资源是有限的,因此需要合理地分配内存空间来满足程序的需求。通过了解不同部分的大小,可以确定它们在内存中的布局和分配方式,以避免冲突或浪费。
-
系统可靠性:内存的合理管理对系统的可靠性至关重要。通过了解程序的大小,特别是RO-data和RW-data的大小,可以确保所使用的内存空间不会超出设备的可用内存范围,避免内存溢出和相关的错误。
-
调试和故障排查:Program Size提供的信息可以用于调试和故障排查。如果程序运行时出现问题,例如崩溃或性能问题,可以通过检查不同部分的大小和内存使用情况来定位问题所在,并分析是否存在内存相关的错误或瓶颈。
能更好的帮助开发人员在嵌入式系统设计中更好地管理内存资源,并确保程序在目标设备上正常运行。
1.含义解释
Code:代码段,指程序由编译器生成的可执行的机器指令。
-
存储位置:ROM
-
特点:只读
RO-data:数据段,指程序中的只读数据部分,包括常量、字符串、const定义的变量等。
-
存储位置:ROM
-
特点:只读
RW-data:数据段,指初始化为“非0值“的可读写数据,程序运行的时候这些数据又会常驻在RAM区,应用程序可以修改其内容。包括初始化为非零的全局变量和静态变量。
-
存储位置:ROM和RAM均有
-
特点:可读可写
ZI-data:数据段,指初始化为0值的可读可写数据,它与RW-data的区别是程序刚运行时这些数据初始值全都为0,程序运行时和RW-data的性质一样,它们也常驻在RAM区,应用程序可以更改其内容。包括未初始化和初始化为零的全局变量和静态变量。
-
存储位置:RAM
-
特点:可读可写
扩充一个:
ZI-data 的栈空间(Stack)及堆空间(Heap):
在C语言中,函数内部定义的局部变量属于栈空间,进入函数的时候会向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。而使用malloc动态分配的变量属于堆空间。在程序中的栈空间和堆空间都是属于ZI-data区域的,这些空间都会被初始值化为0值。编译器给出的ZI-data占用的空间值中包含了堆栈的大小(经实际测试,若程序中完全没有使用malloc动态申请堆空间,编译器会优化,不把堆空间计算在内)。
2. 程序存储分布
关于哪些数据存储在Flash区域,哪些数据存储在SRAM区域,这就涉及到程序的存储状态了,应用程序具有静止和运行两种状态。
静止态的程序被存储在非易失存储器中,如内部FLASH区域,因而系统掉电后也能正常保存。
但是当程序在运行状态的时候,程序常常需要修改一些暂存数据(例如初始化非0值的数据),这些数据往往存放在Flash中,但是由于需要被修改,所以这些数据在程序运行的时候需要被复制到RAM中。
因此,程序在静止与运行的时候它在存储器中的表现是不一样的。可以参考如下图展示:
3.程序占用Flash和SRAM的空间
当程序存储到芯片的内部FLASH时(即ROM区),它占用的空间为Code + RO-data + RW-data的总和,所以如果这些内容比芯片的FLASH空间大,程序就无法被正常保存在芯片的FLASH了。当程序在执行的时候,需要占用内部SRAM空间(即RAM区),占用的空间为RW-data + ZI-data之和。
注:为什么Rom中还要存RW,因为掉电后RAM中所有数据都丢失了,每次上电RAM中的数据是被重新赋值的,每次这些固定的值就是存储在Rom中的,为什么不包含ZI段呢,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可。包含进去反而浪费存储空间。
结论,想要让一个程序正常运行,必须满足以下两个条件:
-
程序需要下载到芯片的FLASH空间,FLASH的最小空间应该大于Code + RO Data + RW Data的总和;
-
程序运行的时候,芯片内部RAM使用的空间应该大于RW Data + ZI Data之和;
注:程序编译后打开工程的map文件,在map文件的最后一段也可以看到ROM的总大小:
五、各段划分缘由(精华部分)
之前学习的时候只记住各个段分别存放什么内容,根本不知道为什么需要进行不同段的划分,也没有主动去弄懂为什么这么划分。现在知道弄清这些段的划分缘由对理解这些段的重要性了。下面分析一下划分的依据,以及这样划分有什么好处。
首先,可以先区分代码段和数据段。程序源代码编译后的机器指令就会放在代码段里;然后这里的数据段包括" .data "、" .bss "、" .rodata ",将程序中定义的全局变量和局部变量都称先称为数据段。
1.为什么把程序的“ 代码段 ”和“ 数据段 ”分开存放?
当程序被装载后,数据和指令分别被映射到两个虚拟内存区域。数据段对进程来讲是可读写的,而代码段对进程来说是只读的,所以这两个虚拟内存区域的权限可以被分别设置为可读写和只读,防止程序的指令被有意和无意地改写。
现代CPU的缓存一般被设计成数据缓存和指令缓存分离,程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
当系统中运行着多个该程序的副本时,例如多个线程同时都运行同一个程序,它们的代码段指令都是一样的,所以内存中只需要保存一份该程序的代码段,然后将每个副本进程的数据段区域分来,这样可以节省大量空间。
2. 为什么数据段还需要分 .data、.bss、.rodata 这么麻烦?有什么区别?
主要根据两个维度进行区分,是否占内存空间、读写权限。
以初始化的全局变量和局部静态变量都保存在" .data “段。未初始化的全局变量和局部静态变量一般都放在” .bss "段,因为未初始化的变量默认值为0,本来它们也可以放在.data段,但是因为它们都是0,所以为它们在.data段分配空间并且存放数据0是没有必要的。
" .data"段和" .bss “段都是可读写的数据段,而” .rodata “存放的是只读数据,主要是一些const变量和字符串常量。单独设立” .radata “段的好处是,在程序加载的时候可以将” .rodata “段的属性映射成只读,这样对这个段的任何修改操作都作为非法操作处理。另外在某些平台还可以将” .rodata "段存放在只读存储器,例如ROM,通过硬件保证只读。
3. 为什么全局变量还有细分初始化和未初始化?
为什么把全局变量分开存放,初始化为0和未初始化的全局变量放BSS区,初始化不为0的全局变量存放在数据区。
在程序有两个存储状态,一个是静止状态,一个是运行状态。静止状态的程序被存储在了非易矢存储器中,单片机一般是放在flash中,因此掉电后也能正常保存;
但是当程序处于运行状态时,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中(RAM),掉电会丢失,这时候我们可以把未初始化的和初始化为0的全局变量放在SRAM中,因为SRAM会自动把其中的变量初始化为0;这样就可以减少从ROM读数据的次数,提高整个程序的效率;
从下图我们可以看出,在处于运行状态时,SRAM会自动将放在它中的变量初始化为0, SRAM会将放在其中的数据自动初始化为0, 然后再运行程序将RW section的数据提取到SRAM中:
上图中的左侧是应用程序的存储状态,右侧是运行状态,上方棕色区域是RAM存储器区域,下方黄色区域是ROM存储器区域。
程序在存储状态时,RO段(RO section)及RW段都被保存在ROM区(数据不能被修改)。当程序开始运行时,内核直接从ROM中读取代码,并且在执行应用程序代码前,会先执行一段加载代码,它把RW段数据从ROM复制到RAM(因为RW数据在执行过程中可能需要被修改), 并且在RAM中加入ZI段,ZI段的数据都会被初始化为0。加载完后RAM区准备完毕,正式开始执行主体程序。
编译生成的RW-data的数据属于图中的RW端,ZI-data的数据属于图中的ZI段。是否需要掉电保存,这就是把RW-data与ZI-data区别存储的原因,因为在RAM创建数据的时候,默认值为0,但如果有的数据要求初值非0,那就需要使用ROM记录该初始值,运行时再复制到RAM中。
本期的内容到这就结束了,如果觉得文章不错,可以点赞、收藏和关注哦,谢谢大家收看,下期再见!
以上这些全是干货,不仅工作中会遇到,就连面试也都会出现,因此,掌握这部分内容,是嵌入式技术人员不可逃避的,建议收藏保存。如果觉得很有帮助,可以打赏一下老王哦,你的支持就是我最大的前进动力。
好了,这里给大家说了事,之前本没有打算建微信群的,但昨天有粉丝询问,再听了他的建议后,我还是创建一个微信技术交流群吧!详情请关注微信公众号。
关于更多嵌入式C语言、FreeRTOS、RT-Thread、Linux应用编程、linux驱动等相关知识,关注公众号【嵌入式Linux知识共享】,后续精彩内容及时收看了解。