free()函数是否会清空内存?

39

直到今天,我一直相信在内存空间上调用free()会释放该空间以供进一步分配,而不需要进行其他修改。特别是考虑到这个SO问题已经明确说明free()不会清空内存。

然而,让我们来看看这段代码(test.c):

#include<stdlib.h>
#include<stdio.h>

int main()
{
    int* pointer;

    if (NULL == (pointer = malloc(sizeof(*pointer))))
        return EXIT_FAILURE;

    *pointer = 1337;

    printf("Before free(): %p, %d\n", pointer, *pointer);

    free(pointer);

    printf("After free(): %p, %d\n", pointer, *pointer);

    return EXIT_SUCCESS;
}

编译(包括GCC和Clang):

gcc test.c -o test_gcc
clang test.c -o test_clang

结果:

$ ./test_gcc 
Before free(): 0x719010, 1337
After free(): 0x719010, 0
$ ./test_clang
Before free: 0x19d2010, 1337
After free: 0x19d2010, 0

为什么会这样?我一直生活在谎言中,还是我误解了一些基本概念?或者有更好的解释吗?

一些技术信息:

Linux 4.0.1-1-ARCH x86_64
gcc version 4.9.2 20150304 (prerelease) (GCC)
clang version 3.6.0 (tags/RELEASE_360/final)

10
当内存归还给分配系统时,它可能被系统用于任何目的。它可能在内存空间中存储控制信息,修改原来的内容。分配器没有限制;它们既不必须修改也不必须保持未更改的已归还内存。对已释放内存的任何访问都是无效的。 - Jonathan Leffler
8
就目前而言,你实际上正在测试相同的东西,因为free是C库的一部分,gccclang都在你的系统上使用glibc。尝试分配一个大块的内存,而不是8字节,比如16MB,并查看是否解引用已释放的内存会导致崩溃。 - chqrlie
4
你看到的这种特定行为可能与动态内存库的元数据管理有关。许多库使用未分配块的前几个字节来跟踪大小、使用情况以及前后指针。在释放过程中,它有可能以某种方式修改数据,从而导致这种行为作为副作用出现,因为你释放内存后不应再访问它。 :) - David Hoelzer
3
好的,根据您提供的内容翻译如下:正如我在回答中所述,是的,这通常是调试实现的做法。但这只适用于调试实现。而释放块的开头通常用于完全不同的家庭目的。顺便说一句,在您的示例中,您特别检查了块的开头,这并不能很好地说明块的其余部分发生了什么。 - AnT stands with Russia
2
请注意,如果在调用free之后,您的分配器决定丢弃虚拟页面,则在以后重新映射它们时,内核(在现代系统中)将在故障时将它们清除(零化或随机化),因为读取另一个进程的已丢弃内存页面是一种安全失败。因此,在释放内存缓冲区后,实际上发生了很多事情,其内容变得不确定。 - Thomas
显示剩余4条评论
7个回答

27

关于您的问题,没有一个确定的答案。

  • 首先,释放块的外部行为取决于它是释放到系统还是存储在进程或C运行时库的内部内存池中的自由块。在现代操作系统中,“返回给系统”的内存将变得对程序不可访问,这意味着是否清零是无关紧要的。

(其余内容仅适用于保留在内部内存池中的块。)

  • 其次,在释放的内存中填充任何特定值没有太多意义(因为您不应该访问它),而这种操作的性能成本可能相当大。这就是为什么大多数实现不会对释放的内存做任何事情。

  • 第三,在调试阶段,在释放的内存中填充某些预定的垃圾值可以有助于捕获错误(例如访问已释放的内存),这就是为什么许多标准库的调试实现将释放的内存填充为某些预定的值或模式。 (顺便说一句,零不是这种值的最佳选择。类似于0xDEADBABE模式更有意义)。但是,这只在库的调试版本中完成,性能影响不是问题。

  • 第四,许多(大多数)流行的堆内存管理实现将使用释放块的一部分进行其内部用途,即在那里存储一些有意义的值。这意味着该块的该区域会被free修改。但通常它不会“清零”。

当然,所有这些都严重取决于实现。

总之,在代码发布版本中,已释放的内存块不会受到任何块级修改。


这个答案是正确的,如果'free'会不会清零是未定义的。例如,在Windows上,许多编译器运行时中的'malloc'和'free'直接使用Windows API Heap函数实现,作为操作系统策略的一部分将清零返回的用户内存,并且许多调试运行时会清除、填充或以其他方式标记释放的内存,以测试坏代码。 - Beeeaaar
10
0xDEADBEEF更为常见。0xDEADBABE不适用于行业中的女性。 - Lightness Races in Orbit
@browning0:虽然我同意那些填充内存的调试技巧并不总是有用,但实际上我发现它比你更有帮助。不仅当你在调试器窗口中看到它们时很明显,而且通过触碰块中每个内存单元,也使得轻松设置监视点,以便在该内存块被释放时精确停止。我发现这种能力非常有效,可以准确定位错误,而不仅仅是看到我的代码第一次使用无效指针破坏数据的情况。 - Cort Ammon
@Celess:当我们从C标准库的角度来看待这个问题时,堆内存管理通常是相当多层次的。这就是为什么不可能用单一的API(如操作系统策略)来描述其行为的原因。 - AnT stands with Russia
@AnT 我不确定我是在试图描述零行为作为整体在任何单一环境的术语中,如果这是你的意思。相反,我指出作为实际问题,行为是未定义的,实际上很容易被清零,并且在支持上面的答案时这样说。我相当确定结果在任何规范中都是未定义的。此外,并非所有运行时实现都非常“多层”,许多只是底层操作系统API的薄壳,我正是以此为例。 - Beeeaaar

18

free() 通常不会将内存清零,它只是释放该内存以便于以后使用 malloc() 来重新分配。某些实现可能会将内存填充为已知值,但这纯粹是库的实现细节。

Microsoft 的运行时很好地利用了有用值标记已释放和已分配的内存(有关更多信息,请参见在 Visual Studio C++ 中,什么是内存分配表示?)。我还看到过将内存填充为会导致明确定义陷阱的值的情况。


3
文件系统也存在类似的情况,它们只是将空间标记为可重用,但在某些实现中,它们可能会在其上写入默认值。 - Nikos M.
最新的MacOS系统。 - Валерий Заподовников

16

有更好的解释吗?

有的。在使用free()释放指针后对其进行解引用会导致未定义的行为,因此实现有权执行任何操作,包括欺骗您相信内存区域已被填充为零。


@BasileStarynkevitch 你是对的,我已经更改了文字以反映这个事实。 - The Paramagnetic Croissant
1
@browning0:你不能引用已经释放的内存。未定义的行为意味着程序可能会因为段错误而停止。 - chqrlie
2
@browning0 你可能想要使用一些工具,比如Valgrind或者AddressSanitizer。然而,这些工具并不能检测到所有可能的损坏。如果你很幸运,你的程序会崩溃 - 如果你不幸的话,它将变成难以追踪的Haisenbugs - Maciej Piechotka
@Michael Kjörling:我同意任何事情都有可能发生。但从统计学角度来看,很可能什么都不会发生,如果释放的块很大,出现分段错误的可能性很高,几乎没有可能会引发税务审计,更不可能通过时间旅行来避免后者。 - chqrlie
@chqrlie,你会惊讶地发现一些流行的积极优化编译器在处理包含未定义行为代码时会做出非常不直观的操作... - The Paramagnetic Croissant
显示剩余2条评论

9

你可能并不知道另一个陷阱,就在这里:

free(pointer);

printf("After free(): %p \n", pointer); 

即使只是在 阅读 完成 free 操作后的指针值,也会导致未定义的行为,因为指针变得不确定。

当然,像下面的示例中那样对已释放的指针进行解引用也是不允许的:

free(pointer);

printf("After free(): %p, %d\n", pointer, *pointer);

提示:通常在使用%p(例如在printf中)打印地址时,请将其转换为(void*),例如(void*)pointer - 否则会出现未定义的行为。


2
读取指针值并不是未定义行为,只有解引用才是。free只是一个函数,C语言不提供通过值传递来改变参数的能力。 - harper
@harper:它是未定义的,请参见我链接的问题中的答案。 - Giorgi Moniava
3
即使指针的值未改变,free()函数会把该值从有效变为不确定。 - Keith Thompson
1
值得一提的是,printf("%p\n", pointer); 具有未定义的行为,因为 %p 需要一个 void* 类型的参数,而 pointerint* 类型。这就是为什么建议将其转换为 void*。 (在大多数实现中,void*int* 具有相同的表示形式,并以相同的方式传递参数,但这并不保证。) - Keith Thompson
1
你提到了类型转换,但没有提到省略它会导致未定义的行为。 - Keith Thompson
显示剩余12条评论

9

free()会清零内存吗?

不会。 glibc malloc实现 可能会为了内部数据管理,覆盖前用户数据指针大小的四倍。

具体细节如下:

以下是glibc的malloc_chunk结构(参见 这里):

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

在分配的内存块中,用户数据的内存区域从size条目之后开始。调用free后,用户数据所在的内存空间可能被用于空闲内存块列表,因此前一个用户数据的4 * sizeof(struct malloc_chunk *)字节可能会被覆盖,因此打印出的值与前一个用户数据的值不同。这是未定义的行为。如果分配的块更大,则可能会出现分段错误。

我不明白这个回答如何解决问题。 - edmz
@black、fdbkfd_nextsizebk_nextsize用于维护空闲列表,仅在未为程序分配内存块时才需要。为了提高内存效率,当调用malloc()时,返回的指针是fd的地址——可供用户使用的内存块的一部分与内存控制结构重叠。当调用free()时,内存块的前几个字节被覆盖为簿记数据,在特定情况下,这会使内存看起来被清零。 - Mark

5

正如其他人指出的那样,您不允许对 free 指针进行任何操作(否则将会是可怕的 未定义行为,您应该始终避免,参见 this)。

实际上,我建议永远不要简单编码。

free(ptr);

但总是编程

free(ptr), ptr=NULL;

(实际上,这在很大程度上有助于捕获一些错误,除了双重free

如果ptr在此之后未使用,则编译器将通过跳过从NULL的分配来优化。

实际上,编译器知道关于freemalloc的信息(因为标准C库头文件可能会声明这些标准函数,并具有适当的function attributes(由GCCClang/LLVM都可以理解)),因此可能能够优化代码(根据mallocfree的标准规范.....),但是mallocfree的实现通常由您的C标准库提供(例如,在Linux上非常常见的是GNU glibcmusl-libc),因此实际行为由您的libc(而不是编译器本身)提供。请阅读适当的文档,特别是free(3)手册页。

顺便说一句,在Linux上,glibcmusl-libc都是自由软件,因此您可以研究它们的源代码以了解它们的行为。它们有时会使用类似mmap(2)的系统调用从内核获取虚拟内存(然后使用munmap(2)将内存释放回内核),但通常会尝试重复使用以前free的内存供未来的malloc使用。

实际上,free 可以释放你的内存(特别是对于 内存的 malloc 区域),然后如果你敢在之后引用已经被 free 的指针,你将会收到一个 SIGSEGV 错误,但通常情况下(特别是对于 内存区域),它会简单地在稍后重用该区域。确切的行为取决于具体的实现。通常情况下,free 不会清除或写入刚刚释放的区域。

你甚至可以重新定义(即重新实现)自己的 mallocfree,例如通过链接一个特殊的库,比如 libtcmalloc,只要你的实现与 C99 或 C11 标准所述的行为兼容即可。
在Linux上,禁用内存过度提交并使用valgrind。使用gcc -Wall -Wextra编译(调试时可能还需要-g;您可能还考虑将-fsanitize = address传递给最近的gccclang,至少可以查找一些淘气的错误)。
顺便说一句,Boehm的保守垃圾收集器有时可能很有用; 您将使用GC_MALLOC而不是malloc(在整个程序中),并且您不必担心释放内存。

“实际行为由您的libc提供”并不完全正确,因为在free后进行printf时,编译器可以将此未定义行为视为其所愿。 - edmz
这就是我说编译器可能能够优化代码时所说的。 - Basile Starynkevitch
我不是在提到优化,而是它可能会-虽然很奇怪-将0分配给那个位置。 - edmz
@BasileStarynkevitch,在每次free()调用后将指针设置为NULL,这样不会隐藏双重释放错误吗?这可能会揭示应用程序的某些逻辑不一致性。因此,将指针设置为NULL既有利也有弊,我是对的吗? - browning0
1
如果你释放了一个被赋值为NULL的指针,这是已定义的行为。 - Giorgi Moniava
显示剩余2条评论

2

free()函数实际上可以将内存返回给操作系统,使进程变得更小。通常情况下,free()只能让后续的malloc调用重用该空间。在此期间,该空间仍然作为malloc内部使用的空闲列表的一部分保留在程序中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接