我自己的原文哦~ https://blog.51cto.com/whaosoft/13777849
一、目标文件xx
每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段(segment):
代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
1 目标文件结构
目标文件是源代码编译但未链接的中间文件(Windows的.obj和Linux的.o),Windows的.obj采用 PE 格式,Linux 采用 ELF 格式,两种格式均是基于通用目标文件格式(COFF,Common Object File Format)变化而来,所以二者大致相同。
目标文件一般包含编译后的机器指令代码、数据、调试信息,还有链接时所需要的一些信息,比如重定位信息和符号表等,而且一般目标文件会将这些不同的信息按照不同的属性,以“节(section)”也叫“段(segment)”的形式进行存储。
#include <stdio.h>
#include <malloc.h>
int gInitVar = 1; // .data 加载阶段加载
int gUninitVar; // .bss 加载阶段加载
const int gConstVar = 2; // .rdata 加载阶段加载
// extern可以修饰const用于扩展文件链接性,const默认是文件内链接的
// static修饰全局变量可以限制其文件链接性,其存储属性不变
void foo(int i) // .text 加载阶段加载
{
static int staticLocalInitVar = 3; // .data 加载阶段加载
static int staticLocalUninitVar; // .bss 加载阶段加载
int stack_localVar = 4; // 栈帧(每个程序运行时会加载1-2M栈空间)
const int LocalConstVar = 5; // 栈帧(运行时自动分配)
staticLocalUninitVar = LocalConstVar + gConstVar;
gUninitVar = staticLocalInitVar + gInitVar;
int *dynamic_heapData = (int*)malloc(sizeof(int)*10000000);
dynamic_heapData[10000000-1] = 9; // 堆区(运行时动态申请)
i += stack_localVar + staticLocalUninitVar + gUninitVar;
printf("%d\n",i+dynamic_heapData[10000000-1]);// .rdata,加载阶段加载"%d\n"
free(dynamic_heapData); // 堆内存需要显式释放
} // 栈内存在超出作用域后自动释放
int main()
{
foo(6);
getchar();
return 0;
} // 加载阶段加载的内存要等到程序结束才释放
文件的内容分割为不同的区块(Setion,又称区段,节等),区段中包含代码数据,各个区块按照页边界来对齐,区块没有限制大小,是一个连续的结构。每块都有他自己在内存中的属性,比如:这个块是否可读可写,或者只读等等。
① .text 代码段
代码段存放程序的机器指令;
② .data 已初始化数据段
初始化数据段存放已初始化的全局变量与局部静态变量;
③ .bss 未初始化数据段
未初始化局部静态变量(或初始化为0的局部静态变量)放到.bss段,对于未初始化全局变量(或初始化为0的全局变量),不同语言与编译器的实现有不同的处理,有的只是在.baa段预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在 .bss 段分配空间。编译器会把未初始化的全局变量标记为一个 COMMON 符号,不为其在 .bss 段分配空间的原因是现在的编译器和链接器支持弱符号机制,即允许同一个弱符号定义在多个目标文件中,因为未初始化的全局变量属于弱符号,编译时无法确定符号大小,所以此时无法在 .bss 段为未初始化的全局变量分配空间。
④ .rdata或.rodata 只读数据段
只读数据段存放程序中只读变量,如const修饰的常量和字符串常量;
单独设立.rodata段的好处有很多,比如语义上支持了C的const常量,而且操作系统在加载的时候可以将.rodata段的内容映射为只读区,这样对于这个段的任何修改都会被判为非法,保证了程序的安全性。
⑤ .symtab 符号表段
.symtab段用于存符号表。每个目标文件都有一个相应的符号表(Symbol Table),记录了目标文件中用到的所有符号。每个符号都有一个对应的值,叫做符号值(Symbol Value),符号值可以是符号所对应的数据在段中的偏移量,也可以是该符号的对齐属性。
链接过程的本质就是要把多个不同的目标文件之间像拼图一样拼起来,相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。比如目标文件B用到了目标文件A中的函数foo,那么称目标文件A定义了函数foo,目标文件B引用了函数foo。定义与引用这两个概念同样适用于变量。每个函数和变量都有自己独一的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号(Symbol),函数或变量名就是符号名(Symbol Name)。
符号是链接的粘合剂,没有符号就无法完成链接。每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。
除了函数和变量之外,还存在其它几种不常用到的符号。符号表中的符号可分为全局符号、局部符号、段名、行号等,对于链接而言,只关心全局符号。
⑥ .strtab 字符串表
因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串(在汇编中使用offset)。
7: char * stringLiteral = "stringLiteral";
00401038 mov dword ptr [ebp-4],offset string "stringLiteral" (004230c0)
⑦ .rela.text 代码段重定位表
重定位表,也叫作重定位段,用于链接器在处理目标文件时,重定位代码段中那些对绝对地址的引用的位置。比如 .text 段中对外部 printf() 函数的调用。每个要被重定位的地方叫重定位入口(Relocation Entry),OFFSET 表示该入口在所在段中的偏移位置,TYPE 表示重定位入口的类型,VALUE 表示重定位入口的符号名称。
2 加载与执行
项目全部相关文件最终会由链接器链接到一起形成一个可执行文件,Linux 系统中的每个可执行文件都运行在一个进程上下文中,有自己的虚拟地址空间。当shell 运行一个程序时,父shell 进程生成一个子进程,它是父进程的一个复制。子进程通过系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。
一个系统中的进程是与其他进程共享CPU和主存资源的。然而,共享主存会形成一些特殊的挑战。如果太多的进程需要太多的内存,那么它们中的一些就根本无法运行。当一个程序没有空间可用时,那就是它运气不好了。内存还很容易被破坏。如果某个进程不小心写了另一个进程使用的内存,它就可能以某种完全和程序逻辑无关的令人迷惑的方式失败。
为了更加有效地管理内存并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存(VM)。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟内存提供了三个重要的能力:
1) 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
2) 它为每个进程提供了一致的地址空间,从而简化了内存管理。
3) 它保护了每个进程的地址空间不被其他进程破坏。
虚拟内存是计算机系统最重要的概念之一。它成功的一个主要原因就是因为它是沉默地、自动地工作的,不需要应用程序员的任何干涉。
3 汇编代码分析
看以下源代码与汇编代码的对应,以及数据(变量)对应的地址值:
1: #include <stdio.h>
2: #include <malloc.h>
3:
4: int gInitVar = 1; // .data 加载阶段加载,所以这里无汇编对应
5: int gUninitVar; // .bss 加载阶段加载
6: const int gConstVar = 2; // .rdata 加载阶段加载
7:
8: void foo(int i) // .text 加载阶段加载
9: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,4Ch
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-4Ch]
0040102C mov ecx,13h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
10: static int staticLocalInitVar = 3; // .data 加载阶段加载,所以这里无汇编对应
11: static int staticLocalUninitVar; // .bss 加载阶段加载
12: int stack_localVar = 4; // 栈帧(每个程序运行时会加载若干M栈空间)
00401038 mov dword ptr [ebp-4],4 // 局部变量保存在栈上,由ebp及其偏移表示
13: const int LocalConstVar = 5; // 栈帧(运行时自动分配)
0040103F mov dword ptr [ebp-8],5 // 局部常量放在栈区
14: staticLocalUninitVar = LocalConstVar + gConstVar;
00401046 mov dword ptr [gUninitVar+4 (00428e40)],7
15: gUninitVar = staticLocalInitVar + gInitVar;
00401050 mov eax,[global_data+4 (00425a34)]
00401055 add eax,dword ptr [gInitVar (00425a30)]
0040105B mov [gUninitVar (00428e3c)],eax
16: int *dynamic_heapData = (int*)malloc(sizeof(int)*10000000);
00401060 push 2625A00h
00401065 call malloc (00401190)
0040106A add esp,4
0040106D mov dword ptr [ebp-0Ch],eax
17: dynamic_heapData[10000000-1] = 9; // 堆区(运行时动态申请)
00401070 mov ecx,dword ptr [ebp-0Ch]
00401073 mov dword ptr [ecx+26259FCh],9
18: i += stack_localVar + staticLocalUninitVar + gUninitVar;
0040107D mov edx,dword ptr [ebp-4]
00401080 add edx,dword ptr [gUninitVar+4 (00428e40)]
00401086 add edx,dword ptr [gUninitVar (00428e3c)]
0040108C mov eax,dword ptr [ebp+8]
0040108F add eax,edx
00401091 mov dword ptr [ebp+8],eax
19: printf("%d\n",i+dynamic_heapData[10000000-1]);// .rdata,加载阶段加载"%d\n"
00401094 mov ecx,dword ptr [ebp-0Ch]
00401097 mov edx,dword ptr [ebp+8]
0040109A add edx,dword ptr [ecx+26259FCh]
004010A0 push edx
004010A1 push offset string "\xd4\xcb\xd0\xd0\xbd\xd7\xb6\xce\xa3\xba\xb6\xaf\xcc\xac\xc9\xea\xc7\xeb\
004010A6 call printf (00403110)
004010AB add esp,8
20: free(dynamic_heapData); // 堆内存需要显式释放
004010AE mov eax,dword ptr [ebp-0Ch]
004010B1 push eax
004010B2 call free (00401c10)
004010B7 add esp,4
21: } // 栈内存在超出作用域后自动释放
x
二、常见的与内存相关的错误
对C语言入门程序员来说,管理和使用虚拟存储器可能是个困难的,容易出错的任务。与存储器有关的错误属于那些最令人惊恐的错误,因为它们经常在时间和空间上,都在距错误源一段距离之后,才表现出来。将错误的数据编写到错误的位置,你的程序可能在最终失败之前运行了好几个小时,且使程序中止的位置距离错误的位置已经很远了。
1、间接引用坏指针
在进程的虚拟地址空间中有较大的漏洞,没有映射到任何有意义的数据。如果我们试图间接引用一个指向这些洞的指针,那么操作系统就会以段异常终止我们的程序。而且,虚拟存储器的某些区域是只读的。试图写这些区域将造成以保护异常终止这个程序。
间接引用坏指针的一个常见示例是经典的scanf错误。假设我们想要使用scanf从stdin读一个整数到变量。做这件事情正确的方法是传递给scanf一个格式串和变量的地址:
然而,对于c语言程序员初学者而言,很容易传递val的内容,而不是它的地址:
在这种情况下,scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即以异常终止。在最糟糕的情况下,val的内容对应于虚拟存储器的某个合法的读/写区域,于是我们就覆盖了存储器,这通常会在相当以后造成灾难性的、令人困惑的后果。
2、读未初始化的存储器
虽然.bss存储器位置(诸如未初始化的全局C变量)总是被加载器初始化为零,但是对于堆存储器却并不是这样的。一个常见的错误就是假设堆存储器被初始化为零:
在这个示例中,程序员不正确地假设向量y被初始化为零。正确的实现方式是在for循环时将y[i]设置为零,或使用calloc。
3、允许栈缓冲区溢出
如果一个程序不检查输入串的大小就写入栈中的目标换成区,那么这个程序就会有缓冲区溢出错误。例如,下面的函数就有缓冲区错误,因为gets函数拷贝一个任意长度的串到缓冲区。为了纠正这个错误,我们必须使用fgets函数,这个函数限制了输入串的大小:
4、假设指针和它们指向的对象是相同大小的
一种常见的错误是假设指向对象的指针和它们所指向的对象是相同大小的:
这里的目的是创建一个由n个指针组成的数组,每个指针都指向一个包含m个int的数组。然而,因为程序员将int **A = (int **)malloc(n * sizeof(int));中将sizeof(int)写成了sizeof(int),代码实际创建的是一个int的数组。这段代码只有在int和指向int的指针大小相同的机器上运行良好。
但是,如果我们在像Alpha这样的机器上运行这段代码,其中指针大于int,那么在for(i = 0; i < n; i++) A[i] = (int *)malloc(m * sizeof(int));将写到超过A数组末端的地方。因为这些字中的一个很可能是分配块的边界标记脚部,所以我们可能不会发现这个错误,而没有任何明显的原因。
5、造成错位错位
错位错误是另一种很常见的覆盖错误发生的原因:
这是前面程序的另一个版本。这里我们创建了一个n个元素的指针数组,但是随后试图初始化这个数组的n+1个元素,在这个过程中覆盖了A数组后面的某个存储器。
6、引用指针,而不是它所指向的对象
如果我们不太注意C操作符的优先级和结合性,我们就会错误地操作指针,而不是期望操作指针所指向的对象。比如,考虑下面的函数,其目的是删除一个有*size项的二叉堆里的第一项,然后对剩下的*size-1项重新建堆。
*size—目的是减少size指针指向的整数的值。然而,因为一元—和*运算符优先级相同,从右向左结合,所以代码实际减少的是指针自己的值,而不是它所指向的整数的值。如果幸运的话,程序会立即失败,但是更有可能发生的是,当程序在它执行过程的很后面产生一个不正确的结果时,我们只能在那里抓脑袋了。这里的原则是如果你对优先级和结合性有疑问,就使用括号。使用表达式(*size)--。
7、误解指针运算
另一种常见的错误是忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的,而这种大小单位并不一定是字节。例如,下面函数的目的是扫描一个int的数组,并返回一个指针,指向val的首次出现:
8、引用不存在的变量
没有太多经验的C程序员不理解栈的规则,有时会引用不再合法的本地变量,如下列所示:
这个函数返回一个指针,指向栈里的一个局部变量,然后弹出它的栈帧。尽管p仍然指向一个合法的存储器地址,但是它已经不再指向一个合法的变量了。当以后在程序中调用其他函数时,存储器将重用它们的帧栈。后来,如果程序分配某个值给*p,那么它可能实际正在修改另一个函数的帧栈中的一个条目,从而带来潜在地灾难性的、令人困惑的后果。
9、引用空闲堆块中的数据
一个相似的错误是引用已被释放了的堆块中的数据。如下面的示例,示例中分配了一个整数数组x,之后释放了块x,最后又引用了它。
10、引起存储器泄漏
存储器泄漏是缓慢、隐形的杀手,当程序员不小心忘记释放已分配块,而在堆里创建了垃圾时,会发生这种问题。例如,下面的函数分配了一个堆块x,然后不释放它就返回。
如果leak经常被调用,堆里就会充满了垃圾,最糟糕的情况下,会占有整个虚拟地址空间。对于像守护进程和服务器这样的程序来说,存储器泄漏是特别严重的,根据定义这些程序是不会终止的。
三、防止内存泄漏
内存泄漏问题原理
堆内存在C代码中的存储方式
内存泄漏问题只有在使用堆内存的时候才会出现,栈内存不存在内存泄漏问题,因为栈内存会自动分配和释放。C代码中堆内存的申请函数是malloc,常见的内存申请代码如下:
由于malloc函数返回的实际上是一个内存地址,所以保存堆内存的变量一定是一个指针(除非代码编写极其不规范)。再重复一遍,保存堆内存的变量一定是一个指针,这对本文主旨的理解很重要。当然,这个指针可以是单指针,也可以是多重指针。
malloc函数有很多变种或封装,如g_malloc、g_malloc0、VOS_Malloc等,这些函数最终都会调用malloc函数。
堆内存的获取方法
看到本小节标题,可能有些同学有疑惑,上一小节中的malloc函数,不就是堆内存的获取方法吗?的确是,通过malloc函数申请是最直接的获取方法,如果只知道这种堆内存获取方法,就容易掉到坑里了。一般的来讲,堆内存有如下两种获取方法:
方法一:
将函数返回值直接赋给指针,一般表现形式如下:
该类涉及到内存申请的函数,返回值一般都指针类型,例如:
方法二:
将指针地址作为函数返回参数,通过返回参数保存堆内存地址,一般表现形式如下:
内存泄漏三要素
最常见的内存泄漏问题,包含以下三个要素:
要素一:
函数内有局部指针变量定义;
要素二:
对该局部指针有通过上一小节中“两种堆内存获取方法”之一获取内存;
要素三:
在函数返回前(含正常分支和异常分支)未释放该内存,也未保存到其它全局变量或返回给上一级函数。
内存释放误区
稍微使用过C语言编写代码的人,都应该知道堆内存申请之后是需要释放的。但为何还这么容易出现内存泄漏问题呢?一方面,是开发人员经验不足、意识不到位或一时疏忽导致;另一方面,是内存释放误区导致。很多开发人员,认为要释放的内存应该局限于以下两种:
1) 直接使用内存申请函数申请出来的内存,如malloc、g_malloc等;
2)该开发人员熟悉的接口中,存在内存申请的情况,如iBMC的兄弟,都应该知道调用如下接口需要释放list指向的内存:
按照以上思维编写代码,一旦遇到不熟悉的接口中需要释放内存的问题,就完全没有释放内存的意识,内存泄漏问题就自然产生了。
内存泄漏问题检视方法
检视内存泄漏问题,关键还是要养成良好的编码检视习惯。与内存泄漏三要素对应,需
要做到如下三点:
1)在函数中看到有局部指针,就要警惕内存泄漏问题,养成进一步排查的习惯
2)分析对局部指针的赋值操作,是否属于前面所说的“两种堆内存获取方法”之一,如果是,就要分析函数返回的指针到底指向啥?是全局数据、静态数据还是堆内存?对于不熟悉的接口,要找到对应的接口文档或源代码分析;又或者看看代码中其它地方对该接口的引用,是否进行了内存释放;
3)如果确认对局部指针存在内存申请操作,就需要分析该内存的去向,是会被保存在全局变量吗?又或者会被作为函数返回值吗?如果都不是,就需要排查函数所有有”return“的地方,保证内存被正确释放。
四、内存四区
1 数据类型本质分析
●“类型”是对数据的抽象
●类型相同的数据有相同的表示形式、存储格式以及相关的操作
●程序中使用的所有数据都必定属于某一种数据类型
数据类型的本质:
●数据类型可理解为创建变量的模具:是固定内存大小的别名。
●数据类型的作用:编译器预算对象(变量)分配的内存空间大小。
●注意:数据类型只是模具,编译器并没有分酤空间,只有根据类型(模具)
创建变量(实物),编译器才会分配空间。
2 变量的本质分析
变量的概念:
既能读又能写的内存对象,称为变量;若一旦初始化后不能修改的对象则称为常量。
变量定义形式:
类型标识符,标识符,…,标识符;
变量的本质:
- 程序通过变量来申请和命名内存空间int a = 0。
- 通过变量名访问内存空间。
3 程序的内存四区模型
流程说明:
- 操作系统把物理硬盘代码load到内存
- 操作系统把c代码分成四个区
- 操作系统找到main函数入口执行。
4 函数调用模型
5 函数调用变量传递分析
6 栈的生长方向和内存存放方向
7 相关代码
02_数据类型本质.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
int main()
{
int a;//告诉编译器,分配4个字节
int b[10];//告诉编译器,分配4*10个字节
/*
类型本质:固定内存块大小别名
可以通过sizeof()测试
*/
printf("sizeof(a)=%d,sizeof(b)=%d\n", sizeof(a), sizeof(b));
//打印地址
//数组名称,数组首元素地址,数组首地址
printf("b:%d,&b:%d\n",b,&b);//地址相同
//b,&b数组类型不同
//b,数组首地址元素 一个元素4字节,+1 地址+4
//&b,整个数组首地址 一个数组4*10=40字节, +1 地址+40
printf("b+1:%d,&b+1:%d\n", b + 1, &b + 1);//不同
//指针类型长度,32位机器32位系统下长度是 4字节
// 64 64 8
char********* p = NULL;
int* q = NULL;
printf("%d,%d\n", sizeof(p), sizeof(q));//4 , 4
return 0;
}
03_给类型起别名.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
typedef unsigned int u32;
//typedef 和结构体结合使用
struct Mystruct
{
int a;
int b;
};
typedef struct Mystruct2
{
int a;
int b;
}TMP;
/*
void 无类型
1.函数参数为空,定义函数时用void修饰 int fun(void)
2.函数没有返回值:使用void void fun (void)
3.不能定义void类型的普通变量:void a;//err 无法确定是什么类型
4.可以定义 void* 变量 void* p;//ok 32位系统下永远是4字节
5.数据类型本质:固定内存块大小别名
6.void *p万能指针,函数返回值,函数参数
*/
int main()
{
u32 t;//unsigned int
//定义结构体变量,一定要加上struct 关键字
struct Mystruct m1;
//Mystruct m2;//err
TMP m3;//typedef配合结构体使用
struct Mystruct2 m4;
printf("\n");
return 0;
}
04_变量的赋值.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
int main()
{
//变量本质:一段连续内存空间别名
//变量相当于门牌号,内存相当于房间
int a;
int* p;
//直接赋值
a = 10;
printf("a=%d\n", a);
//间接赋值
printf("&a:%d\n", &a);
p = &a;
printf("p=%d\n", p);
*p = 22;
printf("*p=%d,a=%d\n", *p, a);
return 0;
}
05_全局区分析.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
int main()
{
//变量本质:一段连续内存空间别名
//变量相当于门牌号,内存相当于房间
int a;
int* p;
//直接赋值
a = 10;
printf("a=%d\n", a);
//间接赋值
printf("&a:%d\n", &a);
p = &a;
printf("p=%d\n", p);
*p = 22;
printf("*p=%d,a=%d\n", *p, a);
return 0;
}
06_堆栈区分析.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
char* get_str()
{
char str[] = "abcdef";//内容分配在栈区,函数运行完毕后内存释放
printf("%s\n", str);
return str;
}
char* get_str2()
{
char* temp = (char*)malloc(100);
if (temp == NULL)
{
return NULL;
}
strcpy(temp, "abcdefg");
return temp;
}
int main()
{
char buf[128] = { 0 };
//strcpy(buf,get_str());
//printf("buf = %s\n", buf);//乱码,不确定内容
char* p = NULL;
p = get_str2();
if (p != NULL)
{
printf("p=%s\n", p);
free(p);
p = NULL;
}
return 0;
}
07_静态局部变量.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
int* getA()
{
static int a = 10;//在静态区,静态区在全局区
return &a;
}
int main()
{
int* p = getA();
*p = 5;
printf("%d\n",);
return 0;
}
08_栈的生长方向.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
int* getA()
{
static int a = 10;//在静态区,静态区在全局区
return &a;
}
int main()
{
int* p = getA();
*p = 5;
printf("%d\n",);
return 0;
}
五、C/C++内存管理
C语言内存管理指对系统内存的分配、创建、使用这一系列操作。
在计算机中,每个应用程序之间的内存是相互独立的,通常情况下应用程序 A 并不能访问应用程序 B,当然一些特殊技巧可以访问,但此文并不详细进行说明。例如在计算机中,一个视频播放程序与一个浏览器程序,它们的内存并不能访问,每个程序所拥有的内存是分区进行管理的。
在计算机系统中,运行程序 A 将会在内存中开辟程序 A 的内存区域 1,运行程序 B 将会在内存中开辟程序 B 的内存区域 2,内存区域 1 与内存区域 2 之间逻辑分隔。
在程序 A 开辟的内存区域 1 会被分为几个区域,这就是内存四区,内存四区分为栈区、堆区、数据区与代码区。
栈区指的是存储一些临时变量的区域,临时变量包括了局部变量、返回值、参数、返回地址等,当这些变量超出了当前作用域时将会自动弹出。该栈的最大存储是有大小的,该值固定,超过该大小将会造成栈溢出。
堆区指的是一个比较大的内存空间,主要用于对动态内存的分配;在程序开发中一般是开发人员进行分配与释放,若在程序结束时都未释放,系统将会自动进行回收。
数据区指的是主要存放全局变量、常量和静态变量的区域,数据区又可以进行划分,分为全局区与静态区。全局变量与静态变量将会存放至该区域。
代码区就比较好理解了,主要是存储可执行代码,该区域的属性是只读的。
使用代码证实内存四区的底层结构
由于栈区与堆区的底层结构比较直观的表现,在此使用代码只演示这两个概念。首先查看代码观察栈区的内存地址分配情况:
运行结果为:
我们可以观察到变量 a 的地址是 2293324 变量 b 的地址是 2293320,由于 int 的数据大小为 4 所以两者之间间隔为 4;再查看变量 c,我们发现变量 c 的地址为 2293319,与变量 b 的地址 2293324 间隔 1,因为 c 的数据类型为 char,类型大小为 1。在此我们观察发现,明明我创建变量的时候顺序是 a 到 b 再到 c,为什么它们之间的地址不是增加而是减少呢?那是因为栈区的一种数据存储结构为先进后出,如图:
首先栈的顶部为地址的“最小”索引,随后往下依次增大,但是由于堆栈的特殊存储结构,我们将变量 a 先进行存储,那么它的一个索引地址将会是最大的,随后依次减少;第二次存储的值是 b,该值的地址索引比 a 小,由于 int 的数据大小为 4,所以在 a 地址为 2293324 的基础上往上减少 4 为 2293320,在存储 c 的时候为 char,大小为 1,则地址为 2293319。由于 a、b、c 三个变量同属于一个栈内,所以它们地址的索引是连续性的。
x
六、C语言malloc申请内存时的碎片问题
解决问题:malloc在申请内存的时候,内存碎片问题会导致原本内存大小足够,却申请大内存失败;
比如:原本内存还有10M内存,此时先申请4M内存,再申请16Bytes内存,之后把4M内存释放掉,按理来说,此时应该还有 10M - 16Bytes 内存,但此时,再去申请8M的大内存,则申请失败。
因为malloc申请的内存,必须是一块连续的内存,但此时中间已经有16Bytes内存碎片导致内存不连续,所以申请内存失败;
以下是我针对碎片问题,对内存管理机制做出一种优化方案:在开机初始化内存之后,先申请一块1M左右内存(根据情况修改大小),用作内存碎片管理,然后把这1M内存分为很多个小内存,并把小内存的地址放在链接节点中,之后申请内存时,优先判断内存碎片管理中是否有满足大小的小内存。
有的话,直接使用提前申请的小内存就可以了,如果内存管理机制中没有适合的内存,但重新用malloc()函数申请;
接下来,解释我写的碎片管理机制:
1 mm_management_init()初始化函数
void mm_management_init(unsigned int free_memory_start, unsigned int free_memory_end)
传入参数free_memory_start是内存初始化之后,剩余可申请的首地址,该地址,一般会传入到main函数,如果main()函数没有传入该参数的话,可以在内存初始化之后,自己malloc(4)申请一下,把返回的地址作为mm_management_init()函数的第一个参数;
传入参数free_memory_end是可以申请的最大地址,每个IC各有不同;
mm_management_init()对16bytes,64bytes,256bytes,512bytes,1024bytes,4096bytes这些小内存做优化,提前计算小内存占用的总大小。
然后直接申请这块大内存占住,再把这块大内存分配给各个小内存,并记录在链表中,比如:mm_fix_16_head
2 mm_management_malloc()申请函数
unsigned int mm_management_malloc(unsigned int size)
申请内存的时候,先判断size大小,如果大小可以在内存管理机制中找到,则直接返回提前申请地址,如果大小不满足,或者小内存已被申请完,则用malloc重新申请。
在内存管理机制中拿到的小内存,该链表节点的标记会设为MM_STATUS_BUSY。
3 mm_management_free()
void mm_management_free(void *mm_ptr)
与mm_management_malloc()相反,先检查所有小内存链表是都有该地址,有的话就把该地址内存清0,并把标记设为MM_STATUS_FREE;如果是用malloc申请的,当时是free()释放掉;
接下来是代码:
#include<stdio.h>
#include<malloc.h>
#define C_MM_16BYTE_NUM (32)
#define C_MM_64BYTE_NUM (16)
#define C_MM_256BYTE_NUM (12)
#define C_MM_512BYTE_NUM (12)
#define C_MM_1024BYTE_NUM (18)
#define C_MM_4096BYTE_NUM (30)
#define C_MM_16BYTE (16)
#define C_MM_64BYTE (64)
#define C_MM_256BYTE (256)
#define C_MM_512BYTE (512)
#define C_MM_1024BYTE (1024)
#define C_MM_4096BYTE (4096)
#define C_MM_MAX_SIZE C_MM_4096BYTE //碎片管理最大的碎片大小
#define MM_STATUS_FREE (0) //0:表示内存空闲
#define MM_STATUS_BUSY (1) //1:表示内存已被申请
#define MM_STATUS_OK (0)
#define MM_STATUS_FAIL (1)
typedef struct mm_node_struct {
unsigned int *mm_node; //存放内存节点指针
unsigned short iflag; //指针是否空闲
struct P_MM_Node_STRUCT *next; //指向下一个内存节点指针
} MM_Node_STRUCT, *P_MM_Node_STRUCT;
typedef struct mm_sdram_struct {
unsigned int count;
P_MM_Node_STRUCT *next;
} MM_SDRAM_STRUCT, *P_MM_SDRAM_STRUCT;
static MM_SDRAM_STRUCT mm_fix_16_head;
static MM_SDRAM_STRUCT mm_fix_64_head;
static MM_SDRAM_STRUCT mm_fix_256_head;
static MM_SDRAM_STRUCT mm_fix_512_head;
static MM_SDRAM_STRUCT mm_fix_1024_head;
static MM_SDRAM_STRUCT mm_fix_4096_head;
static P_MM_SDRAM_STRUCT pmm_fix_16_head = &mm_fix_16_head;
static P_MM_SDRAM_STRUCT pmm_fix_64_head = &mm_fix_64_head;
static P_MM_SDRAM_STRUCT pmm_fix_256_head = &mm_fix_256_head;
static P_MM_SDRAM_STRUCT pmm_fix_512_head = &mm_fix_512_head;
static P_MM_SDRAM_STRUCT pmm_fix_1024_head = &mm_fix_1024_head;
static P_MM_SDRAM_STRUCT pmm_fix_4096_head = &mm_fix_4096_head;
static P_MM_Node_STRUCT mm_management_getnode(P_MM_SDRAM_STRUCT pmm_fix_head);
static unsigned int mm_management_node_free(P_MM_SDRAM_STRUCT pmm_fix_head, unsigned int *mm_ptr, unsigned int size);
static unsigned int *mm_management_ptr = NULL;
static unsigned int mm_management_size = 0;
/*
** free_memory_start : 开机内存初始化之后,剩余可以申请的地址的首地址
** free_memory_end : 内存可以申请的最大地址
*/
void mm_management_init(unsigned int free_memory_start, unsigned int free_memory_end)
{
unsigned int mm_usesize=0,offset=0,mm_offset;
unsigned char *ptr_tmp;
unsigned int i;
P_MM_Node_STRUCT pmm_fix_head, pmm_fix_tmp;
free_memory_start = (free_memory_start + 3) & (~0x3); // Align to 4-bytes boundary
free_memory_end = (free_memory_end + 3) & (~0x3); // Align to 4-bytes boundary
do{
//[1]判断剩余内存是否满足碎片管理所需大小
mm_usesize = 0;
mm_usesize += C_MM_16BYTE * C_MM_16BYTE_NUM;
mm_usesize += C_MM_64BYTE * C_MM_64BYTE_NUM;
mm_usesize += C_MM_256BYTE * C_MM_256BYTE_NUM;
mm_usesize += C_MM_512BYTE * C_MM_512BYTE_NUM;
mm_usesize += C_MM_1024BYTE * C_MM_1024BYTE_NUM;
mm_usesize += C_MM_4096BYTE * C_MM_4096BYTE_NUM;
if(mm_usesize+free_memory_start > free_memory_end)
{
printf("free memory not enough for mm management,init fail\r\n");
break;
}
mm_management_ptr = (unsigned char *)malloc(mm_usesize); //申请整块碎片管理内存大小 //如果有malloc_align函数,建议改用malloc_align申请64bit对其的内存
if(mm_management_ptr == NULL)
{
printf("mm management malloc fail,init fail\r\n");
break;
}
mm_management_size = mm_usesize;
ptr_tmp = mm_management_ptr;
memset(ptr_tmp, 0x00, mm_usesize);
//[2]内存链表头初始化,用于存放以下步骤的子链表节点
memset((void*)pmm_fix_16_head, 0x00, sizeof(mm_fix_16_head));
memset((void*)pmm_fix_64_head, 0x00, sizeof(mm_fix_64_head));
memset((void*)pmm_fix_256_head, 0x00, sizeof(mm_fix_256_head));
memset((void*)pmm_fix_512_head, 0x00, sizeof(mm_fix_512_head));
memset((void*)pmm_fix_1024_head, 0x00, sizeof(mm_fix_1024_head));
memset((void*)pmm_fix_4096_head, 0x00, sizeof(mm_fix_4096_head));
//[3]申请16Bytes碎片内存存放在链表
mm_offset = 0;
mm_fix_16_head.count = C_MM_16BYTE_NUM;
pmm_fix_head = pmm_fix_16_head;
for(i=0; i<C_MM_16BYTE_NUM; i++)
{
pmm_fix_tmp = (P_MM_Node_STRUCT)malloc(sizeof(MM_Node_STRUCT));
pmm_fix_tmp->iflag = MM_STATUS_FREE;
pmm_fix_tmp->next = NULL;
offset = (C_MM_16BYTE * i) + mm_offset; //计算小内存碎片在大buf里的偏移地址
pmm_fix_tmp->mm_node = ptr_tmp + offset;
pmm_fix_head->next = pmm_fix_tmp;
pmm_fix_head = pmm_fix_tmp;
}
//[4]申请64Bytes碎片内存存放在链表
mm_offset += C_MM_16BYTE * C_MM_16BYTE_NUM;
mm_fix_64_head.count = C_MM_64BYTE_NUM;
pmm_fix_head = pmm_fix_64_head;
for(i=0; i<C_MM_64BYTE_NUM; i++)
{
pmm_fix_tmp = (P_MM_Node_STRUCT)malloc(sizeof(MM_Node_STRUCT));
pmm_fix_tmp->iflag = MM_STATUS_FREE;
pmm_fix_tmp->next = NULL;
offset = (C_MM_64BYTE * i) + mm_offset; //计算小内存碎片在大buf里的偏移地址
pmm_fix_tmp->mm_node = ptr_tmp + offset;
pmm_fix_head->next = pmm_fix_tmp;
pmm_fix_head = pmm_fix_tmp;
}
//[5]申请256Bytes碎片内存存放在链表
mm_offset += C_MM_64BYTE * C_MM_64BYTE_NUM;
mm_fix_256_head.count = C_MM_256BYTE_NUM;
pmm_fix_head = pmm_fix_256_head;
for(i=0; i<C_MM_256BYTE_NUM; i++)
{
pmm_fix_tmp = (P_MM_Node_STRUCT)malloc(sizeof(MM_Node_STRUCT));
pmm_fix_tmp->iflag = MM_STATUS_FREE;
pmm_fix_tmp->next = NULL;
offset = (C_MM_256BYTE * i) + mm_offset; //计算小内存碎片在大buf里的偏移地址
pmm_fix_tmp->mm_node = ptr_tmp + offset;
pmm_fix_head->next = pmm_fix_tmp;
pmm_fix_head = pmm_fix_tmp;
}
//[6]申请512Bytes碎片内存存放在链表
mm_offset += C_MM_256BYTE * C_MM_256BYTE_NUM;
mm_fix_512_head.count = C_MM_512BYTE_NUM;
pmm_fix_head = pmm_fix_512_head;
for(i=0; i<C_MM_512BYTE_NUM; i++)
{
pmm_fix_tmp = (P_MM_Node_STRUCT)malloc(sizeof(MM_Node_STRUCT));
pmm_fix_tmp->iflag = MM_STATUS_FREE;
pmm_fix_tmp->next = NULL;
offset = (C_MM_512BYTE * i) + mm_offset; //计算小内存碎片在大buf里的偏移地址
pmm_fix_tmp->mm_node = ptr_tmp + offset;
pmm_fix_head->next = pmm_fix_tmp;
pmm_fix_head = pmm_fix_tmp;
}
//[7]申请1024Bytes碎片内存存放在链表
mm_offset += C_MM_512BYTE * C_MM_512BYTE_NUM;
mm_fix_1024_head.count = C_MM_1024BYTE_NUM;
pmm_fix_head = pmm_fix_1024_head;
for(i=0; i<C_MM_1024BYTE_NUM; i++)
{
pmm_fix_tmp = (P_MM_Node_STRUCT)malloc(sizeof(MM_Node_STRUCT));
pmm_fix_tmp->iflag = MM_STATUS_FREE;
pmm_fix_tmp->next = NULL;
offset = (C_MM_1024BYTE * i) + mm_offset; //计算小内存碎片在大buf里的偏移地址
pmm_fix_tmp->mm_node = ptr_tmp + offset;
pmm_fix_head->next = pmm_fix_tmp;
pmm_fix_head = pmm_fix_tmp;
}
//[8]申请4096Bytes碎片内存存放在链表
mm_offset += C_MM_1024BYTE * C_MM_1024BYTE_NUM;
mm_fix_4096_head.count = C_MM_4096BYTE_NUM;
pmm_fix_head = pmm_fix_4096_head;
for(i=0; i<C_MM_4096BYTE_NUM; i++)
{
pmm_fix_tmp = (P_MM_Node_STRUCT)malloc(sizeof(MM_Node_STRUCT));
pmm_fix_tmp->iflag = MM_STATUS_FREE;
pmm_fix_tmp->next = NULL;
offset = (C_MM_4096BYTE * i) + mm_offset; //计算小内存碎片在大buf里的偏移地址
pmm_fix_tmp->mm_node = ptr_tmp + offset;
pmm_fix_head->next = pmm_fix_tmp;
pmm_fix_head = pmm_fix_tmp;
}
}while(0);
printf("mm management init end!!!\r\n");
}
unsigned int mm_management_malloc(unsigned int size)
{
int status = MM_STATUS_FAIL; //MM_STATUS_FAIL表示还没申请到碎片内存
P_MM_Node_STRUCT pmm_fix_node;
unsigned int *mm_ptr = NULL;
//获取空闲碎片节点
do{
//[1]判断申请内存大小是否满足要求
if(size < 0)
{
status = MM_STATUS_FAIL;
printf("mm management malloc size is error\r\n");
return NULL;
}
//[2]判断大小是否小于16Byets
if(size < C_MM_16BYTE && status == MM_STATUS_FAIL)
{
pmm_fix_node = mm_management_getnode(pmm_fix_16_head);
if(pmm_fix_node != NULL)
{
status = MM_STATUS_OK;
break;
}
}
//[3]判断大小是否小于64Byets
if(size < C_MM_64BYTE && status == MM_STATUS_FAIL)
{
pmm_fix_node = mm_management_getnode(pmm_fix_64_head);
if(pmm_fix_node != NULL)
{
status = MM_STATUS_OK;
break;
}
}
//[4]判断大小是否小于256Byets
if(size < C_MM_256BYTE && status == MM_STATUS_FAIL)
{
pmm_fix_node = mm_management_getnode(pmm_fix_256_head);
if(pmm_fix_node != NULL)
{
status = MM_STATUS_OK;
break;
}
}
//[5]判断大小是否小于512Byets
if(size < C_MM_512BYTE && status == MM_STATUS_FAIL)
{
pmm_fix_node = mm_management_getnode(pmm_fix_512_head);
if(pmm_fix_node != NULL)
{
status = MM_STATUS_OK;
break;
}
}
//[6]判断大小是否小于1024Byets
if(size < C_MM_1024BYTE && status == MM_STATUS_FAIL)
{
pmm_fix_node = mm_management_getnode(pmm_fix_1024_head);
if(pmm_fix_node != NULL)
{
status = MM_STATUS_OK;
break;
}
}
//[7]判断大小是否小于4096Byets
if(size < C_MM_4096BYTE && status == MM_STATUS_FAIL)
{
pmm_fix_node = mm_management_getnode(pmm_fix_4096_head);
if(pmm_fix_node != NULL)
{
status = MM_STATUS_OK;
break;
}
}
}while(0);
if(status == MM_STATUS_OK)
{
mm_ptr = pmm_fix_node->mm_node;
pmm_fix_node->iflag = MM_STATUS_BUSY;
}
else
{
mm_ptr = (unsigned int *)malloc(size);
}
return (unsigned int *)mm_ptr;
}
void mm_management_free(void *mm_ptr)
{
unsigned int i;
int status = MM_STATUS_FAIL;
P_MM_Node_STRUCT pmm_fix_node;
do{
//[1]如果地址是16Bytes碎片地址,则释放内存
status = mm_management_node_free(pmm_fix_16_head, mm_ptr, C_MM_16BYTE);
if(status == MM_STATUS_OK)
break;
//[2]如果地址是64Bytes碎片地址,则释放内存
status = mm_management_node_free(pmm_fix_64_head, mm_ptr, C_MM_64BYTE);
if(status == MM_STATUS_OK)
break;
//[1]如果地址是256Bytes碎片地址,则释放内存
status = mm_management_node_free(pmm_fix_256_head, mm_ptr, C_MM_256BYTE);
if(status == MM_STATUS_OK)
break;
//[1]如果地址是512Bytes碎片地址,则释放内存
status = mm_management_node_free(pmm_fix_512_head, mm_ptr, C_MM_512BYTE);
if(status == MM_STATUS_OK)
break;
//[1]如果地址是1024Bytes碎片地址,则释放内存
status = mm_management_node_free(pmm_fix_1024_head, mm_ptr, C_MM_1024BYTE);
if(status == MM_STATUS_OK)
break;
//[1]如果地址是4096Bytes碎片地址,则释放内存
status = mm_management_node_free(pmm_fix_4096_head, mm_ptr, C_MM_4096BYTE);
if(status == MM_STATUS_OK)
break;
}while(0);
if(status == MM_STATUS_OK)
{
//do nothing,在mm_management_node_free函数中已经将pmm_fix_node->iflag设为MM_STATUS_FREE
}
else
{
free(mm_ptr);
}
}
//获取MM_SDRAM_STRUCT里的空闲节点
static P_MM_Node_STRUCT mm_management_getnode(P_MM_SDRAM_STRUCT pmm_fix_head)
{
P_MM_SDRAM_STRUCT pmm_fix_head_tmp = pmm_fix_head;
P_MM_Node_STRUCT pmm_fix_node = pmm_fix_head_tmp->next;
unsigned int count = pmm_fix_head_tmp->count;
unsigned int i;
for(i=0; i<count; i++)
{
if(pmm_fix_node->iflag == MM_STATUS_FREE)
break;
pmm_fix_node = pmm_fix_node->next;
}
if(i < count)
return pmm_fix_node;
else
return NULL;
}
//比较MM_SDRAM_STRUCT的所有节点,如果地址一致,则释放地址
static unsigned int mm_management_node_free(P_MM_SDRAM_STRUCT pmm_fix_head, unsigned int *mm_ptr, unsigned int size)
{
P_MM_SDRAM_STRUCT pmm_fix_head_tmp = pmm_fix_head;
P_MM_Node_STRUCT pmm_fix_node = pmm_fix_head_tmp->next;
unsigned int count = pmm_fix_head_tmp->count;
unsigned int i;
for(i=0; i<count; i++)
{
if(pmm_fix_node->mm_node == mm_ptr)
{
if(pmm_fix_node->iflag == MM_STATUS_FREE)
{
printf("mm management have been free\r\n");
}
else
{
pmm_fix_node->iflag = MM_STATUS_FREE;
memset((void *)mm_ptr, 0x00, size); //释放内存后,把碎片内存清0
}
return MM_STATUS_OK;
}
pmm_fix_node = pmm_fix_node->next;
}
return MM_STATUS_FAIL;
}
这份代码我写得还是比较简单,注释些也写得清楚,明白它的原理,应该很容易就看懂。
说一下这个机制的优缺点:
优点:
小内存申请的时候,先去提前申请好的内存中获取,这样可以很好地解决内存碎片问题。
缺点以及优化:
1.碎片管理机制可申请的碎片数量是有限的,当数量被申请完之后,还是得重新用malloc申请;但是这可以通过我定义的 C_MM_16BYTE_NUM 和 C_MM_16BYTE 这些宏定义修改碎片数量,根据项目需要修改数量,也是能很好的优化此问题;
2.比如我要申请4个Bytes,但此时,16,64,256,512,1024这几个链表已经用完了,那此时它会用4096这个链表去给4Bytes使用,当然,这同样可以修改C_MM_16BYTE_NUM 和 C_MM_16BYTE 这些宏定义优化这个问题。
七、嵌入式内存管理2
任何程序运行起来都需要分配内存空间存放该进程的资源信息的,C程序也不例外。C程序中的变量、常量、函数、代码等等的信息所存放的区域都有所不同,不同的区域又有不同的特性。C语言学习者、尤其是在学习嵌入式的朋友,这些知识点一定要吃透!
被欺骗的C进程
每一个C语言的程序被执行起来的时候系统为了更方便开发人员操作,会给每一个进程分配一个虚拟的内存空间,它实际上是从处理内存映射出来的。虚拟内存的起始地址结束地址都是固定的,因此虚拟内存的布局都是一样。比如有三个进程 P1 P2 P3 ,他们虽然得到的物理内存是完全不一样,但是从进程的角度来看他们三个得到的内存确实一模一样的。
假设你正在使用的计算机实际物理内存只有 1GB 大小,而当前系统运行了三个进程,Linux 会将 PM 中的某些内存映射为三个大小均为 4GB 的虚拟内存 ,让每个进程都以为自己独自拥有了完整的内存空间,这样极大地方 便了应用层程序的数据和代码的组织。
虚拟内存布局:
虚拟内存布局分为内核空间、栈、堆、数据段、代码段和一个不允许访问的空间(相当于一堵墙)。
一个用户进程可以访问的内存区域介于 0x0804 8000 到0xc0000000 之间,这个“广袤”的区域又被分成了几个部分,分别用来存放进程的代码和数据。
下面让我们更进一步地研究虚拟内存中每一个空间所存放的是什么类型的数据。
栈内存
栈内存是用于存放环境变量、命令行参数和局部变量的。栈内存空间十分有限,默认情况下栈的大小为 8M ,在嵌入式开发的时候我们应该尽可能减少使用栈空间。栈空间的增长,从上(高地址) 往下 (低地址)每当有一个函数被调用的时候,栈就会从上往下分配一个段,这一段空间就是一个栈帧,该内存空间用来存放该函数的局部变量。
当一个函数退出(调用结束)的时候,栈空间会从下往上释放一个栈帧,将所有的内存归还给系统。
注意:
栈空间中的内存存放的数据值是未知的, 因此每一个局部变量在使用之前最好做好初始化
栈内存的空间我们无法手动实现申请与释放,都是由系统自动完成,我们无法干预。
堆空间
堆空间是相对自由的空间,这是一个非常重要的区域,因为在此区域定义的内存的 生命周期我们是可以控制的:从 malloc( )/calloc( )/realloc( )开始,到 free( )结束,其分配和释放完全由我们开发者自定义,这就给了我们最大的自由和灵活性,让程序在运行的过 程当中,以最大的效益使用内存。
注意:
- 相对于栈空间来说,堆的内存空间相对大很多
- 堆空间的增长方式,从下(低地址)往上(高地址)
- 堆空间中的内存都属于匿名空间, 因此需要借助指针来访问
- 有开发者自行申请和释放的,如果没有释放那么这个空间将一直存在,直到程序结束。
数据段
数据段中存放着全局变量、静态变量、和常量这些数据,生命周期与程序一致。程序不止,数据不断(段)。
代码段
代码段中又分成了两个空间:
.text段:存放用户的代码(mian func ...)
init段:当程序运行之初的一些初始化的工作(由编译器根据系统来对应添加的)
内存管理是嵌入式学习的重点知识,也是判断一个人是否入门的重要标志。内存管理学得好,对C语言的理解又会更加深刻一些。
八、malloc(0)会发生什么
这个问题看起来十分刁钻,不过稍有常识的人都知道,制定 C 标准的那帮语言律师也不是吃白饭的,对这种奇奇怪怪的问题一定会有定义。翻阅C17 标准 草案 N2176,在 7.22.3 节里,有如下说法:
The order and contiguity of storage allocated by successive calls to the aligned_alloc, calloc, malloc, and realloc functions is unspecified. The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object with a fundamental alignment requirement and then used to access such an object or an array of such objects in the space allocated (until the space is explicitly deallocated). The lifetime of an allocated object extends from the allocation until the deallocation. Each such allocation shall yield a pointer to an object disjoint from any other object. The pointer returned points to the start (lowest byte address) of the allocated space. If the space cannot be allocated, a null pointer is returned. If the size of the space requested is zero, the behavior is implementation-defined: either a null pointer is returned to indicate an error, or the behavior is as if the size were some nonzero value, except that the returned pointer shall not be used to access an object.
在这里,标准委员会明确规定了:当 malloc 接到的参数为 0 时,其行为是由实现定义的(implementation-defined)。
由实现定义的行为这个词就提醒我们,在实际编程时如果要考虑到程序在多个运行环境下进行运行时,不能对 malloc 返回的数值进行任何假设。
换言之,没事儿不要吃饱了撑的在实际编程中写下 malloc(0) 这种天怒人怨的代码。
但是,这个无意义的问题吸引了我的兴趣。因此我开始查阅 glibc 的源代码,依此了解在 glibc 下,mallloc(0) 的行为。在 glibc2.27/malloc/malloc.c 中,有如下注释:
/*
malloc(size_t n)
Returns a pointer to a newly allocated chunk of at least n bytes, or null
if no space is available. Additionally, on failure, errno is
set to ENOMEM on ANSI C systems.
If n is zero, malloc returns a minumum-sized chunk. (The minimum
size is 16 bytes on most 32bit systems, and 24 or 32 bytes on 64bit
systems.) On most systems, size_t is an unsigned type, so calls
with negative arguments are interpreted as requests for huge amounts
of space, which will often fail. The maximum supported value of n
differs across systems, but is in all cases less than the maximum
representable value of a size_t.
*/
注释已经说的很清楚了,当我们执行 malloc(0) 时,我们实际会拿到一个指向一小块内存的指针,这个指针指向的(分配给我们的)内存的大小是由机器决定的。
细读代码,可以发现,将读入的内存大小进行转换是由宏 checked_request2size 实现的。
相关的宏定义如下:
/* pad request bytes into a usable size -- internal version */
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
/* Same, except also perform an argument and result check. First, we check
that the padding done by request2size didn't result in an integer
overflow. Then we check (using REQUEST_OUT_OF_RANGE) that the resulting
size isn't so large that a later alignment would lead to another integer
overflow. */
#define checked_request2size(req, sz) \
({ \
(sz) = request2size (req); \
if (((sz) < (req)) \
|| REQUEST_OUT_OF_RANGE (sz)) \
{ \
__set_errno (ENOMEM); \
return 0; \
} \
})
也就是说,我们能申请到的数值最小为 MINSIZE ,这个 MINSIZE 的相关定义如下:
/* The smallest possible chunk */
#define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))
/* The smallest size we can malloc is an aligned minimal chunk */
#define MINSIZE \
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))/* The corresponding bit mask value. */
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
/* MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks. It
must be a power of two at least 2 * SIZE_SZ, even on machines for
which smaller alignments would suffice. It may be defined as larger
than this though. Note however that code and data structures are
optimized for the case of 8-byte alignment. */
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
? __alignof__ (long double) : 2 * SIZE_SZ)
#ifndef INTERNAL_SIZE_T
# define INTERNAL_SIZE_T size_t
#endif
/* The corresponding word size. */
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
// GCC 提供
/* Offset of member MEMBER in a struct of type TYPE. */
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)
至此,我们就可以根据这些计算出使用 glibc 在我们的电脑上运行时 malloc 出的最小空间的大小了。计算完后,还可以根据 malloc_usable_size 判断自己的计算是否正确,样例代码如下:
#include <stdio.h>
#include <malloc.h>
int main(void) {
char *p = malloc(0);
printf("Address: 0x%x.\nLength: %ld.\n",p,malloc_usable_size(p));
return 0;
}
该样例在我电脑内输出的结果为 24。
因此,我们知道了,在 glibc 下,执行 malloc 会得到一个指向分配给我们的大小为 24 字节的内存空间的指针。
但这只是在 glibc 下的结果,在其他 C 标准库实现内,可能你会得到一个空指针。因为标准中提到了,对于 malloc(0) 这种故意挑事的代码,实现时可以返回一个空指针作为回礼。
九、STM32单片机内存管理器
本代码适用于无操作系统的STM32单片机开发,功能强大。
可申请到地址空间连续的不同大小的内存空间,且用户接口简单,使用方便。
直接复制粘贴如下代码即可:
memory.h:
#ifndef __MEMORY_H__
#define __MEMORY_H__
#include"stdio.h"
#include"string.h"
#include"includes.h"
//用户使用
typedefstruct
{
void *addr;//申请到的内存的起始地址
uint32_t size;//申请到的内存的大小,按照块大小分配,大于等于申请大小
uint16_t tb; //申请表序号,申请内存时分配,释放内存时使用,用户不使用
}DMEM;
//若返回空,则申请失败
DMEM *DynMemGet(uint32_t size);
voidDynMemPut(DMEM *pDmem);
#endif//__MEMORY_H__
memory.c:
#include"memory.h"
#define DMEM_BLOCK_SIZE 256 //内存块大小为128字节
#define DMEM_BLOCK_NUM 20 //内存块个数为40个
#define DMEM_TOTAL_SIZE (DMEM_BLOCK_SIZE*DMEM_BLOCK_NUM) //内存总大小
typedefenum
{
DMEM_FREE = 0,
DMEM_USED = 1,
}DMEM_USED_ITEM;
typedefstruct
{
DMEM_USED_ITEM used; //使用状态
uint16_t blk_s; //起始块序号
uint16_t blk_num; //块个数
}DMEM_APPLY;
typedefstruct
{
DMEM_USED_ITEM tb_blk[DMEM_BLOCK_NUM];
DMEM tb_user[DMEM_BLOCK_NUM]; //用户申请内存信息
DMEM_APPLY tb_apply[DMEM_BLOCK_NUM]; //系统分配内存信息
uint16_t apply_num; //内存申请表占用数目
uint16_t blk_num; //内存块占用数目
}DMEM_STATE;
staticuint8_t DMEMORY[DMEM_TOTAL_SIZE];
static DMEM_STATE DMEMS = {0};
DMEM *DynMemGet(uint32_t size)
{
uint16_t loop = 0;
uint16_t find = 0;
uint16_t blk_num_want = 0;
DMEM * user = NULL;
DMEM_APPLY *apply = NULL;
//申请内存大小不能为0
if(size == 0) { returnNULL; }
//申请内存不可超过总内存大小
if(size > DMEM_TOTAL_SIZE) { returnNULL; }
//申请内存不可超过剩余内存大小
if(size > (DMEM_BLOCK_NUM - DMEMS.blk_num) * DMEM_BLOCK_SIZE) { returnNULL; }
//申请表必须有空余
if(DMEMS.apply_num >= DMEM_BLOCK_NUM) { returnNULL; }
//计算所需连续块的个数
blk_num_want = (size + DMEM_BLOCK_SIZE - 1) / DMEM_BLOCK_SIZE;
//寻找申请表
for(loop = 0; loop < DMEM_BLOCK_NUM; loop++)
{
if(DMEMS.tb_apply[loop].used == DMEM_FREE)
{
apply = &DMEMS.tb_apply[loop]; //申请表已找到
user = &DMEMS.tb_user[loop]; //用户表对应找到
user->tb = loop; //申请表编号记录
user->size = blk_num_want * DMEM_BLOCK_SIZE; //分配大小计算
break;
}
}
//没有找到可用申请表,理论上是不会出现此现象的,申请表剩余已在上面校验
if(loop == DMEM_BLOCK_NUM) { returnNULL; }
//寻找连续内存块
for(loop = 0; loop < DMEM_BLOCK_NUM; loop++)
{
if(DMEMS.tb_blk[loop] == DMEM_FREE)
{//找到第一个空闲内存块
for(find = 1; (find < blk_num_want) && (loop + find < DMEM_BLOCK_NUM); find ++)
{//找到下一个空闲内存块
if(DMEMS.tb_blk[loop + find] != DMEM_FREE)
{//发现已使用内存块
break;
}
}
if(find >= blk_num_want)
{//寻找到的空闲内存块数目已经够用
user->addr = DMEMORY + loop * DMEM_BLOCK_SIZE; //计算申请到的内存的地址
apply->blk_s = loop; //记录申请到的内存块首序号
apply->blk_num = blk_num_want; //记录申请到的内存块数目
for(find = 0 ; find < apply->blk_num; find++)
{
DMEMS.tb_blk[loop + find] = DMEM_USED;
}
apply->used = DMEM_USED; //标记申请表已使用
DMEMS.apply_num += 1;
DMEMS.blk_num += blk_num_want;
return user;
}
else
{//寻找到的空闲内存块不够用,从下一个开始找
loop += find;
}
}
}
//搜索整个内存块,未找到大小适合的空间
returnNULL;
}
voidDynMemPut(DMEM *user)
{
uint16_t loop = 0;
//若参数为空,直接返回
if(NULL == user) { return; }
//释放内存空间
for(loop = DMEMS.tb_apply[user->tb].blk_s; loop < DMEMS.tb_apply[user->tb].blk_s + DMEMS.tb_apply[user->tb].blk_num; loop++)
{
DMEMS.tb_blk[loop] = DMEM_FREE;
DMEMS.blk_num -= 1;
}
//释放申请表
DMEMS.tb_apply[user->tb].used = DMEM_FREE;
DMEMS.appl