文章目录
三. 编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。1)归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析;2)加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解。下图是一个Cortex-M4的存储器地址映射示意图(与Cortex-M3/stm32F10x基本相同,只存在微小差异)
一、C程序中的一些基本概念
在标准C中,编译出来的可执行程序分为代码区(text)、数据区(data)和未初始化数据区(bss)3个部分。如下代码:
#include <stdlib.h>
int a = 0;//a在全局已初始化数据区
char *p1;//p1在BSS区(未初始化全局变量)
void main(){
int b;//b在栈区
int c;//c为全局(静态)数据,存在于已初始化数据区
char s[] = "abc";//s为数组变量,存储在栈区
char *p2,*p3;//p2,p3在栈区
p2 = (char *)malloc(10);//分配得来的10个字节的区域在堆区
p3 = (char *)malloc(20);//分配得来的20个字节的区域在堆区
free(p2);
free(p3);
}
代码段(text):存放代码的地方。只能访问,不能修改,代码段就是程序中的可执行部分,直观理解代码段就是函数堆叠组成的。
数据段(data):全局变量和静态局部变量存放的地方。也被称为数据区、静态数据区、静态区:数据段就是程序中的数据,直观理解就是C语言程序中的全局变量。注意是全局变量或静态局部变量,局部变量不算。
未初始化数据区(bss):bss段的特点就是被初始化为0,bss段本质上也是属于数据段。
stm32L152芯片内部分区如下:
1.1、 局部变量
定义在函数内部或复合语句内部的变量称为局部变量(Local Variable),它的作用域仅限于函数或复合语句内部,离开作用域就不能再使用。
void function(void)
{
int i = 1;
i++;
printf("i = %d.\n", i);
}
静态局部变量需要定义前面加static
关键字。
- 1、只会初始化一次,退出函数不会被回收,下次调用直接用上一次的值。
1.2、全局变量
全局变量也称外部变量,定义在函数外部。不属于哪一个函数,而是属于一个源程序文件;其作用域是整个源程序。
- 从程序运行时起占据内存,程序退出时释放内存。
- 在不指定初值的情况下,初始化为0。
- 在同一源文件中,允许全局变量和局部变量同名。在局部变量的作用域内,全局变量不起作用。重名时局部变量会屏蔽全局变量。
静态全局变量解决重名问题,在定义前加上static关键字,只能在本文件内使用,无法在其它文件使用。
跨文件引用全局变量(extern) 可以在一个.c文件中定义全局变量,也可以在另一个文件中引用该变量(引用前要声明)
1.3、堆栈
堆栈是一个特殊存储区,属于RAM空间的一部分,用于在函数调用、中断切换时保存和恢复现场数据。堆栈具有FILO(先进后出)的特性。主要包括PUSH(入栈)和POP(出栈)操作:
- PUSH:堆栈指针(SP)加1,在顶部加入一个元素。
- POP:先将SP所指示的内部ram单元中内容送入直接地址寻址的单元中,SP减1。
为什么需要堆栈
堆栈是为了减少程序内存(RAM)占用的问题。单片机的内存容量一直都是非常有限的,在单片机上电后,所有的变量又都需要被拷贝到内存中运行。为了解决这个矛盾,尽可能的节约代码在运行时变量所占用的内存空间,“堆栈”和“局部变量”两个概念就提了出来。
- 局部变量: 从软件的角度指出某些变量只需要某个特定时间段生存期,比如函数内部或者复合语句内部。
- 堆栈:从硬件角度为程序员控制局部变量的生存期提供便利。局部变量的出现减少内存占用时间,也提高了函数的重用性。但是每次都需要手动的用汇编指令去创建删除变量,那么为了让局部变量创建和删除自动化起来,就出现了栈。通过esp,ebp两个堆栈寄存器实现,其原理如下:
Esp寄存器: 存储栈顶地址
Ebp寄存器:存储栈基地址
1.4 栈区
- 栈区由编译器自动分配释放,由系统自动管理,无需手动管理。
- 栈区上的内容只在函数范围内存在,当函数运行结束,这些内容会被自动销毁。
- 栈区按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性查,最大空间不大。
- 栈区是先进后出原则。
- 临时创建的局部变量和const定义的局部变量存放在栈区
- 函数调用和返回时,入口参数和返回值存放在栈区
1.5 堆区
- 堆区由程序员分配内存和释放
- 堆区按内存地址由高到低生长,大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大
- 使用malloc等函数动态分布的变量
//main.cpp
#include <stdio.h>
static unsigned int val1 = 1; //val1存放在.data段
unsigned int val2 = 1; //初始化的全局变量存放在.data段
unsigned int val3 ; //未初始化的全局变量存放在.bss段
const unsigned int val4 = 1; //val4存放在.rodata(只读数据段)
unsigned char Demo(unsigned int num) // num 存放在栈区
{
char var = "123456"; // var存放在栈区,"123456"存放在常量区
unsigned int num1 = 1 ; // num1存放在栈区
static unsigned int num2 = 0; // num2存放在.data段
const unsigned int num3 = 7; //num3存放在栈区
void *p;
p = malloc(8); //p存放在堆区
free(p);
return 1;
}
void main()
{
unsigned int num = 0 ;
num = Demo(num); //Demo()函数的返回值存放在栈区。
}
二、Keil中Build Output窗口
MDK将C中的栈、堆、bss、data、code段分为了Code、RO-data、RW-data、ZI-data段。
可以看到Code
、RO-data
、RW-data
、ZI-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
其中程序运行时,堆和栈也会占用RAM空间。
在stm32的启动文件.s文件里面,就有堆栈的设置,其实这个堆栈的内存占用就是在上面RAM分配给RW-data+ZI-data之后的地址开始分配的。
三、堆栈的对比
3.1 申请方式
堆: 由系统自动分配。
栈: 需要动态申请。
3.2 申请和系统响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则会报异常提示栈溢出
堆:首先要知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该节点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3.3 申请大小的限制
栈:在windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在windows下,栈的大小是2M,如果申请的空间超过栈的剩余空间,就会提示栈溢出。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
3.4 申请效率的比较
栈: 有系统自动分配,速度快。但程序员无法控制
堆:动态分配,速度慢。
3.5 堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,从右到左入栈,然后是函数中的局部变量。静态变量不入栈。
堆:在堆的头部用一个字节存放堆的大小。
四、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;
}
4.1 Ubuntu运行
可以发现,Ubuntu在栈区和堆区的地址都是从上到下增长的
4.2 Keil
keil环境下默认的内存配置
- 默认分配的ROM区域是从0x8000000开始,大小是0x80000的一片区域,只读,不可修改,存放代码区和常量区。
- 默认分配的RAM区域是0x20000000开始,大小事0x10000的一片区域,这片区域可读可写存放静态区、栈区、堆区。
参考
https://zhuanlan.zhihu.com/p/93934910
https://zhuanlan.zhihu.com/p/142964520
https://blog.csdn.net/MQ0522/article/details/114823770
https://blog.csdn.net/qq_46467126/article/details/121875496