C语言+单片机-内存分布详解,全网最全,值得收藏保存

       

目录

一、C语言内存分区

1. 代码区

2. 常量区

3. 全局(静态)区

4. 堆区(heap)

5. 栈区(stack)

二、STM32存储器分配

1. 随机存储器—RAM

2. 只读存储器—ROM

三、基于STM32代码验证

1. 详细代码如下

2. 运行结果如下

四、单片机中的内存分布

1.含义解释

2. 程序存储分布

3.程序占用Flash和SRAM的空间

五、各段划分缘由(精华部分)

1.为什么把程序的“ 代码段 ”和“ 数据段 ”分开存放?

2. 为什么数据段还需要分 .data、.bss、.rodata 这么麻烦?有什么区别?

3. 为什么全局变量还有细分初始化和未初始化?


        本篇主要讲在C语言和单片机中内存分布情况。

一、C语言内存分区

C语言内存分区示意图如下:

ddab81176bcfdfda0d35f898913149d0.png

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环境下默认的内存配置见下图:

fa731f684c18ff29863e5d492a4c1ee2.png

  • ROM区域是0x8000000开始,大小是0x10000,这片区域是只读区域,不可修改,存放代码区和常量区。

  • RAM区域是0x20000000开始,大小是0x5000,这片区域是可读写区域,存放的是全局(静态)区、堆区和栈区。

该芯片的内部分区如下图所示:

8083bb18c6c9025c5e28a9ccb39f2db9.png

三、基于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”,这句话提供了程序在编译后的大小信息,包括CodeRO-dataRW-dataZI-data的大小。这些大小信息对开发人员和嵌入式系统设计者来说具有以下作用:

  1. 内存优化:通过了解不同部分的大小,可以评估程序对内存的占用情况,帮助进行内存优化。例如,可以通过减少代码大小、优化数据存储方式或优化算法来减少内存的使用,从而节约系统资源。

  2. 内存规划:了解程序的Code、RO-data、RW-data和ZI-data大小,可以帮助进行内存规划。在嵌入式系统设计中,内存资源是有限的,因此需要合理地分配内存空间来满足程序的需求。通过了解不同部分的大小,可以确定它们在内存中的布局和分配方式,以避免冲突或浪费。

  3. 系统可靠性:内存的合理管理对系统的可靠性至关重要。通过了解程序的大小,特别是RO-data和RW-data的大小,可以确保所使用的内存空间不会超出设备的可用内存范围,避免内存溢出和相关的错误。

  4. 调试和故障排查: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中。

        因此,程序在静止与运行的时候它在存储器中的表现是不一样的。可以参考如下图展示:

bed17834e3f6a718f6861e2182ad06f8.png

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的总大小:

5a720b3f2b7d781460af1c66ec50f0b9.png

五、各段划分缘由(精华部分)

        之前学习的时候只记住各个段分别存放什么内容,根本不知道为什么需要进行不同段的划分,也没有主动去弄懂为什么这么划分。现在知道弄清这些段的划分缘由对理解这些段的重要性了。下面分析一下划分的依据,以及这样划分有什么好处。

        首先,可以先区分代码段和数据段。程序源代码编译后的机器指令就会放在代码段里;然后这里的数据段包括" .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中:

b1b9b2d28d1e1e108f29e5f2fcab6423.png

        上图中的左侧是应用程序的存储状态,右侧是运行状态,上方棕色区域是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知识共享】,后续精彩内容及时收看了解。