Ubuntu与stm32变量分配方式
一、用户空间内存分区
存储区图解
对各个内存分区的说明:
内存分区 | 说明 |
---|---|
程序代码区(code) | 存放函数体的二进制代码。一个C语言程序由多个函数构成,C语言程序的执行就是函数之间的相互调用。 |
文字常量区(constant) | 存放一般的常量、字符串常量等。这块内存只有读取权限,没有写入权限,因此他们的值在程序运行期间不能改变。 |
全局数据区(global data) | 存放全局变量、静态变量等。这块内存有读写权限,因此他们的值在程序运行期间可以任意改变。 |
堆区(heap) | 一般由程序员分配和释放,若程序员不释放,程序运行结束时由操作系统回收。malloc()、free()函数操作的就是这块内存。 |
动态链接库 | 用于在程序运行期间加载和卸载动态链接库。 |
栈区(Stack) | 存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。 |
内核空间 | 在程序拥有的独立空间中,分出一部分给操作系统内核使用,应用程序无法直接访问这一段内存,这一部分内存地址被称为内核空间(Kernel Space)。 |
在Windows下和Ubuntu系统下的内存模型如下(基于64位)
linux内存模型
Windows内存模型
不同于Linux,Windows是闭源的,有版权保护,资料较少,不好深入研究每一个细节,至今仍然有一些内部原理不被大家知晓。关于Windows地址空间的内存分布,官网上只给出了简单的说明:
- 对于32位程序,内核占用较高的2GB,剩下的2GB分配给用户程序
- 对于64位程序,内核占用最高的248TB,用户程序占用最低的8TB
参考:
Linux、Windows下C语言内存布局(内存模型)
接下来,让我们编写程序打印变量地址来比较其不同点
二、打印各变量地址信息
(一)代码
#include <stdio.h>
#include <stdlib.h>
//定义全局变量
int init_global_a = 1; //.text段
int uninit_global_a; //.bss段
static int inits_global_b = 2; //.text段
static int uninits_global_b; //.bss段
void output(int a)
{
printf("这是一个变量分配验证函数\n");
printf("%d\n",a);
printf("\n");
}
int main( )
{
//定义局部变量
int a=2;
static int inits_local_c=2, //.text段
uninits_local_c; //.bss段
int init_local_d = 1; //栈区
output(a);
char *p;
char str[10] = "lyy"; //栈区
//定义常量字符串
char *var1 = "1234567890"; //文字常量区
char *var2 = "qwertyuiop"; //文字常量区
//动态分配
int *p1=(int *)malloc(4); //堆区
int *p2=(int *)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;
}
(二)不同系统下运行
在Ubuntu下运行
在Windows下运行
三、MDK下程序的组成、存储与运行
(一)程序的存储
每次我们在使用MDK工具时,每次编译成功都会输出以下信息
在工程的编译提示输出信息中有一个语句“Program Size: Code=xx RO-data=xx RW-data=xx ZIdata=xx”
,它说明了程序各个域的大小,编译后,应用程序中所有具有同一性质的数据 (包括代码) 被归到一个域,程序在存储或运行的时候,不同的域会呈现不同的状态,这些域的意义如下:
Code:即代码域,它指的是编译器生成的机器指令,这些内容被存储到 ROM 区。
• RO-data: Read Only data,即只读数据域,它指程序中用到的只读数据,这些数据被存储在ROM
区,因而程序不能修改其内容。例如 C 语言中 const 关键字定义的变量就是典型的RO-data。
• RW-data: Read Write data,即可读写数据域,它指初始化为“非 0 值”的可读写数据,程序刚运行时,这些数据具有非 0 的初始值,且运行的时候它们会常驻在 RAM 区,因而应用程序可以修改其内容。例如 C 语言中使用定义的全局变量,且定义时赋予“非 0值”给该变量进行初始化。
• ZI-data: Zero Initialie data,即 0 初始化数据,它指初始化为“0 值”的可读写数据域,它与RW-data 的区别是程序刚运行时这些数据初始值全都为 0,而后续运行过程与 RW-data 的性质一样,它们也常驻在 RAM 区,因而应用程序可以更改其内容。例如 C 语言中使用定义的全局变量,且定义时赋予“0 值”给该变量进行初始化 (若定义该变量时没有赋予初始值,编译器会把它当 ZI-data 来对待,初始化为 0);
• ZI-data 的栈空间 (Stack) 及堆空间 (Heap):在 C 语言中,函数内部定义的局部变量属于栈空间,进入函数的时候从向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。而使用 malloc 动态分配的变量属于堆空间。在程序中的栈空间和堆空间都是属于ZI-data 区域的,这些空间都会被初始值化为 0 值。编译器给出的 ZI-data 占用的空间值中包含了堆栈的大小 。
详见下图
(二)程序的运行
RW-data 和 ZI-data 它们仅仅是初始值不一样而已,为什么编译器非要把它们区分开?这就涉及 到程序的存储状态了,应用程序具有静止状态和运行状态。静止态的程序被存储在非易失存储器中,如STM32 的内部 FLASH,因而系统掉电后也能正常保存。但是当程序在运行状态的时候,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中(RAM),掉电后这些数据会丢失。因此,程序在静止与运行的时候它在存储器中的表现是不一样的。
详情见下图
图中的左侧是应用程序的存储状态,右侧是运行状态,而上方是 RAM 存储器区域,下方是 ROM存储器区域。
(三)在stm32上运行
在刚刚代码的基础上需要添加串口初始化函数,并将重定向printf()函数,在MDK中勾选Use MicroLIB
查看stm32地址的分配
打印各变量的地址信息
总结
有些时候,我们将一些稍微大一点的工程移植到Flash与RAM较小的开发板中,会出现堆栈溢出等错误;而有些时候,STM32硬件错误HardFault_Handler,也有可能是因为堆栈分配不合理、数组越界等。
掌握好STM32内存分配方式,对我们移植代码或者代码调试有很大的帮助;并且在嵌入式系统中,合理的分配变量,达到一个尽可能小的代码量也是极为重要的。
参考
《STM32库开发实战指南–基于野火指南者开发板》