前言
(1)之前因为一个字符指针和字符数组指针引发的bug,折磨了我一个下午才发现问题。之后我就打算研究一下系统是如何发现野指针乱访问问题。后面就一直深入到微机系统中的内存管理了。
(2)这些其实都是基础知识,只不过我一直不太明白,所以在此记录一下。
(3)阅读本文之前,需要对C语言的指针和动态内存管理有一定的了解!
程序的存储
程序的主要存储模型
(1)一般来说,一个程序主要是由代码段,常量区,静态数据区,栈,堆这五个部分组成。而静态数据区又可以分为BSS段和数据段。
(2)代码段:负责存放程序编译后的二进制代码。有时候存在一些看起来是会存储的数据,实则上不占空间,他们是最终是成为了代码,放在了代码段。如下:
<1>#define 宏定义:在C/C++中,可以使用预处理指令#define定义常量宏。例如:#define PI 3.1415926。在编译时,预处理器会将所有的宏定义进行替换,将宏定义的常量直接嵌入到代码中。
<2>枚举(Enum):在C/C++中,使用枚举可以定义一组具有常量值的符号名称。枚举在代码中实现的功能跟使用宏类似,都可以用名字来代替数值。宏是在预处理阶段将名字换成了值,而枚举是在编译阶段将名字换成值。也就是说,枚举类型的这些名字不是变量,它们不会占据任何内存。且这些名字的有效范围是全局的,如果有变量等命名冲突了可能导致编译不过。
<3>字面常量(Literal Constants):在代码中直接使用的字面常量,例如整数、浮点数、字符常量和字符串常量。这些字面常量在编译时会直接存放在常量区。例如,if(a<5)…这个数字5就是字面常量。
(3)常量区:存放只读变量和字符串常量,一经初始化,不可修改。
<1>const修饰的全局变量:在全局作用域中使用const修饰的变量也会被存放在常量区。如果是被const修饰的局部变量,是被存放在栈上的。
<2>常量字符串:char *str2 = “hello”;中的"hello"就是一个常量字符串。
(3)静态数据区:用于存放全局变量和静态数据(前面加上了static的变量)。
<1>BSS段:用于存储未初始化或显示初始化为0的全局变量和静态变量。
<2>数据段(.data):用于存储已经初始化的全局变量和静态变量。
<3>为什么要给静态数据区分.data段和.bss段?
在程序编译的时候,不会给.bss段中的数据分配空间,只是记录数据所需空间的大小。在程序执行的时候,才会给.bss段中的数据分配内存。通过这种方式,可以节省一部分内存空间,进一步缩减可执行程序的大小。
(4)栈:存放局部变量,函数调用信息等。由系统自动分配/释放。
(5)堆:malloc,alloc函数申请的空间。由程序员手动申请/释放,若不手动释放,程序结束后由系统回收。
通过代码分析
(1)从课本上我们能够知道很多专业术语,所以说,我是不想再谈术语了,我将直接上代码理解。否则这就是水文了。
#include <stdio.h> #include <stdlib.h> #include <string.h> //宏定义和枚举常量会被当成代码编译进代码段,所以说,不占空间 #define PI 3.1415926 enum color { red, green, yellow }; int a = 6; //存放在静态数据区的数据段 char* str1; //存放在静态数据区的BSS段 static int c = 3; //存储在静态数据区的数据段 static int d; //存储在静态数据区的BSS段 const int b = 5; //存放在常量区 int main() //main函数本质上就是一个函数指针,存放在栈中 { static int e; //存储在静态数据区的BSS段 static int f = 2; //存储在静态数据区的数据段 const char g; //存储在栈中 char *str2 = "hello"; //str2存放在栈中,“hello”存放在常量区 char str3[]="world"; //str3存放在栈中 str1 = (char*)malloc(sizeof(char)*strlen(str2)); //申请的内存存放在堆中 strcpy(str1,str2); //进行字符串拷贝 if(a>5); printf("str1 = %s\r\n",str1); printf("str2 = %s\r\n",str2); printf("str3 = %s\r\n",str3); return 0; }
(2)虽然我在上面这串代码写上了注释,说明了这些东西都存放在哪里,但是还是给各位分析一波。我们首先分析定义在函数之外的变量。
<1>#define PI 3.1415926 这个宏定义,在程序预处理阶段,会将所有的PI转换为数字3.1415926 ,而3.1415926 这个数字,会被当成代码,存储在代码段。
<2>enum color{}这个枚举类型,里面的枚举常量red,green, yellow他们和宏定义类似,在程序的编译阶段,他们会被替换成对应的数字,所以说他们是不占空间的!(这个不占空间是相对的,他们是存放在了代码段,而代码段一般不算在我们所说的空间里面)
<3>然后我们来看变量a,他被初始化了成了一个非0的数据,所以存放在静态数据区的数据段。
<4>而str1没有被初始化,所以他被存放在静态数据区的BSS段。
<5>同理,c是在静态数据区的数据段,d是在静态数据区的BSS段。
<6>因为b被const修饰,所以b是被存放在常量区。b是无法被再度修改了。
//宏定义和枚举常量会被当成代码编译进代码段,所以说,不占空间 #define PI 3.1415926 enum color { red, green, yellow }; int a = 6; //存放在静态数据区的数据段 char* str1; //存放在静态数据区的BSS段 static int c = 3; //存储在静态数据区的数据段 static int d; //存储在静态数据区的BSS段 const int b = 5; //存放在常量区
(3)然后我们再分析函数之内的变量。
<1>e和f虽然是被定义在了函数内部,属于局部变量,但是被static修饰,所以他们属于静态局部变量,最终是存储在静态数据区。然后根据有没有被初始化,可以区分他们到底是存储在静态数据区的BSS段,还是数据段。
<2>g虽然被const关键字修饰,但是他不是全局变量,所以依旧是和局部变量一样,保存在栈中,函数结束就会被释放。但是和局部变量的不同之处在于,g这个变量是无法被改变的。
<3>定义在函数内部变量属于局部变量。所以str2和str3是属于局部变量,而且两个数据没有被特殊关键字修饰,因此str2和str3被存放在了栈中。
<4>那么现在有一个问题,“hello”和“world”是存放在了哪里?上面说了,常量区可以存储字符串常量。所以"hello"其实是被被存放在了常量区。str2作为一个指针,可以来访问这个数据,但是不能进行篡改。(后面会讲为什么)而“world”却不一样,他是存放在栈中的。
<5>为什么呢?因为str3[]是一个字符数组,而不是和str2一样的字符指针。str3[]中存放的是数据,str2中存放的是指针。
<6>char *str2 = “hello”;对于计算机而言,就是先在常量区弄一个"hello"字符串,然后把这个字符串的首地址返还给存储在栈中的str2这个字符指针。
<7>而char str3[]=“world”;对于计算机而言,就是在栈中开辟一个字符数组空间存放"world"字符串。他并不是指针!
<8>最后malloc申请的空间,是存放在堆中的。然后存放在静态数据区的BSS段的str字符指针指向这一块堆。
<9>当我们调用strcpy(str1,str2);函数,就会读取存放在常量区的"hello"常量字符串。(注意,这里是读取,不是写入。常量区可读不可写)读取到这个字符串之后,将这个数据复制到malloc在堆申请的空间中。
int main() //main函数本质上就是一个函数指针,存放在栈中 { static int e; //存储在静态数据区的BSS段 static int f = 2; //存储在静态数据区的数据段 const char g; //存储在栈中 char *str2 = "hello"; //str2存放在栈中,“hello”存放在常量区 char str3[]="world"; //str3存放在栈中 str1 = (char*)malloc(sizeof(char)*strlen(str2)); //申请的内存存放在堆中 strcpy(str1,str2); //进行字符串拷贝 if(a>5); printf("str1 = %s\r\n",str1); printf("str2 = %s\r\n",str2); printf("str3 = %s\r\n",str3); return 0; }
中断向量表
(1)在嵌入式系统中,还有一个很重要的概念,中断向量表。
(2)中断向量表是用于存储中断程序的,比如我们在写单片机程序的时候,有串口中断,定时器中断这种程序。这些程序负责存储在中断向量表中。
(3)对于同一款MCU,他的中断向量表大小和位置一般是固定的。他和代码段的存储大小相比,一般都小很多。所以说,这也是为什么经验老道的单片机攻城狮反复强调中断程序要短而精悍的原因之一了。
这些存储区与ROM和RAM的关系
ROM和RAM是什么
(1)很多人学习这一块知识的时候,经常会听到,代码存放在ROM区,代码存放在代码段。malloc申请的数据是存放在RAM,还有一种说法是malloc申请的数据就是堆。想必大家听这些东西,肯定听的云里雾里的。没错,我当时也是一样云里雾里的。
(2)所以说,为了搞清楚这个。我们就可以先搞明白ROM和RAM是什么东西。
<1>ROM,英文全称Read-Only Memory,翻译过来就是只可以读取的存储器。从名字上就能够很清楚的知道ROM是一个只可以读取的存储器,但是我们需要注意的一点是,这个只可以读取是针对于CPU而言的,人们可以通过一些外部手段进行写入数据。ROM的存储特性比较稳定,他断电之后也不会丢失数据。所以说,ROM是一个存储稳定(断电不丢失数据),对CPU而言只可读的存储器。
<2>RAM,英文全称Random Access Memory,翻译过来就是随机存取存储器。从随机两个字就可以知道,这个存储器并不稳定,所以他断电就会丢失数据。存储两个字可以知道,这个存储器CPU是可以对他进行写操作的。所以说,RAM是一个可读可写,但是存储不稳定的存储器。(注意,这个不稳定是指,断电之后数据会丢失,但是如果你通电了,数据是不会出现问题的!)
<3>ROM和RAM不但从存储的稳定性和读写权限上有区别,从数据的读取速度上也有区别。RAM的数据读取速度是比ROM要高的。
ROM与Flash是什么关系
(1)上个世纪,发明了两种存储器,一个是RAM,一个是ROM。当时的ROM一旦存储数据就再也无法将之改变或者删除,也就是说,程序只有一次下载机会。如果你程序写错了,那么这个芯片就报废了。很明显非常的鸡肋。
(2)为了便于使用和大批量生产,进一步发展出了可编程只读存储器(PROM)、可擦除可编程只读存储器(EPROM)。EPROM需要用紫外线长时间照射才能擦除,使用很不方便。
(3)1980s又出现了电可擦除可编程只读存储器(EEPROM),它克服了EPROM的不足,但是集成度不高、价格较贵。
(4)于是又发展出了一种新型的存储单元结构同EPROM类似的快闪存储器(FLASH MEMORY)。FLASH集成度高、功耗低、体积小,又能在线快速擦除,因而获得了快速发展。关系图如下
(5)因此,现在的单片机存储数据主要是使用的Flash,就是替代以前的ROM,最大的有有点是降低了芯片的成本并且可以做到电擦写。但是因为最先发展出来的是ROM,所以很多时候,人们常常愿意说芯片里面的是ROM存储代码,而不是说Flash存储代码。我们需要了解到这个即可,随便你怎么讲。我们只需要知道,现在的芯片都是用Flash存储数据,但是也要能够明白,很多人讲的ROM就是Flash。
ROM,RAM和以上的哪些存储结构体关系
(1)现在我们知道了ROM和RAM是什么之后,那么他们和上面的存储结构有什么联系呢?
<1>中断向量表:我们的中断程序肯定是要存放在ROM中的,为什么呢?假设,中断程序存储在RAM中,会发生什么。你系统一断电,辛辛苦苦写的代码,没了。你想想有多崩溃。
<2>代码段:代码段和中断向量表是一样的存储在ROM区域。但是,可以通过特殊方法让代码段存储在RAM中。为啥要这么做呢?上面说了RAM的数据读取速度要比ROM高,如果让代码段放在RAM中,那样代码的执行效率会有提高。(注意,这个提高是需要一定的数据量,之前我在B站看到一个人测试代码放在ROM和RAM中跑的效果差不多,是因为数据量太少了,效果不明显)
这个时候肯定有细心的同学会问了。不对啊,你代码段放在RAM中,断电所有程序不就没有了吗?对的,这个就是我们后面要讲解的通电和断电状态下的数据存储。我们上面讨论的,都是通电状态下的数据存储!
<3>常量区:存储在ROM,因为常量区的数据不能被修改,而且ROM的比较大,所以正好合适。
<4>静态数据区:存储在RAM,因为这些变量我们会要经常使用到,而且还会进行更改。
<5>栈:存储在RAM,函数的调用和局部变量的申请和释放是都是变化的,而且里面的数据我们会进行调整。所以要放在RAM。
<6>堆:存储在RAM,malloc和alloc申请的空间,是要被使用的,他们会要存储数据,所以要放在RAM中。
断电和通电状态下的数据存储
为什么要分断电和通电状态分析
(1)在一款MCU中,一个程序编译完成之后,需要烧录进去。这个程序有代码段,只读数据,可读可写数据。上文我们分析了,代码段和只读数据存储在ROM,那么可读可写的数据存储在哪里呢?难道是RAM吗?
(2)可能有人认为我自相矛盾,上面都说了,可读可写的数据放在RAM,这里还反问。一塌糊涂!可是,大家认真思考一下,这些可读可写的数据有被赋予初值的变量。假设,这些变量存储在RAM,会发生什么?
(3)很明显,这些被赋予初值的变量存储在RAM,那么一旦断电了,这些初值将会消失!也就是说,我们的这些初值没了!
(4)由此可见,我们数据存储要分成断电和通电两种状态进行分析。而我们上面讲了那么多,其实都是以通电状态进行数据存储的分析的。
断电状态数据存储
(1)从上面的分析我们可以知道,虽然在通电状态下,可读可写数据是要存储在RAM的,但是在断电状态还是要存储在ROM的。所以,现在我就以STM32的断电和通电存储来进行讲解。
(2)我们对一个STM32的工程文件进行编译,会出现如下的编译结果。其中有一行是Program Size: Code=5984 RO-data=336 RW-data=56 ZI-data=1024 。
(3)
<1>Code : 代码段,存放程序的代码部分。
<2>RO-data:只读数据段,存放程序中定义的常量。
<3>RW-data:读写数据段,存放初始化为非 0 值的全局变量。
<4>ZI-data:零初始化的可读写变量,存放未初始化的全局变量及初始化为 0 的变量。
(4)如上为最终我们编写的代码产生的四类数据。但是这四类数据都会存储在STM32的ROM中吗?这个时候我们可以双击工程文件文件——>出现.map文件——>滑动到最后——>看Total ROM Size,这个才是最终下载进入STM32的数据。
(5)细心的朋友会发现,怎么这个Total ROM Size = Code + RO Data + RW Data。ZI-data去哪里?
(6)我们上面在讲解静态数据区的时候,看到静态数据区被分成了BSS段和数据段。为什么需要给静态数据区进行分区,给出的解释是,在程序编译的时候,不会给.bss段中的数据分配空间,只是记录数据所需空间的大小。在程序执行的时候,才会给.bss段中的数据分配内存。
(7)可能又有人要说了,我是怎么记录数据所需空间大小的呢?这个就和编译器有关了,我查阅了一些资料后发现这个与编译器的符号表有关。但是符号表最终不会存储进入芯片。所以,我个人猜测,程序需要访问一个变量,首先需要知道这个变量的首地址,然后访问几个字节,这些都是存储在代码中了因此不需要占空间。(这个看第9段理解为什么说不占空间)
(8)而被初始化的全局变量则不同,我就算知道了访问变量的首地址,和要访问几个字节数据,同时还需要知道他的初始值。所以我们需要将这个初始值存放在ROM中。在程序加载的阶段将这个初始值存入对应的RAM。
(9)经过上面两段对比,我们发现,被初始化的全局变量和没有被初始化的全局变量区别在于,我们是否需要知道他的初始值。而这个变量的首地址,然后访问几个字节都是是要有的,他们都会存入代码段。因此,我们就说,没有被初始化的全局变量不占空间。
(10)因此,我们知道,在断电状态,数据都存储在了ROM中,如下。(注意,这张图没有出现Code,但是我们还是需要知道Code数据存储在ROM中)
通电状态数据存储
(1)通过上面的分析我们知道了断电和通电状态下的数据存储了。那么上电的一瞬间发生了什么?
(2)RO-Data数据不变,RW-Data的数据被复制到RAM区域,RAM中划分出一个ZI-data区域。当程序执行过程中,被初始化为0的数据,CPU会向ZI-data对应的变量中写入0。没有被初始化的数据,那么就是随机值。这也是为什么我们初学C语言的时候,反复强调要进行初始化。但是keil帮我们做了很多事情,如果你变量没有被初始化,他在划分ZI-data区域的时候,会把这个区域全部清零。
(3)RO-Data的数据就是常量区的数据,RW-Data对应静态区的数据段,ZI-data对应静态区的BSS段。那么堆栈呢?
(4)堆栈是由启动文件划分的,如果你感兴趣,可以打开STM32的启动你文件startup_stm32f10x_hd.s。他可以看到他第一部分就是设置堆栈信息的,所以堆栈是由启动文件来划分。
;1-栈 ; Amount of memory (in bytes) allocated for Stack ; Tailor this value to your application needs ; <h> 栈配置,用于变量,函数调用 ; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8> ; </h> Stack_Size EQU 0x00000400 ; 1KB AREA STACK, NOINIT, READWRITE, ALIGN=3 ;告诉汇编器汇编一个新的代码段或者数据段,名字叫做STACK,不初始化,可读可写,以8字节对齐 Stack_Mem SPACE Stack_Size ;对应EQU的那一行,表示分配1KB的空间 __initial_sp ;2-堆 ; <h> 堆配置,用于malloc等函数的动态内存分配 ; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8> ; </h> Heap_Size EQU 0x00000200 ;512B AREA HEAP, NOINIT, READWRITE, ALIGN=3 ;告诉汇编器汇编一个新的代码段或者数据段,名字叫做HEAP,不初始化,可读可写,以8字节对齐 __heap_base ;堆的起始地址 Heap_Mem SPACE Heap_Size ;对应EQU的那一行,表示分配512B的空间 __heap_limit PRESERVE8 ;当前堆栈8字节对齐 THUMB ;兼容 THUMB 指令,老的指令,16bit,现在Cortex-M3的都是THUMB-2指令,兼容16/32位
参考文章
(1)RAM、ROM、FLASH 有什么不同?
https://bbs.elecfans.com/jishu_1404990_1_3.html
(2)STM32 KEIL里的MAP文件分析:
https://blog.csdn.net/qlexcel/article/details/78884379
(3)谈谈内存分配中的(.data)段和(.bss)段的区别?为什么要分.data段和.bss段?
https://blog.csdn.net/Ivan804638781/article/details/110209548
(4)请描述一个可执行程序占用的内存分为哪几个区?每个分区各自的作用?
https://blog.csdn.net/Ivan804638781/article/details/110010286
(5)ROM, FLASH和RAM的区别:
https://zhuanlan.zhihu.com/p/38339306
(6)单片机FLASH与RAM、ROM的关系:
https://zhidao.baidu.com/question/151273888.html
(7)STM32单片机程序的存储和执行: