单片机学习小组
直播中

新疆切糕

11年用户 1173经验值
私信 关注

C语言内存运行时不同变量是怎样分配的

C语言内存运行时不同变量是怎样分配的?
怎样验证C语言编译后的内存地址分配是否合理?

回帖(1)

张毕鹄

2022-2-25 13:41:04
(一)C程序内存分配


1.内存分配图解



其中C程序的内存分配为栈区、堆区、全局区、常量区和代码区这五大区域,而全局区又分为.bss段和.data段。一般来说,代码区、数据区、.bss在程序运行之后,大小都是固定的,而栈区和堆区是根据数据的多少再自由的扩张,堆区从小到大扩张,栈区从大到小扩张即堆区由低地址向高地址生长,栈区由高地址向低地址生长),合理分配和使用内存,能够使内存利用率最大化。

2.内存分配介绍


1、栈区(stack)


  • 栈区介绍

    • 栈区与堆区是相互毗邻的,并且生长方向相反;当栈指针触及到堆指针时,意味着栈空间已经被耗尽(如今地址空间越来越大,及虚拟内存技术发展,栈与堆可能放置在内存的任何地方,但生长方向依然还是相向的)。
    • 栈区域包含一个LIFO结构的程序栈,其通常放置在内存的高地址处,栈区按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大
    • 栈指针寄存器跟踪栈顶位置,每当有数值被压入栈中,栈顶指针会被调整,在一个函数的调用过程中,压入的一系列数值被称作“栈帧”,栈帧至少包含一个返回地址。

  • 存放内容



    • 临时创建和const定义的局部变量存放在栈区。
    • 函数调用和返回时,其入口参数和返回值存放在栈区。



2、堆区(heap)



  • 堆区介绍
    堆区通常用作动态内存分配,堆空间起始于BSS段的末尾,并按内存地址由低到高方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大
  • 存放内容



    • 堆区用于存放程序运行中被动态分布的内存段,可增可减。
    • 可以用malloc等函数实现动态分布内存。当有malloc函数分配的内存时必须用free函数进行内存释放,否则会造成内存泄漏



3、全局区(静态区)



  • 全局区介绍
    和栈区一样,通常是用于那些在编译期间就能确定存储大小的变量的存储区,但它用于的是在整个程序运行期间都可见的全局变量和静态变量。全局区有.bss段和.data段组成,可读可写。
  • .bss段

    • 未初始化的全局变量存放在.bss段。
    • 初始化为0的全局变量和初始化为0的静态变量存放在.bss段。
    • .bss段不占用可执行文件空间,其内容有操作系统初始化。

  • .data段



    • 已经初始化的全局变量存放在.data段。
    • 静态变量存放在.data段。
    • .data段占用可执行文件空间,其内容有程序初始化。
    • const定义的全局变量存放在.rodata段。



4、常量区



  • 字符串、数字等常量存放在常量区。
  • 程序运行期间,常量区的内容不可以被修改。


5、代码区



  • 程序执行代码存放在代码区,其值不能修改(若修改则会出现错误)。
  • 字符串常量也有可能存放在代码区。

(二)Ubuntu系统下实验验证

1.详细代码

#include
#include

int k1 = 1;        //已初始化全局int型变量k1
int k2;            //未初始化全局int型变量k2
static int k3 = 2; //已初始化静态全局int型变量k3
static int k4;     //未初始化静态全局int型变量k4

int test()
{
        int j1;
    int j2;
    printf("未初始化局部int型变量j1       :%pn", &j1);
    printf("未初始化局部int型变量j2       :%pn", &j2);
return 0;
}
int main()
{
    static int m1 = 2;      //已初始化静态局部int型变量m1
    static int m2;          //未初始化静态局部int型变量m2
    int i1;              //未初始化局部int型变量i1
    int i2;                                //未初始化局部int型变量i2
    char *p;                //未初始化局部char型指针变量p
    char str[10] = "hello"; //已初始化局部char型数组str
    char *var1 = "123456";  //已初始化局部char型指针变量var1
    char *var2 = "abcdef";  //已初始化局部char型指针变量var2
    int *p1 = malloc(4);    //已初始化局部int型指针变量p1
    int *p2 = malloc(4);    //已初始化局部int型指针变量p2


    printf("栈区-变量地址n");
    printf("未初始化局部int型变量i        :%pn", &i1);
    printf("未初始化局部int型变量i2       :%pn", &i2);
    printf("未初始化局部char型指针变量p   :%pn", &p);
    printf("已初始化局部char型数组str     :%pn", str);
    test();

    printf("n堆区-动态申请地址n");
    printf("已初始化局部int型指针变量p1   :%pn", p1);
    printf("已初始化局部int型指针变量p2   :%pn", p2);

    printf("n.bss段地址n");
    printf("未初始化全局int型变量 k2      :%pn", &k2);
    printf("未初始化静态全局int型变量k4   :%pn", &k4);
    printf("未初始化静态局部int型变量m2   :%pn", &m2);

    printf("n.data段地址n");
    printf("已初始化全局int型变量k1       :%pn", &k1);
    printf("已初始化静态全局int型变量k3   :%pn", &k3);
    printf("已初始化静态局部int型变量m1   :%pn", &m1);

    printf("n常量区地址n");
    printf("已初始化局部char型指针变量var1:%pn", var1);
    printf("已初始化局部char型指针变量var2:%pn", var2);

    printf("n代码区地址n");
    printf("程序代码区main函数入口地址    :%pn", &main);

    free(p1);
    free(p2);
   
    return 0;
}

2.实验结果



3.实验结果分析



  • 栈区分配

    • 从理论上来说,栈区的地址应该是从高地址向低地址分配,但是我们可以看到上面的程序中同一main函数和test函数内的变量却是按照从低地址向高地址分配,这是为什么呢?经过查阅网上资料,得出的结果如下:
      同一函数内的局部变量的地址分配的大小不能说明问题,因为编译器分配空间可以随意,得看多个函数调用时分配局部变量的地址大小。
    • 所以这里我使用test函数和main函数里的局部变量定义来分析栈区地址分配是否是从高到低,结果如上图:
      其中:main函数的变量i的地址为0x7ffc838074e8,test函数的变量j1分配的地址为0x7ffc838074c0,可以看出栈区地址分配是从高地址往低地址生长。最终实验结果还是符合理论的。

  • 堆区分配
    可以看出,堆区地址分配是从高地址往低地址分配的,实验结果与理论相符合。

(三)STM32系统下实验验证

1.详细代码

#include "led.h"
#include "delay.h"
//#include "key.h"
#include "sys.h"
#include "usart.h"
#include
#include

int k1 = 1;        //已初始化全局int型变量k1
int k2;            //未初始化全局int型变量k2
static int k3 = 2; //已初始化静态全局int型变量k3
static int k4;     //未初始化静态全局int型变量k4

int main(void)
{      
    delay_init();                     //延时函数初始化         
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    uart_init(115200);         //串口初始化为115200
    LED_Init();                             //LED端口初始化
    //KEY_Init();          //初始化与按键连接的硬件接口
    while(1)
    {
        static int m1 = 2;      //已初始化静态局部int型变量m1
        static int m2;          //未初始化静态局部int型变量m2
        int i1;              //未初始化局部int型变量i1
        int i2;                                //未初始化局部int型变量i2
        char *p;                //未初始化局部char型指针变量p
        char str[10] = "hello"; //已初始化局部char型数组str
        char *var1 = "123456";  //已初始化局部char型指针变量var1
        char *var2 = "abcdef";  //已初始化局部char型指针变量var2
        int *p1 = malloc(4);    //已初始化局部int型指针变量p1
        int *p2 = malloc(4);    //已初始化局部int型指针变量p2


        printf("栈区-变量地址rn");
        printf("未初始化局部int型变量i1       :0x%prn", &i1);
        printf("未初始化局部int型变量i2       :0x%prn", &i2);
        printf("未初始化局部char型指针变量p   :0x%prn", &p);
        printf("已初始化局部char型数组str     :0x%prn", str);
        //test();

        printf("n堆区-动态申请地址rn");
        printf("已初始化局部int型指针变量p1   :0x%prn", p1);
        printf("已初始化局部int型指针变量p2   :0x%prn", p2);

        printf("n.bss段地址rn");
        printf("未初始化全局int型变量k2       :0x%prn", &k2);
        printf("未初始化静态全局int型变量k4   :0x%prn", &k4);
        printf("未初始化静态局部int型变量m2   :0x%prn", &m2);

        printf("n.data段地址rn");
        printf("已初始化全局int型变量k1       :0x%prn", &k1);
        printf("已初始化静态全局int型变量k3   :0x%prn", &k3);
        printf("已初始化静态局部int型变量m1   :0x%prn", &m1);

        printf("n常量区地址rn");
        printf("已初始化局部char型指针变量var1:0x%prn", var1);
        printf("已初始化局部char型指针变量var2:0x%prn", var2);

        printf("n代码区地址rn");
        printf("程序代码区main函数入口地址    :0x%prn", &main);

        free(p1);
        free(p2);        
    }
}

2.实验结果



  • 编译信息

  • 程序占用空间
    Code:表示程序所占用 FLASH 的大小(FLASH)。
    RO-data:即 Read Only-data,表示程序定义的常量,如 const 类型(FLASH)。
    RW-data:即 Read Write-data,表示已被初始化的全局变量(SRAM)
    ZI-data:即 Zero Init-data,表示未被初始化的全局变量(SRAM)


FLASH占用大小计算公式:Code+RO-data
SRAM占用大小计算公式:RW-data+ZI-data



  • 这里简要介绍一下FLASH和SRAM的区别:
    单片机中的FLASH和SRAM都有存储功能,但是存储类型不一样,FLASH是闪存,一般用来存储烧录的源码程序,断电不会丢失数据,相对于我们电脑来说是ROM(外存);而SRAM存储的一般是CPU运行时产生的临时文件,相对于电脑来说是RAM(内存)。


程序占用FLASH 大小为:6680字节(6332+348),所用的 SRAM 大小为:1824 个字节(84+1740)



  • 以下为stm32串口输出的数据:

栈区-变量地址
未初始化局部int型变量i1       :0x2000071c
未初始化局部int型变量i2       :0x20000718
未初始化局部char型指针变量p   :0x20000714
已初始化局部char型数组str     :0x20000708
堆区-动态申请地址
已初始化局部int型指针变量p1   :0x20000128
已初始化局部int型指针变量p2   :0x20000130
.bss段地址
未初始化全局int型变量k2       :0x20000004
未初始化静态全局int型变量k4   :0x2000000c
未初始化静态局部int型变量m2   :0x20000014
.data段地址
已初始化全局int型变量k1       :0x20000000
已初始化静态全局int型变量k3   :0x20000008
已初始化静态局部int型变量m1   :0x20000010
常量区地址
已初始化局部char型指针变量var1:0x08000230
已初始化局部char型指针变量var2:0x08000238
代码区地址
程序代码区main函数入口地址    :0x08000145

3.实验结果分析



  • 从stm32串口输出的数据可以看出:

    • stm32的代码区和常量区都分配在0x08000000储存地址(低地址)这一块,全局和静态变量、栈区和堆区的地址都分配在0x20000000储存地址(高地址)处。
    • stm32的栈区地址分配是按照高地址到低地址的方式进行分配,而堆区是按照低地址到高地址的方式进行分配,其他的区域基本上都按照从低地址向高地址的方式进行内存分配。

  • Keil软件里的芯片地址

    从keil软件的option中可以看出关于STM32内存的起始地址和总大小。比如说,ROM的起始地址为0x8000000,大小为0x80000;RAM的起始地址为0x20000000,大小为0x10000。

(四)总结

通过在不同平台进行编程验证C程序编译后在内存的地址分配,我更加深入地了解了不同变量及代码在内存中地址分配的不同,同时对于不同平台的C程序的内存分配也有了更加清晰的认识。由于作者水平有限,文中难免有疏漏之处,希望读者批评指正。
举报

更多回帖

发帖
×
20
完善资料,
赚取积分