301

我想知道如何malloc工作free

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

如果可能的话,如果答案在记忆水平上是深入的,我将不胜感激。

4

13 回答 13

420

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

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

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

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

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

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

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

为什么你的代码会崩溃:

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

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

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

于 2009-07-13T13:06:06.860 回答
63

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

您的进程有一个内存区域,从地址 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.

于 2009-07-13T13:14:13.160 回答
43

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

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

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

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

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

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

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

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

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

操作理论

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


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

于 2011-04-03T21:10:20.567 回答
23

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

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

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

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

于 2009-07-13T12:31:36.177 回答
12

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

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

使用调试风格的内存分配器,您可能会发现那里已经写入了一个特殊的保护值,并且 free 会检查该值并在找不到时发生恐慌。

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

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

于 2009-07-13T12:32:37.597 回答
12

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

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

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

于 2009-07-13T12:35:23.403 回答
6

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

于 2009-07-13T12:29:17.830 回答
5

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

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

于 2009-07-13T12:34:12.933 回答
4

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

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

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

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

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

于 2009-07-13T12:32:33.823 回答
3

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

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

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

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

于 2009-07-13T12:31:01.687 回答
1

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

于 2013-09-16T21:07:59.957 回答