malloc()和free()如何工作?
我想知道如何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; }
如果答案是在记忆层面,如果可能的话,我会很感激。
好的,关于malloc的一些答案已经发布了。
更有趣的部分是自由工作 (在这个方向上,malloc也可以更好地理解)。
在许多malloc / free实现中,free一般不会将内存返回给操作系统(或者至less在极less数情况下)。 原因是你会在你的堆中留下空隙,因此可能会发生,你刚刚完成了2或4 GB的虚拟内存与空白。 这应该避免,因为一旦虚拟内存完成,你将会遇到很大的麻烦。 另一个原因是,操作系统只能处理具有特定大小和alignment的内存块。 具体而言:通常操作系统只能处理虚拟内存pipe理器可以处理的块(最常见的是512字节的倍数,例如4KB)。
所以返回40字节的操作系统将无法正常工作。 那么免费做什么?
免费将把内存块放在自己的空闲块列表中。 通常它也试图将地址空间中的相邻块融合在一起。 空闲块列表只是一个内存块的循环列表,它在开始时有一些pipe理数据。 这也是为什么用标准的malloc / freepipe理非常小的内存元素效率不高的原因。 每个内存块都需要额外的数据,更小的尺寸会发生更多的碎片。
free-list也是malloc在需要新的内存块时的首要位置。 在从OS调用新内存之前对其进行扫描。 当发现大于所需内存的块时,它被分成两部分。 一个返回给调用者,另一个退回到空闲列表中。
这个标准行为有很多不同的优化(例如对于小块的内存)。 但是由于malloc和free必须是如此普遍,所以当替代品不可用时,标准行为总是是后备的。 在处理自由列表时也有优化 – 例如按大小sorting的列表中存储块。 但是,所有优化也有其自身的局限性。
为什么你的代码崩溃了:
原因在于,通过将9个字符(不要忘记尾随的空字节)放入一个大小为4个字符的区域,您可能会覆盖为位于数据块后面的另一块内存而存储的pipe理数据(因为这些数据通常被存储在内存块的“前面”)。 当空闲时,试图把你的块放入空闲列表,它可以触摸这个pipe理数据,因此偶然发现一个被覆盖的指针。 这会使系统崩溃。
这是一个相当优美的行为。 我也看到了一些情况,指向某处的失控指针覆盖了无内存列表中的数据,并且系统没有立即崩溃,而是稍后再执行一些子程序。 即使在一个中等复杂的系统中,这些问题真的很难debugging! 在我涉及的一个案例中,我们(一大批开发人员)花了好几天的时间来找出崩溃的原因 – 因为它与内存转储所指出的位置完全不同。 这就像定时炸弹。 你知道,你的下一个“免费”或“malloc”会崩溃,但你不知道为什么!
这些是最差的C / C ++问题,也是指针可能如此有问题的一个原因。
正如这位论坛主持人所说:
你的进程有一个从地址x到地址y的内存区域,称为堆。 你所有的malloc的数据都住在这个区域。 malloc()保留一些数据结构,让我们说一个堆中所有空闲块空间的列表。 当你调用malloc时,它通过列表查找一个足够大的块,返回一个指向它的指针,并logging它不再是空闲的以及它有多大的事实。 当用相同的指针调用free()时,free()会查找该块的大小,并将其添加回空闲块()的列表中。 如果你调用malloc()并且在堆中找不到足够大的块,它使用brk()系统调用来增长堆,即增加地址y并使旧y和新y之间的所有地址成为有效的记忆。 brk()必须是系统调用; 没有办法完全从用户空间做同样的事情。
malloc()依赖于系统/编译器,所以很难给出具体的答案。 但基本上,它确实logging了分配的内存,取决于它是如何实现的,所以你的免费通话可能会失败或成功。
malloc() and free() don't work the same way on every O/S.
malloc / free的一个实现如下:
- 通过sbrk()(Unix调用)从OS获得一块内存。
- 在内存块的周围创build一个页眉和页脚,并提供一些信息,如大小,权限以及下一个和上一个块的位置。
- 当malloc调用进来时,会引用一个指向适当大小块的列表。
- 然后返回该块,并相应地更新页眉和页脚。
理论上,malloc从操作系统获取这个应用程序的内存。 但是,由于您只需要4个字节,并且操作系统需要以页面的forms工作(通常是4k),malloc会做的更多一点。 它需要一个页面,并把它自己的信息放在那里,以便跟踪你已经分配和从那个页面中释放的内容。
例如,当你分配4个字节时,malloc会给你一个4字节的指针。 你可能没有意识到的是,你的4字节之前的8-12字节的内存正在被malloc用来创build你分配的所有内存的链。 当你免费拨打电话时,需要你的指针,备份到数据所在的位置,然后进行操作。
当你释放内存的时候,malloc把这个内存块从链中取出,并且可能会或者可能不会把这个内存返回给操作系统。 如果存在,则访问该内存可能会失败,因为操作系统将取消您访问该位置的权限。 如果malloc保存内存(因为它在页面中分配了其他内容,或者进行了一些优化),那么访问将会发生。 这仍然是错误的,但它可能工作。
免责声明:我所描述的是malloc的通用实现,但绝不是唯一可能的实现。
内存保护具有页面粒度,并需要内核交互
你的示例代码实质上是问为什么示例程序没有陷阱,答案是内存保护是一个内核function,只适用于整个页面,而内存分配器是一个库function,它pipe理..没有强制执行..任意这些块通常比页面小得多。
内存只能以页面为单位从程序中删除,甚至不可能被观察到。
如果需要,calloc(3)和malloc(3)与内核进行交互以获得内存。 但free(3)的大部分实现都不会将内存返回给内核1 ,只是将它添加到一个空闲的列表中,calloc()和malloc()稍后会查询以便重用释放的块。
即使free()想把内存返回给系统,为了让内核实际上保护这个区域,至less需要一个连续的内存页面,所以释放一个小块只会导致保护的改变页面中的最后一个小块。
所以你的座位在那里,坐在自由列表上。 你几乎可以随时访问它和附近的内存,就像它仍然被分配一样。 C直接编译为机器代码,没有特别的debugging安排,对加载和存储没有理智的检查。 现在,如果你试图访问一个空闲块,那么这个行为就不会被标准所定义,以免对图书馆执行者提出不合理的要求。 如果尝试访问已分配块之外的已释放内存或存储空间,则会出现各种可能出错的情况:
- 有时分配器会维护独立的内存块,有时候他们会使用一个头,他们只是在你的块之前或之后(我们猜测你的块)分配了一个头,但是他们可能只想使用块中的内存来保存空闲列表链接在一起。 如果是这样的话,那么你读取块是可以的,但是它的内容可能会改变,并且写入块可能会导致分配器不正常或崩溃。
- 当然,你的块可能会在将来被分配,然后很可能被你的代码或库例程覆盖,或者被calloc()清零。
- 如果块被重新分配,其大小也可以改变,在这种情况下,更多的链接或初始化将被写在不同的地方。
- 显然,你可能会参考到目前为止超出范围,你跨越你的程序的核心已知段之一的边界,在这种情况下,你会陷阱。
操作理论
所以,从你的例子到整体理论,malloc(3)从内核获取内存,当它需要的时候,通常以页为单位。 这些页面按照程序要求进行分割或合并。 Malloc和自由合作保持一个目录。 他们尽可能合并相邻的空闲块,以便能够提供大块。 该目录可能涉及或可能不涉及使用释放的块中的内存来形成链接列表。 (另一种方法是多一点共享内存和分页友好,它涉及专门为目录分配内存。)即使将特殊和可选的debugging代码编译成为单独的块,malloc和free也不具有强制访问单个块的function该程序。
1. free()尝试将内存返回给系统的实现并不一定是由于实现者松懈的事实。 与简单的执行库代码相比,与内核的交互要慢得多,而且好处也很小。 大多数程序有一个稳定状态或增加内存占用,所以分析堆寻找可回收内存的时间将被完全浪费。 其他原因包括内部碎片使得页面alignment的块不太可能存在,而且很可能返回一个块会将块分割到任何一边。 最后,less数返回大量内存的程序可能会绕过malloc(),并且简单地分配和释放页面。
在The Book(Kernighan和Ritchie“ The C Programming Language ”)中有一个malloc()
和free()
实现。 既然你不得不问,你还没有读过它 – 去阅读它,并悔改你的罪恶方式。 :d
你的strcpy行试图存储9个字节,而不是8个,因为NUL终止符。 它调用未定义的行为。
免费电话可能会或可能不会崩溃。 你的分配的4个字节之后的内存可能会被C或C ++实现所使用。 如果它被用于别的什么东西,那么在它上面涂写会导致“别的东西”出错,但是如果它不被用于其他任何东西,那么你可能会碰巧得到它。 “走开”听起来不错,但实际上是不好的,因为这意味着你的代码看起来运行良好,但在未来的运行中,你可能不会逃避。
有了一个debugging式的内存分配器,你可能会发现在那里写了一个特殊的保护值,如果它没有find它,那么这个值会自由地检查并且发生混乱。
否则,您可能会发现接下来的5个字节包含属于尚未分配的某个其他内存块的链接节点的一部分。 释放块可能涉及将其添加到可用块的列表中,并且由于您已在列表节点中进行了潦草的操作,该操作可能会使用无效值取消引用指针,导致崩溃。
这一切都取决于内存分配器 – 不同的实现使用不同的机制。
malloc()和free()如何工作取决于使用的运行时库。 通常,malloc()从操作系统分配一个堆(一块内存)。 malloc()的每个请求然后分配这个内存的一小块返回一个指向调用者的指针。 内存分配例程将不得不存储有关分配的内存块的额外信息,以便能够跟踪堆中使用的和空闲的内存。 这些信息通常在malloc()返回的指针之前的几个字节中存储,并且可以是内存块的链表。
通过写入由malloc()分配的内存块,你很可能会破坏下一个块的一些簿记信息,这可能是剩余的未使用的内存块。
一个地方,你编程也可能会崩溃是复制太多字符到缓冲区。 如果多余的字符位于堆外部,则可能会在尝试写入不存在的内存时遇到访问冲突。
这与malloc和free没有任何特别的关系。 复制string后,程序会显示未定义的行为 – 可能会在此时或之后的任何时刻崩溃。 即使你从来没有使用过malloc和free,并且在堆栈中静态分配了char数组,也是如此。
malloc和free是依赖于实现的。 一个典型的实现涉及将可用内存划分为“空闲列表” – 可用内存块的链接列表。 许多实现人为地把它分成小的对大的对象。 空闲块从有关内存块的大小以及下一个块的位置等信息开始。
当你malloc时,一个块被从空闲列表中拉出来。 空闲时,该块被放回到空闲列表中。 有机会,当你覆盖你的指针的结尾时,你正在写在空闲列表中的块的标题。 当你释放你的内存时,free()试图查看下一个块,最后可能会碰到一个导致总线错误的指针。
那么这取决于内存分配器的实施和操作系统。
例如,在Windows下,一个进程可以要求一页或更多的RAM。 OS然后将这些页面分配给进程。 然而,这不是分配给你的应用程序的内存。 CRT内存分配器将把内存标记为连续的“可用”块。 然后,CRT内存分配器将遍历空闲块列表并find可以使用的最小可能块。 然后,它会根据需要获取尽可能多的块,并将其添加到“已分配”列表中。 附加到实际内存分配的头部将是一个标题。 这个头部将包含各种各样的信息(例如,它可以包含下一个和上一个分配的块来形成一个链表,它最有可能包含分配的大小)。
然后,免费将删除标题,并将其添加回空闲内存列表。 如果它与周围的空闲块形成一个更大的块,这些将被加在一起给出一个更大的块。 如果整个页面现在是空闲的,分配器很可能会将页面返回给操作系统。
这不是一个简单的问题。 操作系统分配器部分是完全不受控制的。 我推荐你阅读Doug Lea的Malloc(DLMalloc)这样的东西来理解一个相当快的分配器是如何工作的。
编辑:你的崩溃将是由于写入大于你已经覆盖下一个内存头的分配。 这种方式,当它释放它非常困惑,到底什么是free'ing和如何合并到下面的块。 这可能并不总是直接导致自由的崩溃。 稍后可能会导致崩溃。 一般避免内存覆盖!
你的程序崩溃,因为它使用了不属于你的内存。 它可能被其他人使用 – 如果你幸运的话,你会崩溃,如果不是的话,问题可能会隐藏很长时间,然后再回来咬你。
就malloc / free实现而言 – 整本书都专注于这个话题。 基本上分配器会从操作系统获得更大的内存块,并为您pipe理它们。 分配器必须解决的一些问题是:
- 如何获得新的记忆
- 如何存储它 – (列表或其他结构,不同大小的内存块的多个列表等等)
- 如果用户请求的内存比当前可用的要多(从操作系统请求更多的内存,join一些现有的块,如何精确地join它们)
- 当用户释放内存时该怎么做
- debugging分配器可能会给你更大的块,你请求并填充它的一些字节模式,当你释放内存分配器可以检查是否写在块外(这可能发生在你的情况)…
很难说,因为不同编译器/运行时间的实际行为是不同的。 即使debugging/发布版本也有不同的行为。 VS2005的debugging版本将在分配之间插入标记以检测内存损坏,所以不是崩溃,而是在free()中声明。
认识到简单地用brk
和sbrk
移动程序中断指针实际上并不分配内存也是很重要的,它只是设置地址空间。 例如,在Linux上,访问该地址范围时,内存将由实际的物理页面“支持”,这将导致页面错误,最终导致内核调用进入页面分配器以获得后台页面。