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;
}

如果答案是在记忆层面上的深度,如果可能的话,我会非常感激。

185026 次浏览

malloc/free的一个实现如下所示:

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

这与malloc和free没有特别的关系。你的程序在复制字符串后表现出未定义的行为——它可能在那一点或之后的任何一点崩溃。即使您从未使用malloc和free,并在堆栈上或静态地分配char数组,也会出现这种情况。

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

理论上,malloc为这个应用程序从操作系统获取内存。然而,由于您可能只需要4个字节,而操作系统需要在页面上工作(通常是4k), malloc所做的要比这多一点。它取一个页面,并把它自己的信息放在那里,这样它就可以跟踪你从该页中分配和释放了什么。

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

当你释放内存时,malloc将内存块从链上取下…并且可能会也可能不会将这些内存返回给操作系统。如果它这样做,那么访问内存可能会失败,因为操作系统将拿走你访问该位置的权限。如果malloc保留内存(因为它在该页中分配了其他内容,或者用于某些优化),则访问将正常工作。这仍然是错误的,但可能会起作用。

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

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

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

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

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

编辑:你的崩溃将由这样一个事实引起,即写大于分配,你已经覆盖了下一个内存头。这样,当它释放时,它会非常困惑,不知道它到底释放了什么,以及如何合并到下面的块中。这可能并不总是会直接导致免费的崩溃。这可能会导致以后的崩溃。一般避免内存覆盖!

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

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

使用调试风格的内存分配器,您可能会发现在那里写了一个特殊的保护值,free会检查该值,如果没有找到它就会惊慌失措。

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

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

Malloc和free依赖于实现。典型的实现包括将可用内存划分为“空闲列表”——可用内存块的链表。许多实现人为地将它分为小对象和大对象。空闲块以内存块有多大以及下一个内存块在哪里等信息开始。

当你malloc时,一个块从空闲列表中拉出来。当你释放时,块被放回空闲列表。很有可能,当你重写指针的末尾时,你是在写空闲列表中一个块的头。当您释放内存时,free()尝试查看下一个块,并可能最终击中导致总线错误的指针。

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

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

在向缓冲区复制太多字符时,程序也可能崩溃。如果额外的字符位于堆之外,当您试图写入不存在的内存时,可能会遇到访问冲突。

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

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

  • 如何获得新的记忆
  • 如何存储它(列表或其他结构,不同大小的内存块的多个列表,等等)
  • 如果用户请求比当前可用的内存更多的内存怎么办(从操作系统请求更多的内存,加入一些现有的块,如何确切地加入它们,…)
  • 当用户释放内存时该怎么办
  • 调试分配器可能会给你你所请求的更大的块,并填充一些字节模式,当你释放内存时,分配器可以检查是否在块外写入(这可能发生在你的情况下) 李…< / >

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

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

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

所以返回40字节到操作系统将不能工作。那么,免费有什么作用呢?

Free会把内存块放在它自己的空闲块列表中。通常情况下,它还尝试将地址空间中的相邻块融合在一起。空闲块列表只是一个循环的内存块列表,在开始的时候有一些管理数据。这也是为什么使用标准malloc/free管理非常小的内存元素效率不高的原因。每个内存块都需要额外的数据,越小的内存块就会产生越多的碎片。

当需要新的内存块时,malloc首先查看的还是空闲列表。在它从操作系统调用新内存之前,它会被扫描。当发现一个块比所需的内存大时,它被分为两部分。一个返回给调用者,另一个返回到空闲列表中。

对于这个标准行为有许多不同的优化(例如对于小块内存)。但是,由于malloc和free必须是通用的,所以当不可用替代方案时,标准行为总是一个退路。在处理空闲列表方面也有优化——例如将块存储在按大小排序的列表中。但是所有的优化也有其局限性。

为什么你的代码崩溃:

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

这是一种相当优雅的行为。我还见过这样的情况:某个地方的失控指针覆盖了空闲内存列表中的数据,系统并没有立即崩溃,而是稍后一些子程序崩溃。即使在中等复杂度的系统中,这样的问题也很难调试!在我参与的一个案例中,我们(一个较大的开发团队)花了好几天时间才找到崩溃的原因——因为崩溃的位置与内存转储所指示的位置完全不同。这就像一个定时炸弹。你知道,你的下一个“免费”或“malloc”将崩溃,但你不知道为什么!

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

正如aluser在这个论坛线程中所说:

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

Malloc()依赖于系统/编译器,所以很难给出一个具体的答案。基本上,它会跟踪它所分配的内存,这取决于它是如何做的,所以你对free的调用可能失败或成功。

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

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

你的示例代码本质上是在问为什么示例程序没有陷阱,答案是内存保护是一个内核特性,只应用于整个页面,而内存分配器是一个库特性,它管理。没有强制执行…任意大小的块,通常比页面小得多。

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

如果需要,Calloc(3)和malloc(3)会与内核交互以获取内存。但是大多数free(3)的实现都不会将内存返回给kernel1,它们只是将内存添加到一个空闲列表中,稍后calloc()和malloc()会参考这个列表,以便重用释放的块。

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

你的block在那里,在空闲列表上。您几乎总是可以访问它和附近的内存,就像它仍然被分配一样。C直接编译为机器码,没有特殊的调试安排,就没有加载和存储的完整性检查。现在,如果您尝试访问一个空闲块,为了不对库实现者提出不合理的要求,该行为在标准中是未定义的。如果你试图访问已释放的内存或已分配块之外的内存,有各种各样的事情可能会出错:

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

操作原理

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


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

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