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

时间:2009-07-13 12:22:19

标签: c++ c memory-management malloc free

我想知道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;
}

如果答案在内存级别深入,如果可能的话,我将非常感激。

13 个答案:

答案 0 :(得分:362)

确定已经发布了一些关于malloc的答案。

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

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

因此返回40字节到操作系统将无法正常工作。那么免费做什么呢?

Free会将内存块放在自己的空闲阻止列表中。通常它也会尝试将地址空间中的相邻块融合在一起。空闲块列表只是一个内存块的循环列表,它在开头有一些管理数据。这也是使用标准malloc / free管理非常小的内存元素效率不高的原因。每个内存块都需要额外的数据,而更小的内存会发生更多碎片。

当需要新的内存块时,freeoc列表也是malloc查看的第一个地方。它在从OS调用新内存之前进行扫描。当发现大于所需内存的块时,它被分成两部分。一个返回给调用者,另一个返回到空闲列表中。

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

为什么代码崩溃:

原因是通过将9个字符(不要忘记尾随的空字节)写入一个大小为4个字符的区域,您可能会覆盖存储在另一块内存中的管理数据,这些内存位于您的块“后面”数据(因为该数据通常存储在存储器块的“前面”)。如果空闲然后尝试将您的块放入空闲列表,它可以触摸此管理数据,因此偶然发现覆盖指针。这会使系统崩溃。

这是一种相当优雅的行为。我还看到了某个地方失控指针在无内存列表中覆盖了数据的情况,系统没有立即崩溃,但后来又出现了一些子程序。即使在中等复杂的系统中,这些问题也可能真的非常难以调试!在我参与的一个案例中,我们(一大群开发人员)花了好几天才找到崩溃的原因 - 因为它与内存转储指示的位置完全不同。这就像一颗定时炸弹。你知道,你的下一个“免费”或“malloc”会崩溃,但你不知道为什么!

这些是一些最糟糕的C / C ++问题,也是指针可能存在问题的一个原因。

答案 1 :(得分:52)

正如aluser在this forum thread中所说:

  

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

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

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

答案 2 :(得分:34)

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

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

答案 3 :(得分:26)

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

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

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

如果需要,

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

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

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

  • 有时分配器会维护单独的内存块,有时候它们会使用它们在块之前或之后分配的标头(我猜是“块脚”),但它们可能只是想在块内使用内存以保持目的自由列表链接在一起。如果是这样,你读取块是正常的,但其内容可能会改变,写入块可能会导致分配器行为不端或崩溃。
  • 当然,您的块可能会在将来分配,然后很可能被您的代码或库例程覆盖,或者被calloc()覆盖。
  • 如果重新分配了块,它的大小也可能会改变,在这种情况下,会在不同的地方写入更多的链接或初始化。
  • 显然,到目前为止,您可能会引用超出范围的任何一个程序内核已知段的边界,并且在这种情况下,您将陷阱。

操作理论

因此,从您的示例向后工作到整体理论,malloc(3)在需要时从内核获取内存,通常以页为单位。这些页面按程序要求进行划分或合并。 Malloc和免费合作维护目录。它们在可能的情况下合并相邻的自由块,以便能够提供大块。该目录可能涉及或可能不涉及使用释放块中的存储器来形成链表。 (替代方案是更多共享内存和分页友好,它涉及专门为目录分配内存。)即使特殊和可选的调试代码被编译成,Malloc和free也几乎没有能力强制访问各个块。该计划。


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

答案 4 :(得分:23)

理论上,malloc从操作系统获取此应用程序的内存。但是,由于您可能只需要4个字节,并且操作系统需要在页面中工作(通常为4k),因此malloc会做更多的事情。它需要一个页面,并在其中放置自己的信息,以便它可以跟踪您已分配和从该页面中释放的内容。

例如,当你分配4个字节时,malloc会给你一个指向4个字节的指针。您可能没有意识到的是,malloc使用内存8-12字节 之前的来创建已分配的所有内存的链。当你自由呼叫时,它会指向你的指针,备份到它的数据所在的位置,然后对它进行操作。

当你释放内存时,malloc将该内存块从链中取出......并且可能会也可能不会将该内存返回给操作系统。如果是这样,那么访问该内存可能会失败,因为操作系统会剥夺您访问该位置的权限。如果malloc保留内存(因为它在该页面中分配了其他内容,或者进行了一些优化),那么访问将会起作用。它仍然是错的,但它可能会奏效。

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

答案 5 :(得分:12)

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

免费电话可能会或可能不会崩溃。分配的4个字节后面的内存可能会被C或C ++实现用于其他内容。如果它被用于其他东西,那么在它上面涂鸦会导致“别的东西”出错,但如果它没有被用于其他任何东西,那么你可能碰巧侥幸逃脱它。 “躲开它”可能听起来不错,但实际上很糟糕,因为这意味着你的代码看起来运行正常,但是在未来的运行中你可能无法逃脱它。

使用调试样式的内存分配器,您可能会发现在那里写入了一个特殊的保护值,并且可以自由检查该值并在没有找到它时发生恐慌。

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

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

答案 6 :(得分:12)

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

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

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

答案 7 :(得分:6)

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

答案 8 :(得分:5)

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

当你使用malloc时,会从空闲列表中拉出一个块。当你空闲时,该块会被放回到空闲列表中。有可能,当你覆盖指针的末尾时,你正在写在空闲列表中的一个块的标题上。释放内存时,free()会尝试查看下一个块,最终可能会遇到导致总线错误的指针。

答案 9 :(得分:4)

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

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

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

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

编辑:您的崩溃将由以下事实引起:写入大于您已覆盖下一个内存标头的分配。这种方式当它释放它时,会非常混淆它究竟是什么样的自由以及如何合并到下面的块中。这可能并不总是导致免费崩溃。它可能会导致以后崩溃。一般来说,避免内存覆盖!

答案 10 :(得分:3)

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

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

  • 如何获得新记忆
  • 如何存储 - (列表或其他结构,不同大小的内存块的多个列表,等等)
  • 如果用户请求的内存超过当前可用内存(从操作系统请求更多内存,加入一些现有块,如何准确加入,......),该怎么办?
  • 当用户释放内存时该怎么办
  • 调试分配器可能会为您提供更大的块请求并填充一些字节模式,当您释放内存时,分配器可以检查是否在块外写入(可能在您的情况下发生) ...

答案 11 :(得分:2)

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

答案 12 :(得分:1)

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