嵌入式作业3

三. 编写一个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段。
在这里插入图片描述
可以看到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

其中程序运行时,堆和栈也会占用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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值