一、前言
本文我将会和大家一起先回顾C程序中的全局变量、局部变量、堆、栈的概念,并通过C程序分别在Ubuntu和STM32中找到变量的地址分配,并对其进行分析。
二、了解C程序的内存分配
1、堆区是程序里动态分配的内容,堆区的内存容量大,使用灵活,分配后要自行回收容易产生内存碎片。
2、栈区主要是存储函数的局部变量,然后程序结束后操作系统自行回收但是栈区容量较小。
3、全局区(静态区)(static),全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(.data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bss)。 - 程序结束后由系统释放。
4、文字常量区 —常量字符串就是放在这里的(.rodata)。 程序结束后由系统释放。
5、程序代码区—存放函数体的二进制代码(.text)。
按存储区域分,全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。
按作用域分,全局变量在整个工程文件内都有效;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效。
全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。
详细请参考:全局变量和局部变量在内存里的区别
其实在程序运行时,由于内存的管理方式是以页为单位的,而且程序使用的地址都是虚拟地址,当程序要使用内存时,操作系统再把虚拟地址映射到真实的物理内存的地址上。所以在程序中,以虚拟地址来看,数据或代码是一块块地存在于内存中的,通常我们称其为一个段。而且代码和数据是分开存放的,即不储存于同于一个段中,而且各种数据也是分开存放在不同的段中的。
正常的程序在内存中可以分为程序段、数据段和堆栈三部分。
其中程序段里存放的是程序的机器码和只读数据,这个存储区是只读的,对于其进行写操作是非法的。
数据段中保存的是程序中的静态数据。
堆栈是一个地址连续的存储区。堆栈指针寄存器(SP)指向堆栈的栈顶,栈底的内存是固定的,存储的新数据作为栈顶,一个一个叠加起来,一直到出栈的时候栈顶的数据先出来从上至下,所以堆栈是后进先出。
内存区示意图:
相信现在大家都对我们的内存分配和变量存储地址有了一定的了解。
三、分别在Ubuntu和STM32中验证C变量地址分配
我们可以写一段程序,在程序中定义全局变量和局部变量,并通过程序输出这些变量所保存的地址信息。
代码:
#include <stdio.h>
#include <stdlib.h>
//定义全局变量
int init_global_a = 1;
int uninit_global_a;
static int inits_global_b = 2;
static int uninits_global_b;
void output(int a)
{
printf("hello");
printf("%d",a);
printf("\n");
}
int main( )
{
//定义局部变量
int a=2;
static int inits_local_c=2, uninits_local_c;
int init_local_d = 1;
output(a);
char *p;
char str[10] = "lyy";
//定义常量字符串
char *var1 = "1234567890";
char *var2 = "qwertyuiop";
//动态分配
int *p1=malloc(4);
int *p2=malloc(4);
//释放
free(p1);
free(p2);
printf("栈区-变量地址\n");
printf(" a:%p\n", &a);
printf(" init_local_d:%p\n", &init_local_d);
printf(" p:%p\n", &p);
printf(" str:%p\n", str);
printf("\n堆区-动态申请地址\n");
printf(" %p\n", p1);
printf(" %p\n", p2);
printf("\n全局区-全局变量和静态变量\n");
printf("\n.bss段\n");
printf("全局外部无初值 uninit_global_a:%p\n", &uninit_global_a);
printf("静态外部无初值 uninits_global_b:%p\n", &uninits_global_b);
printf("静态内部无初值 uninits_local_c:%p\n", &uninits_local_c);
printf("\n.data段\n");
printf("全局外部有初值 init_global_a:%p\n", &init_global_a);
printf("静态外部有初值 inits_global_b:%p\n", &inits_global_b);
printf("静态内部有初值 inits_local_c:%p\n", &inits_local_c);
printf("\n文字常量区\n");
printf("文字常量地址 :%p\n",var1);
printf("文字常量地址 :%p\n",var2);
printf("\n代码区\n");
printf("程序区地址 :%p\n",&main);
printf("函数地址 :%p\n",&output);
return 0;
}
1、在Ubuntu上运行:
通过观察上两图,我们可以发现在Ubuntu中栈区地址是逐步增大的,同样堆区的地址也是逐渐增大的。
在STM32上运行:
由于在STM32上运行时需要我们将地址返回到上位机上,所以需要配合串口初始化函数才能在上位机上显示对应的地址信息。串口通信可以参考我之前的博客:STM32USART串口通信
我们只需要将main.c文件修改一下即可:
#include "stm32f10x.h" //STM32头文件
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include <stdio.h>
#include <stdlib.h>
//定义全局变量
int init_global_a = 1;
int uninit_global_a;
static int inits_global_b = 2;
static int uninits_global_b;
void output(int a)
{
printf("hello");
printf("%d",a);
printf("\n");
}
int main (void){//主程序
u8 a=7,b=8;
//初始化程序
RCC_Configuration(); //时钟设置
USART1_Init(115200); //串口初始化(参数是波特率)
//主循环
while(1){
int a=2;
static int inits_local_c=2, uninits_local_c;
int init_local_d = 1;
output(a);
char *p;
char str[10] = "zzq";
//定义常量字符串
char *var1 = "1234567890";
char *var2 = "abcdefghij";
//动态分配
int *p1=malloc(4);
int *p2=malloc(4);
//释放
free(p1);
free(p2);
printf("栈区-变量地址\n\r");
printf(" a:%p\n\r", &a);
printf(" init_local_d:%p\n\r", &init_local_d);
printf(" p:%p\n\r", &p);
printf(" str:%p\n\r", str);
printf("\n堆区-动态申请地址\n\r");
printf(" %p\n\r", p1);
printf(" %p\n\r", p2);
printf("\n全局区-全局变量和静态变量\n\r");
printf("\n.bss段\n");
printf("全局外部无初值 uninit_global_a:%p\n\r", &uninit_global_a);
printf("静态外部无初值 uninits_global_b:%p\n\r", &uninits_global_b);
printf("静态内部无初值 uninits_local_c:%p\n\r", &uninits_local_c);
printf("\n.data段\n\r");
printf("全局外部有初值 init_global_a:%p\n\r", &init_global_a);
printf("静态外部有初值 inits_global_b:%p\n\r", &inits_global_b);
printf("静态内部有初值 inits_local_c:%p\n\r", &inits_local_c);
printf("\n文字常量区\n\r");
printf("文字常量地址 :%p\n\r",var1);
printf("文字常量地址 :%p\n\r",var2);
printf("\n代码区\n\r");
printf("程序区地址 :%p\n\r",&main);
printf("函数地址 :%p\n\r",&output);
delay_ms(1000); //延时
}
}
编译一下,竟然报错了:
error: #268: declaration may not appear after executable statement in block 通过在网上查找资料,我发现这个错误是因为声明不能出现在可执行状态之后,C语言关于变量的定义只能放在函数的开头,放在执行语句的前面定义,这是C89的标准。
后来的C99标准就已经改变了,无论定义在之前还是之后都是可以的。
解决方案:
点击“仙女棒”,在C/C++ 选项卡下的 C99 Mode勾选上即可:
再次编译,没有报错,完美!
接下来只需要我们将程序烧录进STM32并打开串口助手就OK啦!
我们一起来看看输出的结果吧:
如图我们可以发现STM32的栈区地址逐渐减小,但是其堆区的地址是逐渐增大的
四、结果分析
通过实验我们发现,Ubuntu在栈区和堆区的地址都是从上到下增长的,但是STM32栈区的地址是从上到下减小,而堆区是从上到下增长的。从每个区来看,栈区的地址都是高地址,代码区的地址都处于低地址。
STM32的数据存储位置:
RAM(随机存取存储器)
存储的内容可通过指令随机读写访问。RAM中的存储的数据在掉电是会丢失,因而只能在开机运行时存储数据。其中RAM又可以分为两种,一种是Dynamic RAM(DRAM动态随机存储器),另一种是Static RAM(SRAM,静态随机存储器)。栈、堆、全局区(.bss段、.data段)都是存放在RAM中。
ROM(只读存储器)
只能从里面读出数据而不能任意写入数据。ROM与RAM相比,具有读写速度慢的缺点。但由于其具有掉电后数据可保持不变的优点,因此常用也存放一次性写入的程序和数据,比如主版的BIOS程序的芯片就是ROM存储器。代码区和常量区的内容是不允许被修改的,所以存放于ROM中。
我们可以通过Keil来看出STM32的RAM和ROM存储器大小和起始地址值:
如图所示为我们单片机的RAM和ROM存储器的相关信息,其中RAM用于存放栈、堆和全局区(.bss段和.data段)。ROM用于存放代码区和文字常量区。
五、总结
主要是对C程序的内存分配有进一步的认识,知道一个C程序内存应该包括哪些部分。其中,主要是程序段、数据段、堆栈三个部分。不同系统下面,区域内的地址值变化是不相同。总的来说,是对内存的分配有了比较新的认识。