Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的地址分配


前言

全文主要介绍全局常量、全局变量、局部变量、静态变量、堆、栈等概念,并且在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。
目的:
1)归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析;
2)加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解。


一、全局变量VS局部变量

1.全局变量

所有的函数外部定义的变量,它的作用域是整个程序,也就是所有的源文件,包括.c和.h文件。

2.局部变量

定义在函数体内部的变量,作用域仅限于函数体内部。离开函数体就会无效。再调用就是出错。

3.二者之间的区别

全局变量局部变量
定义位置在方法外部,直接写在类中在方法内部
作用范围整个类中都可以使用只能在方法中使用
默认值如果没有赋值,则有默认值,规则同数组没有默认值,要使用必须手动赋值
内存位置位于堆内存位于栈内存

4.举例

(1)先来看下面一段程序:
在这里插入图片描述
乍一看没有问题,但是却报错了,这个c=a+a,似乎是语句,必须在函数体内才能执行,报错提示 c没有指定类型,后来看到这么一句话:
赋值语句需要有函数执行,c语言最基本的执行体是函数,你可以初始化,但不能在函数体外赋值。

(2)全局变量的例子如下:

在这里插入图片描述

提示的错误是 变量 x 没有定义,原因是:
C语言代码是从前往后依次执行的,由于 x 定义在函数 func1() 之后,所以在 func1() 内无效;

修改程序如下:
在这里插入图片描述
可以看出,输出正确的答案,所以全局变量必须定义在我们需要用它的函数体之前。

(3)注意事项:
①在main函数中定义的变量也是局部变量,只能在main函数中使用;同时,main函数中也不能使用其他函数中定义的变量。main函数也是一个函数,与其它函数地位平等。

②形参变量、在函数体内定义的变量都是局部变量,实参给形参传值的过程也就是给局部变量赋值的过程。

③可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互补干扰,也不会发生混乱。

④在语句块(由一对{}包含的若干条语句)中也可以定义变量,它的作用域只限于当前的语句块。

(4)综合理解
代码如下:

#include <stdio.h>

int n=10;//全局变量

void func1()
{
	int n=20;//局部变量
	printf("func1 n: %d\n",n); 
} 

void func2(int n)
{
	printf("func2 n: %d\n",n); 
} 

void func3()
{
	printf("func3 n: %d\n",n); 
} 

int main()
{
	int n=30;//局部变量
	func1();
	func2(n);
	func3(); 
	
	//代码块由{}包围 
	{
		int n=40;//局部变量 
		printf("block n: %d\n",n);
	}
	
	printf("main n :%d\n",n);
	return 0;
	
}

分析:
(1). 先调用fun1(),在fun1()中,有局部变量n,所以输出的结果是 n:20;
(2). 对于fun2(),使用了形参,形参的作用范围也是整个函数内(实参给形参传值的过程也就是给局部变量赋值的过程),所以输出的结果是 n:30;
(3). 对于fun3(),没有局部变量,所以全局变量定义的 n 对所有的函数都可见。所以输出的结果是 n:10;
(4). 根据局部变量注意事项(代码块中的局部变量)。所以输出的结果是 n:40;
(5). C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。对于 main() 函数,即使代码块中的 n 离输出语句更近,但它仍然会使用 main() 函数开头定义的 n,所以输出结果是 30。

运行结果:
在这里插入图片描述

二、C/C++程序的内存分配

1.预备知识

一个由C/C++编译的程序占用的内存分为以下几个部分:
(1).栈(stack): 由编译器进行管理,自动分配和释放,存放函数调用过程中的各种参数、局部变量、返回值以及函数返回地址。栈区就像是一家客栈,里面有很多房间,客人来了之后自动分配房间,房间里的客人可以变动,是一种动态的数据变动。
(2).堆(heap): 用于程序动态申请分配和释放空间。C语言中的malloc和free,C++中的new和delete均是在堆中进行的。正常情况下,程序员申请的空间在使用结束后应该释放,若程序员没有释放空间,则程序结束时系统自动回收。
(3).全局(静态)存储区: 分为DATA段和BSS段。DATA段(全局初始化区)存放初始化的全局变量和静态变量;BSS段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。
(4).文字常量区: 存放常量字符串(程序在运行的期间不能够被改变的量,例如: 10,字符串常量”abcde”, 数组的名字等)。程序结束后由系统释放。
(5).程序代码区: 存放程序的二进制代码,即CPU执行的机器指令,并且是只读的。

如下图,是内存分区的示意图:
在这里插入图片描述

例子程序:
这是一个前辈写的,非常详细:

//main.cpp
int a = 0; //全局初始化区
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main() {
    int b; //栈
    char s[] = "abc"; //栈
    char *p2; //栈
    char *p3 = "123456"; //123456\0在常量区,p3在栈上。
    static int c = 0; //全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);
    //分配得来得10和20字节的区域就在堆区。
    strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

2.堆和栈的理论知识

2.1 申请方式

stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
p1 = (char *)malloc(10);
在C++中用new运算符
p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。

2.2 申请后系统的响应

栈: 只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
堆: 首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

2.3 申请大小的限制

栈:在Windows下,栈是向低地址(栈顶是高地址,栈底是低地址)扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

2.4 申请效率的比较:

由系统自动分配,速度较快。但程序员是无法控制的。
是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是,直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

2.5 堆和栈中的存储内容

栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆: 一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

2.6 存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在上的数组比指针所指向的字符串(例如堆)

比如:

#include <stdio.h>
void main() {
    char a = 1;
    char c[] = "1234567890";
    char *p ="1234567890";
    a = c[1];
    a = p[1];
    return;
}

对应的汇编代码

10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al

第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。

2.7小结:

堆和栈的区别可以用如下的比喻来看出:
使用栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

3.windows进程中的内存结构

接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。

首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。 每种变量都有不同的分配方式。先来看下面这段代码:

#include <stdio.h>
int g1=0, g2=0, g3=0;//全局初始化区 DATA段
int main()
{
    static int s1=0, s2=0, s3=0;//初始化的静态局部变量 DATA段
    int v1=0, v2=0, v3=0;//临时申请的局部变量 栈
    //打印出各个变量的内存地址    
    printf("0x%08x\n",&v1); //打印各本地变量的内存地址
    printf("0x%08x\n",&v2);
    printf("0x%08x\n\n",&v3);
    printf("0x%08x\n",&g1); //打印各全局变量的内存地址
    printf("0x%08x\n",&g2);
    printf("0x%08x\n\n",&g3);
    printf("0x%08x\n",&s1); //打印各静态变量的内存地址
    printf("0x%08x\n",&s2);
    printf("0x%08x\n\n",&s3);
    return 0;
}

编译后的执行结果是:
在这里插入图片描述

输出的结果就是变量的内存地址。其中v1,v2,v3本地变量g1,g2,g3全局变量s1,s2,s3静态变量
可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。
对于一个进程的内存空间而言,可以在逻辑上分成3个部分:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。

如下图所示:
在这里插入图片描述

4.各区的存放位置

下面对这些区存放在哪种介质上进行讨论。

首先,我们需要明白RAM和ROM、Flash Memory的物理特性:

RAM
RAM又称随机存取存储器,存储的内容可通过指令随机读写访问。RAM中的存储的数据在掉电是是会丢失,因而只能在开机运行时存储数据。其中RAM又可以分为两种,一种是Dynamic RAM(DRAM动态随机存储器),另一种是Static RAM(SRAM,静态随机存储器)。

ROM
ROM又称只读存储器,只能从里面读出数据而不能任意写入数据。ROM与RAM相比,具有价格高,容量小的缺点。但由于其具有掉电后数据可保持不变的优点,因此常用也存放一次性写入的程序和数据,比如主版的BIOS程序的芯片就是ROM存储器。

Flash Memory
由于ROM具有不易更改的特性,后面就发展了Flash Memory。Flash Memory不仅具有ROM掉电不丢失数据的特点,又可以在需要的时候对数据进行更改,不过价格比ROM要高。

不同数据的存放位置
由前面的分析我们知道,代码区和常量区的内容是不允许被修改的,ROM(STM32就是Flash Memory)也是不允许被修改的,所以代码区和常量区的内容编译后存储在ROM中。

栈、堆、全局区(.bss段、.data段)都是存放在RAM中。

5.Keil中的Build Output窗口

C 语言上分为栈、堆、bss、data、code段;
MDK 下分为:Code、RO-data、RW-data、ZI-data 这几个段。

任意在Keil中编译一个文件(点击Rebuild按钮)
在这里插入图片描述

编译后,我们可以看到存在CodeRO-dataRW-dataZI-data四个代码段大小

其中Code代码占用大小RO-data只读常量RW-data已初始化的可读可写变量ZI-data未初始化的可读可写变量。

有些时候,我们需要知道RAM和ROM的使用情况如何,那么我们就可以使用下面的公式计算。

RAM = RW-data + ZI-data
ROM = Code + RO-data + RW-data
Flash=Code + RO Data + RW Data

这个是 MDK 编译之后能够得到的每个段的大小,也就能得到占用相应的FLASH和RAM的大小,但是还有两个数据段也会占用RAM,但是是在程序运行的时候,才会占用,那就是堆和栈。

在stm32的启动文件.s文件里面,就有堆栈的设置,其实这个堆栈的内存占用就是在上面RAM分配给RW-data+ZI-data之后的地址开始分配的。

是编译器调用动态内存分配的内存区域;

是程序运行的时候局部变量的地方,所以局部变量用数组太大了都有可能造成栈溢出。

堆栈的大小在编译器编译之后是不知道的,只有运行的时候才知道,所以需要注意不要造成堆栈溢出,会出现 hardfault 问题。
————————————————
版权声明:本文为CSDN博主「噗噗的罐子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_46467126/article/details/121875496

三、Ubuntu(x86)系统和STM32(Keil)中编程验证

1.分别在Ubuntu、STM32下运行如下代码

#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] = "yaoyao";//栈
    //定义常量字符串
    char *var1 = "1234567890";
    char *var2 = "abcdefghij";
    //动态分配——堆区
    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;
}

2.Ubuntu运行

将上面的代码放入nano文本编辑器中,进行编译
在这里插入图片描述
可以发现,Ubuntu在栈区堆区地址值都是从上到下增长的。

3.Keil运行

keil 环境下默认的内存配置说明
在这里插入图片描述
① 默认分配的 ROM 区域是0x8000000开始,大小是0x80000的一片区域,那么这篇区域是只读区域,不可修改,也就是存放的代码区常量区

② 默认分配的 RAM 区域是0x20000000开始,大小是0x10000的一片区域,这篇区域是可读写区域,存放的是静态区栈区堆区

在这里插入图片描述

实际运行情况
在工程中进行串口初始化(由于本人串口配置暂未完成,以下内容后续完善)
修改主函数

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.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)
 {		
 	u16 t;  
	u16 len;	
	u16 times=0;
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 //串口初始化为115200
 	LED_Init();			     //LED端口初始化
	KEY_Init();          //初始化与按键连接的硬件接口
 	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] = "yaoyao";
    //定义常量字符串
    char *var1 = "1234567890";
    char *var2 = "abcdefghij";
    //动态分配
    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;
	}	 
 }

总结

主要是对C程序的内存分配有进一步的认识,知道一个C程序内存应该包括哪些部分。其中,主要是程序段、数据段、堆栈三个部分。不同系统下面,区域内的地址值变化是不相同。总的来说,是对内存的分配有了比较新的认识。

参考

C语言全局变量和局部变量总结

关于堆栈的讲解(我见过的最经典的)

什么是代码区、常量区、静态区(全局区)、堆区、栈区?

Linux虚拟地址空间布局以及进程栈和线程栈总结

基于Ubuntu、树莓派、STM32分析C语言经编译后的分区情况

“栈”顶到底是上面还是下面?高地址还是低地址?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菲菲QAQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值