我想知道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/free的一个实现如下所示:

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


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


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


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

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

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

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


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

Under windows for example a process can ask for a page or more of RAM. The OS then assigns those pages to the process. This is not, however, memory allocated to your application. The CRT memory allocator will mark the memory as a contiguous "available" block. The CRT memory allocator will then run through the list of free blocks and find the smallest possible block that it can use. It will then take as much of that block as it needs and add it to an "allocated" list. Attached to the head of the actual memory allocation will be a header. This header will contain various bit of information (it could, for example, contain the next and previous allocated blocks to form a linked list. It will most probably contain the size of the allocation).

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

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

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


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

The call to free may or may not crash. The memory "after" the 4 bytes of your allocation might be used for something else by your C or C++ implementation. If it is used for something else, then scribbling all over it will cause that "something else" to go wrong, but if it isn't used for anything else, then you could happen to get away with it. "Getting away with it" might sound good, but is actually bad, since it means your code will appear to run OK, but on a future run you might not get away with it.

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

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

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


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

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


How malloc() and free() works depends on the runtime library used. Generally, malloc() allocates a heap (a block of memory) from the operating system. Each request to malloc() then allocates a small chunk of this memory be returning a pointer to the caller. The memory allocation routines will have to store some extra information about the block of memory allocated to be able to keep track of used and free memory on the heap. This information is often stored in a few bytes just before the pointer returned by malloc() and it can be a linked list of memory blocks.

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

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


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

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

How to get new memory How to store it - ( list or other structure, multiple lists for memory chunks of different size, and so on ) What to do if the user requests more memory than currently available ( request more memory from OS, join some of the existing blocks, how to join them exactly, ... ) What to do when the user frees memory Debug allocators may give you bigger chunk that you requested and fill it some byte pattern, when you free the memory the allocator can check if wrote outside of the block ( which is probably happening in your case) ...


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

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

In many malloc/free implementations, free does normally not return the memory to the operating system (or at least only in rare cases). The reason is that you will get gaps in your heap and thus it can happen, that you just finish off your 2 or 4 GB of virtual memory with gaps. This should be avoided, since as soon as the virtual memory is finished, you will be in really big trouble. The other reason is, that the OS can only handle memory chunks that are of a specific size and alignment. To be specific: Normally the OS can only handle blocks that the virtual memory manager can handle (most often multiples of 512 bytes e.g. 4KB).

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

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

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

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

为什么你的代码崩溃:

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

This is a rather graceful behaviour. I have also seen situations where a runaway pointer somewhere has overwritten data in the memory-free-list and the system did not immediately crash but some subroutines later. Even in a system of medium complexity such problems can be really, really hard to debug! In the one case I was involved, it took us (a larger group of developers) several days to find the reason of the crash -- since it was in a totally different location than the one indicated by the memory dump. It is like a time-bomb. You know, your next "free" or "malloc" will crash, but you don't know why!

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


正如aluser在这个论坛上说的:

Your process has a region of memory, from address x to address y, called the heap. All your malloc'd data lives in this area. malloc() keeps some data structure, let's say a list, of all the free chunks of space in the heap. When you call malloc, it looks through the list for a chunk that's big enough for you, returns a pointer to it, and records the fact that it's not free any more as well as how big it is. When you call free() with the same pointer, free() looks up how big that chunk is and adds it back into the list of free chunks(). If you call malloc() and it can't find any large enough chunk in the heap, it uses the brk() syscall to grow the heap, i.e. increase address y and cause all the addresses between the old y and the new y to be valid memory. brk() must be a syscall; there is no way to do the same thing entirely from userspace.

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

malloc()和free()并不是在每个O/S上都以相同的方式工作。


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

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

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

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

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

So your block is there, sitting on the free list. You can almost always access it and nearby memory just as if it were still allocated. C compiles straight to machine code and without special debugging arrangements there are no sanity checks on loads and stores. Now, if you try and access a free block, the behavior is undefined by the standard in order to not make unreasonable demands on library implementators. If you try and access freed memory or meory outside an allocated block, there are various things that can go wrong:

Sometimes allocators maintain separate blocks of memory, sometimes they use a header they allocate just before or after (a "footer", I guess) your block, but they just might want to use memory within the block for the purpose of keeping the free list linked together. If so, your reading the block is OK, but its contents may change, and writing to the block would be likely to cause the allocator to misbehave or crash. Naturally, your block may be allocated in the future, and then it is likely to be overwritten by your code or a library routine, or with zeroes by calloc(). If the block is reallocated, it may also have its size changed, in which case yet more links or initialization will be written in various places. Obviously you may reference so far out of range that you cross a boundary of one of your program's kernel-known segments, and in this one case you will trap.

操作原理

So, working backwards from your example to the overall theory, malloc(3) gets memory from the kernel when it needs it, and typically in units of pages. These pages are divided or consolidated as the program requires. Malloc and free cooperate to maintain a directory. They coalesce adjacent free blocks when possible in order to be able to provide large blocks. The directory may or may not involve using the memory in freed blocks to form a linked list. (The alternative is a bit more shared-memory and paging-friendly, and it involves allocating memory specifically for the directory.) Malloc and free have little if any ability to enforce access to individual blocks even when special and optional debugging code is compiled into the program.


1. The fact that very few implementations of free() attempt to return memory to the system is not necessarily due to the implementors slacking off. Interacting with the kernel is much slower than simply executing library code, and the benefit would be small. Most programs have a steady-state or increasing memory footprint, so the time spent analyzing the heap looking for returnable memory would be completely wasted. Other reasons include the fact that internal fragmentation makes page-aligned blocks unlikely to exist, and it's likely that returning a block would fragment blocks to either side. Finally, the few programs that do return large amounts of memory are likely to bypass malloc() and simply allocate and free pages anyway.


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