malloc() 和 free() 是如何工作的?

How do malloc() and free() work?

提问人:Priyanka Mishra 提问时间:7/13/2009 最后编辑:SU3Priyanka Mishra 更新时间:4/13/2018 访问量:198601

问:

我想知道如何工作。mallocfree

int main() {
    unsigned char *p = (unsigned char*)malloc(4*sizeof(unsigned char));
    memset(p,0,4);
    strcpy((char*)p,"abcdabcd"); // **deliberately storing 8bytes**
    cout << p;
    free(p); // Obvious Crash, but I need how it works and why crash.
    cout << p;
    return 0;
}

如果答案在记忆层面上很深入,如果可能的话,我将不胜感激。

C++ C 内存管理 malloc 免费

评论

6赞 Vilx- 7/13/2009
它实际上不应该依赖于所使用的编译器和运行时库吗?
10赞 Naveen 7/13/2009
这将取决于 CRT 的实现。所以你不能一概而论。
67赞 Evan Teran 7/13/2009
该 strcpy 写入 9 个字节,而不是 8 个字节。不要忘记 NULL 终止符 ;-)。
5赞 phuclv 4/15/2016
不要在 C 中强制转换 malloc 的结果
2赞 Braden Best 12/18/2016
@LưuVĩnhPhúc这C++。请注意cout <<

答:

44赞 samoz 7/13/2009 #1

malloc/free 的一个实现执行以下操作:

  1. 通过 sbrk() 从操作系统获取内存块(Unix 调用)。
  2. 在该内存块周围创建一个页眉和页脚,其中包含一些信息,例如大小、权限以及下一个和上一个块的位置。
  3. 当对 malloc 的调用传入时,会引用一个列表,该列表指向适当大小的块。
  4. 然后返回此块,并相应地更新页眉和页脚。
6赞 anon 7/13/2009 #2

这与 malloc 和 free 没有特别关系。复制字符串后,程序表现出未定义的行为 - 它可能会在该点或之后的任何时间点崩溃。即使您从未使用过 malloc 和 free,并在堆栈上或静态分配了 char 数组,也是如此。

2赞 Sebastiaan M 7/13/2009 #3

这很难说,因为不同编译器/运行时之间的实际行为是不同的。甚至调试/发布版本也具有不同的行为。VS2005 的调试版本将在分配之间插入标记以检测内存损坏,因此它不会崩溃,而是在 free() 中断言。

23赞 Chris Arguin 7/13/2009 #4

从理论上讲,malloc 从此应用程序的操作系统中获取内存。但是,由于您可能只需要 4 个字节,并且操作系统需要在页面(通常是 4k)中工作,因此 malloc 的作用远不止于此。它占用一个页面,并将自己的信息放在那里,以便它可以跟踪您从该页面分配和释放的内容。

例如,当您分配 4 个字节时,malloc 会为您提供指向 4 个字节的指针。您可能没有意识到的是,malloc 正在使用 8-12 字节之前的 4-4 个字节的内存来形成您分配的所有内存的链。当您调用 free 时,它会获取您的指针,备份到数据所在的位置,并对其进行操作。

当您释放内存时,malloc 会将该内存块从链上移除......并且可能会也可能不会将该内存返回到操作系统。如果是这样,那么访问该内存可能会失败,因为操作系统将剥夺您访问该位置的权限。如果 malloc 保留内存(因为它在该页面中分配了其他内容,或者用于某些优化),则访问将发生工作。这仍然是错误的,但它可能会起作用。

免责声明:我所描述的是 malloc 的常见实现,但绝不是唯一可能的实现。

4赞 Goz 7/13/2009 #5

这取决于内存分配器实现和操作系统。

例如,在 Windows 下,进程可以请求一页或更多 RAM。然后,OS 将这些页面分配给进程。但是,这不是分配给应用程序的内存。CRT 内存分配器会将内存标记为连续的“可用”块。然后,CRT 内存分配器将遍历可用块列表,并找到它可以使用的最小块。然后,它将根据需要获取该块,并将其添加到“已分配”列表中。附加到实际内存分配头的将是头。此标头将包含各种信息位(例如,它可以包含下一个和之前分配的块以形成链表。它很可能包含分配的大小)。

然后,Free 将删除标头并将其添加回可用内存列表。如果它与周围的自由块形成一个更大的块,这些块将被加在一起以产生一个更大的块。如果整个页面现在都是空闲的,则分配器很可能会将该页面返回到操作系统。

这不是一个简单的问题。操作系统分配器部分完全不受您的控制。我建议你通读 Doug Lea 的 Malloc (DLMalloc) 之类的东西,以了解一个相当快的分配器是如何工作的。

编辑:您的崩溃将是由于写入大于分配而覆盖了下一个内存标头的事实。这样,当它释放时,它会非常困惑它到底释放了什么以及如何合并到下一个块中。这可能并不总是在免费时立即导致崩溃。它可能会导致以后崩溃。一般来说,避免内存覆盖!

12赞 Steve Jessop 7/13/2009 #6

由于 NUL 终止符,strcpy 行尝试存储 9 个字节,而不是 8 个字节。它调用未定义的行为。

对 free 的调用可能会也可能不会崩溃。分配的 4 个字节之后的内存可能会被 C 或 C++ 实现用于其他用途。如果它被用于其他事情,那么在上面乱涂乱画会导致“其他事情”出错,但如果它不用于其他任何事情,那么你可能会碰巧逃脱它。“侥幸逃脱”可能听起来不错,但实际上很糟糕,因为这意味着你的代码看起来运行正常,但在将来的运行中,你可能无法逃脱它。

使用调试样式的内存分配器时,你可能会发现其中写入了一个特殊的保护值,并且 free 会检查该值,如果找不到该值,则会发生警报。

否则,您可能会发现接下来的 5 个字节包含属于尚未分配的其他内存块的链接节点的一部分。释放块可能涉及将其添加到可用块列表中,并且由于您在列表节点中乱涂乱画,因此该操作可能会取消引用具有无效值的指针,从而导致崩溃。

这完全取决于内存分配器 - 不同的实现使用不同的机制。

5赞 plinth 7/13/2009 #7

malloc 和 free 依赖于实现。典型的实现涉及将可用内存分区为“可用列表”,即可用内存块的链接列表。许多实现人为地将其划分为小对象和大对象。空闲块从有关内存块的大小以及下一个内存块的位置等信息开始。

当您 malloc 时,会从空闲列表中拉取一个块。释放后,该块将放回可用列表中。很有可能,当您覆盖指针的末尾时,您正在写在可用列表中的块的标题上。当您释放内存时,free() 会尝试查看下一个块,并且可能最终会命中导致总线错误的指针。

12赞 Martin Liversage 7/13/2009 #8

malloc() 和 free() 的工作原理取决于所使用的运行时库。通常,malloc() 从操作系统中分配一个堆(内存块)。然后,对 malloc() 的每个请求都会分配一小块内存,并返回指向调用方的指针。内存分配例程必须存储有关分配的内存块的一些额外信息,以便能够跟踪堆上的已用内存和可用内存。此信息通常存储在 malloc() 返回的指针之前的几个字节中,它可以是内存块的链接列表。

通过写入 malloc() 分配的内存块,您很可能会破坏下一个块的一些簿记信息,这可能是剩余的未使用的内存块。

程序也可能崩溃的一个地方是将过多字符复制到缓冲区中时。如果额外的字符位于堆外部,则在尝试写入不存在的内存时可能会遇到访问冲突。

3赞 devdimi 7/13/2009 #9

您的程序崩溃是因为它使用了不属于您的内存。它可能被其他人使用,也可能不被其他人使用 - 如果你幸运的话,你崩溃了,如果不是,问题可能会隐藏很长时间,稍后再回来咬你。

就 malloc/free 实现而言 - 整本书都致力于这个主题。基本上,分配器将从操作系统中获取更大的内存块并为您管理它们。分配器必须解决的一些问题是:

  • 如何获取新内存
  • 如何存储它 - (列表或其他结构,不同大小的内存块的多个列表,等等)
  • 如果用户请求的内存多于当前可用内存,该怎么办(从操作系统请求更多内存,加入一些现有块,如何准确加入它们,...
  • 当用户释放内存时要执行的操作
  • 调试分配器可能会给你请求的更大的块,并填充它一些字节模式,当你释放内存时,分配器可以检查是否写在块之外(这可能发生在你的情况下) ...
444赞 Juergen 7/13/2009 #10

好的,一些关于 malloc 的答案已经发布。

更有趣的部分是自由是如何工作的(在这个方向上,malloc 也可以更好地理解)。

在许多 malloc/free 实现中,free 通常不会将内存返回给操作系统(或者至少在极少数情况下)。原因是你会在堆中得到间隙,因此可能会发生这种情况,你只是用间隙完成了 2 或 4 GB 的虚拟内存。应该避免这种情况,因为一旦虚拟内存完成,您将遇到非常大的麻烦。另一个原因是,OS 只能处理具有特定大小和对齐方式的内存块。具体来说:通常操作系统只能处理虚拟内存管理器可以处理的块(通常是 512 字节的倍数,例如 4KB)。

因此,将 40 字节返回给操作系统是行不通的。那么免费有什么作用呢?

Free 会将内存块放在它自己的可用块列表中。通常,它还会尝试将地址空间中的相邻块融合在一起。空闲阻止列表只是一个内存块的循环列表,这些内存块在开始时有一些管理数据。这也是为什么使用标准 malloc/free 管理非常小的内存元素效率低下的原因。每个内存块都需要额外的数据,并且随着大小的变小,会发生更多的碎片。

当需要新的内存块时,free-list 也是 malloc 首先查看的地方。在从操作系统调用新内存之前,会对其进行扫描。当发现一个块大于所需内存时,它会分为两部分。一个返回给调用方,另一个被放回空闲列表。

此标准行为有许多不同的优化(例如,针对小块内存)。但是,由于 malloc 和 free 必须如此普遍,因此当替代方案不可用时,标准行为始终是后备。在处理自由列表方面也进行了优化,例如将块存储在按大小排序的列表中。但所有优化也有其自身的局限性。

为什么你的代码会崩溃:

原因是,通过将 9 个字符(不要忘记尾随的 null 字节)写入大小为 4 个字符的区域,您可能会覆盖为另一个内存块存储的管理数据,该内存块位于数据块的“后面”(因为此数据通常存储在内存块的“前面”)。当 free 尝试将您的块放入 free 列表中时,它可能会触及此管理数据,因此会绊倒被覆盖的指针。这将使系统崩溃。

这是一种相当优雅的行为。我还看到过这样的情况:某处失控的指针覆盖了可用内存列表中的数据,并且系统没有立即崩溃,而是后来发生了一些子例程。即使在中等复杂度的系统中,此类问题也很难调试!在我参与的一个案例中,我们(一大群开发人员)花了几天时间才找到崩溃的原因——因为它的位置与内存转储指示的位置完全不同。这就像一颗定时炸弹。你知道,你的下一个“free”或“malloc”会崩溃,但你不知道为什么!

这些是一些最糟糕的 C/C++ 问题,也是指针如此成问题的原因之一。

评论

78赞 Artelius 9/8/2009
很多人没有意识到 free() 可能不会将内存返回给操作系统,这很令人愤怒。谢谢你帮助启发他们。
0赞 Guillaume Paris 8/10/2014
阿特留斯:恰恰相反,新的意志总是如此?
4赞 Yay295 3/22/2015
@Guillaume07 我猜你的意思是删除,而不是新的。不,它没有(不一定)。delete 和 free (几乎)做同样的事情。以下是每个人在MSVC2013中调用的代码:goo.gl/3O2Kyu
3赞 David C. 10/8/2015
delete 将始终调用析构函数,但内存本身可能会进入空闲列表以供以后分配。根据实现的不同,它甚至可能是 malloc 使用的相同自由列表。
1赞 Undefined Behaviour 8/5/2016
@Juergen 但是当 free() 读取包含从 malloc 分配多少内存的信息的额外字节时,它得到 4。那么崩溃是如何发生的,或者free()如何接触管理数据?
68赞 joe 7/13/2009 #11

正如 aluser 在这个论坛帖子中所说:

您的进程有一个内存区域,从地址 x 到地址 y, 称为堆。您的所有恶意数据都位于此区域。malloc() 保留一些数据结构,比如说一个列表,包含所有可用块 堆中的空间。当您调用 malloc 时,它会在列表中查找 一个对你来说足够大的块,返回指向它的指针,然后 记录了它不再免费的事实以及它有多大。 当您使用相同的指针调用 free() 时,free() 会查找多大 该块并将其添加回 free chunks() 列表中。如果你 调用 malloc() 时,它在堆中找不到任何足够大的块,它 使用 brk() 系统调用来增加堆,即增加地址 y 和 使旧 Y 和新 Y 之间的所有地址都有效 记忆。brk() 必须是系统调用;没有办法做同样的事情 完全来自用户空间。

malloc() 依赖于系统/编译器,因此很难给出具体的答案。但是,基本上,它确实会跟踪它分配的内存,并且根据它的方式,因此您对 free 的调用可能会失败或成功。

malloc() and free() don't work the same way on every O/S.

评论

3赞 Braden Best 12/18/2016
这就是为什么它被称为未定义的行为。一种实现可能会使恶魔从你的鼻子里飞出来,当你在无效写入后调用 free 时。你永远不知道。
33赞 DigitalRoss 4/4/2011 #12

内存保护具有页面粒度,需要内核交互

您的示例代码本质上是询问为什么示例程序不捕获,答案是内存保护是内核功能,仅适用于整个页面,而内存分配器是库功能,它管理 ..没有强制执行..任意大小的块,通常比页面小得多。

内存只能以页为单位从程序中删除,即使这样也不太可能被观察到。

如有必要,calloc(3) 和 malloc(3) 会与内核进行交互以获取内存。但是大多数 free(3) 的实现不会将内存返回给内核1,它们只是将其添加到一个 free 列表中,calloc() 和 malloc() 稍后会参考该列表,以便重用已释放的块。

即使 free() 想要将内存返回给系统,它也至少需要一个连续的内存页才能让内核实际保护该区域,因此释放一个小块只会导致保护更改,如果它是页面中的最后一个小块。

所以你的区块就在那里,坐在免费名单上。您几乎总是可以访问它和附近的内存,就像它仍然被分配一样。C 直接编译为机器代码,没有特殊的调试安排,没有对加载和存储的健全性检查。现在,如果您尝试访问一个空闲块,则该行为不会被标准定义,以免对库实现者提出不合理的要求。如果您尝试在分配的块之外访问释放的内存或内存,则可能会出现各种问题:

  • 有时分配器维护单独的内存块,有时它们使用在块之前或之后分配的标头(我猜是“页脚”),但它们可能只是想使用块中的内存,以便将可用列表链接在一起。如果是这样,则读取块是可以的,但其内容可能会更改,并且写入块可能会导致分配器行为异常或崩溃。
  • 当然,你的块可能会在将来被分配,然后它可能会被你的代码或库例程覆盖,或者被 calloc() 覆盖为零。
  • 如果重新分配块,它的大小也可能发生变化,在这种情况下,将在各个地方写入更多链接或初始化。
  • 显然,你可能会引用的范围太远,以至于你越过了程序的内核已知段之一的边界,在这种情况下,你将陷入困境。

工作原理

因此,从你的示例到整体理论,malloc(3) 在需要时从内核获取内存,通常以页为单位。这些页面根据程序的需要进行拆分或合并。Malloc 和 free 合作维护一个目录。在可能的情况下,它们会合并相邻的自由块,以便能够提供大块。该目录可能涉及也可能不涉及使用释放块中的内存来形成链表。(另一种选择是共享内存和分页友好型,它涉及专门为目录分配内存。Malloc 和 free 几乎没有能力强制访问单个块,即使将特殊和可选的调试代码编译到程序中也是如此。


1. 很少有 free() 的实现尝试将内存返回给系统,这并不一定是由于实现者懈怠了。与内核交互比简单地执行库代码要慢得多,而且好处很小。大多数程序都具有稳定状态或增加的内存占用,因此分析堆以寻找可返回内存所花费的时间将完全浪费。其他原因包括内部碎片使页面对齐的块不太可能存在,并且返回块可能会将块碎片化到任一侧。最后,少数返回大量内存的程序可能会绕过 malloc(),无论如何都只是分配和释放页面。

评论

1赞 Goaler444 4/14/2015
好答案。推荐这篇论文:Dynamic Storage Allocation: A survey and Critical review by Wilson et al,用于对分配器使用的内部机制(如标头字段和自由列表)进行深入审查。
1赞 mgalgs 9/17/2013 #13

同样重要的是要认识到,简单地移动程序中断指针,实际上并没有分配内存,它只是设置了地址空间。例如,在 Linux 上,当访问该地址范围时,内存将由实际的物理页“备份”,这将导致页面错误,并最终导致内核调用页面分配器以获取备份页。brksbrk