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


276

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

如果可能的话,我很感激如果答案在内存级别。


5
它实际上不应该取决于所使用的编译器和运行时库吗?
Vilx-

9
这将取决于CRT的实施。因此,您无法将其概括。
Naveen

58
该strcpy写入9个字节,而不是8个字节。请不要忘记NULL终止符;-)。
Evan Teran


2
@LưuVĩnhPhúc是C ++。请注意cout <<
Braden Best 2013年

Answers:


385

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

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

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

因此,将40字节返回给操作系统将不起作用。那么免费做什么?

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

当需要新的内存块时,空闲列表也是malloc查找的第一位。在从操作系统调用新内存之前,将对其进行扫描。当发现一块大于所需内存的块时,它将分为两部分。一个返回给呼叫者,另一个返回到空闲列表。

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

为什么您的代码崩溃:

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

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

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


62
太多人没有意识到free()可能不会将内存返回给操作系统,这真是令人生气。感谢您的帮助。
Artelius

阿特柳斯:相反,新的事物会一直存在吗?
Guillaume07年

3
@ Guillaume07我假设您的意思是删除,而不是新的。不,它不是(有必要)。删除和释放(几乎)做同一件事。这是每个人在MSVC2013中调用的代码:goo.gl/3O2Kyu
Yay295

1
delete将始终调用析构函数,但内存本身可能会进入空闲列表以供以后分配。根据实现的不同,它甚至可能与malloc使用的空闲列表相同。
David C.

1
@Juergen但是,当free()读取包含信息的额外字节时,它从malloc分配了多少内存,它将得到4。然后崩溃是如何发生的,或者free()如何接触管理数据?
未定义的行为

56

正如aluser在这个论坛主题中所说:

您的进程具有从地址x到地址y的内存区域,称为堆。您所有分配的数据都位于该区域中。malloc()保留了堆中所有空闲空间的一些数据结构,比如一个列表。调用malloc时,它将在列表中查找对您来说足够大的块,并返回指向该块的指针,并记录它不再释放以及释放了多少的事实。当您使用相同的指针调用free()时,free()查找该块的大小,并将其重新添加到空闲chunks()的列表中。如果调用malloc()却在堆中找不到足够大的块,它将使用brk()系统调用来增加堆,即增加地址y并使旧y与新y之间的所有地址变为是有效的内存。brk()必须是系统调用;

malloc()是依赖于系统/编译器的,因此很难给出特定的答案。但是从根本上讲,它确实会跟踪分配的内存,并取决于其执行方式,因此对free的调用可能失败或成功。

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


1
这就是为什么将其称为未定义行为。当您在无效写入后免费拨打电话时,一种实现方式可能会使恶魔从您的鼻子中飞出。你永远不会知道。
Braden Best

35

malloc / free的一种实现方式如下:

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

25

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

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

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

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

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

因此,您所在的区块就在那儿,位于免费列表中。您几乎总是可以访问它和附近的内存,就像仍在分配它一样。C直接编译成机器代码,并且没有特殊的调试安排,就不会对装载和存储进行完整性检查。现在,如果您尝试访问一个自由块,则该行为未由标准定义,以便不对库实现者提出不合理的要求。如果您尝试访问已分配块之外的已释放内存或内存,则可能会发生各种错误:

  • 有时分配器维护单独的内存块,有时他们使用在分配的块之前或之后分配的标头(我想是“页脚”),但他们只是想在块中使用内存以保持空闲列表的目的链接在一起。如果是这样,则您阅读该块是可以的,但是其内容可能会更改,并且写入该块很可能会导致分配器行为异常或崩溃。
  • 当然,您的块可能会在将来分配,然后可能会被您的代码或库例程覆盖,或者被calloc()填充为零。
  • 如果重新分配了块,则它的大小也可能会更改,在这种情况下,还会在不同位置写入更多链接或初始化。
  • 显然,您可能引用的范围太广,以至于超出了程序的内核已知段之一的边界,在这种情况下,您将陷入陷阱。

操作理论

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


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


好答案。会推荐以下文章:动态存储分配:Wilson等人的调查和重要评论,以对分配器使用的内部机制(例如标头字段和自由列表)进行深入审查。
Goaler444

23

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

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

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

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


12

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

免费电话可能会崩溃,也可能不会崩溃。C或C ++实现可能会将分配的4个字节“之后”的内存用于其他用途。如果将它用于其他用途,则对其进行遍历会导致“其他”错误,但是,如果不将其用于其他用途,则可能会碰巧放弃它。“摆脱它”听起来不错,但实际上是不好的,因为这意味着您的代码似乎可以正常运行,但是在以后的运行中,您可能无法摆脱它。

使用调试样式的内存分配器,您可能会发现已在其中写入了特殊的保护值,并且可以免费检查该值,并在找不到该值时感到恐慌。

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

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


12

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

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

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


6

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


5

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

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


4

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

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

Free然后将删除标题并将其重新添加到可用内存列表中。如果它与周围的空闲块形成一个更大的块,则将这些块加在一起以得到更大的块。如果现在整个页面都可用,则分配器很可能会将页面返回给OS。

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

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


3

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

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

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

2

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


1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.