free() 是不是将内存清零?
Posted
技术标签:
【中文标题】free() 是不是将内存清零?【英文标题】:Is free() zeroing out memory?free() 是否将内存清零? 【发布时间】:2015-08-21 09:45:55 【问题描述】:直到今天,我一直相信在内存空间上调用 free()
会释放它以供进一步分配而无需进行任何其他修改。特别是考虑到this SO question 明确指出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)
【问题讨论】:
当内存返回分配系统时,它可以用于系统喜欢的任何目的。它可能将控制信息存储在内存空间中,修改返回的内容。分配器没有限制;他们既不需要修改也不需要保持返回给他们的内存不变。对已释放内存的任何访问都是无效的。 对于它的价值,你实际上是在测试同样的东西,因为free
是 C 库的一部分,gcc
和 clang
在你的系统上使用 glibc
。尝试分配一大块内存而不是 8 个字节,比如 16 MB,看看取消引用释放的内存是否会崩溃。
您看到这种特定行为的原因完全有可能与动态内存库的元数据管理有关。许多使用未分配块的前几个字节来跟踪大小、使用中和前后指针。有可能在释放的过程中,它以某种方式修改了数据,从而产生了这种行为作为副作用,因为在释放内存后您没有业务取消引用内存。 :)
@browning0:嗯,正如我在回答中所说,是的,这就是调试实现通常会做的事情。但这仅适用于调试实现。而被释放块的开始通常用于完全不同的家庭用途。顺便说一句,在您的示例中,您正在专门检查块的开头,这并不能很好地表明块的其余部分会发生什么。
另外请注意,如果在调用 free 之后,您的分配器决定删除虚拟页面,当它稍后再次映射它们时,内核(在现代系统中)将在出现故障时将它们擦除干净(或者归零或随机化),因为读取另一个进程丢弃的内存页是安全故障。因此,实际上发生了很多事情,出于所有意图和目的,内存缓冲区的内容在释放后变得不确定。
【参考方案1】:
free()
通常不会将内存归零。它只是将其释放以供将来对malloc()
的调用重新使用。某些实现可能会用已知值填充内存,但这纯粹是库的实现细节。
Microsoft 的运行时充分利用了用有用值标记释放和分配的内存(有关更多信息,请参阅In Visual Studio C++, what are the memory allocation representations?)。我还看到它充满了在执行时会导致明确定义的陷阱的值。
【讨论】:
类似的情况发生在文件系统上,它们只是将空间标记为可重用,但在某些实现中,它们可能会在顶部写入默认值【参考方案2】:有更好的解释吗?
有。在指针为free()
d 之后取消引用它会导致未定义的行为,因此实现有权做任何它喜欢的事情,包括诱使您相信内存区域已被零填充的行为。
【讨论】:
严格意义上来说不只是编译器,而是实现(包括编译器和标准C库) 这是 GCC 或 Clang 的记录行为吗?在文档中找不到任何东西(甚至不知道我应该搜索什么)。如果出于调试目的,我想在释放内存之前用垃圾填充内存怎么办?以这种方式可以更容易地跟踪对充满垃圾的 free()d 内存的引用。 @BasileStarynkevitch 你是对的,改变了文字以反映这一事实。 @browning0: 你不能引用 free(d) 内存。未定义的行为意味着程序可以因段错误而停止。 @browning0 您可能想要使用Valgrind 或AddressSanitizer 等工具。但是,这些无法检测到所有可能的损坏。如果你很幸运,你的程序会出现段错误 - 如果不是,它将变得难以跟踪 Haisenbugs。【参考方案3】:free()
其实可以把内存还给操作系统,让进程变小。通常,它所能做的就是允许稍后调用 malloc 重用空间。同时,该空间作为malloc
内部使用的空闲列表的一部分保留在您的程序中。
【讨论】:
【参考方案4】:这里还有一个你可能不知道的陷阱:
free(pointer);
printf("After free(): %p \n", pointer);
即使只是readingpointer
的值在你free
之后也是未定义的行为,因为指针变得不确定。
当然也不允许取消引用已释放的指针(如下例所示):
free(pointer);
printf("After free(): %p, %d\n", pointer, *pointer);
ps。通常,当使用%p
(如printf
)打印地址时,将其转换为(void*)
,例如(void*)pointer
- 否则你也会得到未定义的行为
【讨论】:
读取指针值不是未定义的行为,只有解引用才是。free
只是一个函数,C语言不提供改变传值参数的能力。
@harper:未定义请参阅我链接的问题中的答案。
@harper:即使指针的值没有改变,free()
也会将该值从有效变为不确定。
值得一提的是printf("%p\n", pointer);
具有未定义的行为,因为%p
需要void*
类型的参数,而pointer
是int*
类型的参数。这就是为什么推荐使用void*
的原因。 (在大多数实现中,void*
和 int*
具有相同的表示形式并以相同的方式作为参数传递,但这不能保证。)
你推荐了演员阵容。您没有提到省略它会导致未定义的行为。【参考方案5】:
free() 是否将内存清零?
没有。 glibc malloc implementation 可以覆盖多达四倍于内部管家数据的前用户数据指针大小。
详情:
以下是glibc的malloc_chunk
结构(见here):
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 *)
字节可能会被覆盖,因此与之前的用户数据值不同的值被打印出来。这是未定义的行为。如果分配的块较大,则可能存在分段错误。
【讨论】:
我不明白这是如何回答这个问题的。 @black、fd
、bk
、fd_nextsize
和 bk_nextsize
用于维护空闲列表,并且仅在尚未将内存块分配给程序供使用。为了提高内存效率,当你调用malloc()
时,返回的指针是fd
的地址——内存块的用户可用部分与内存控制结构重叠。当您调用free()
时,内存块的前几个字节会被簿记数据覆盖,在问题的特定情况下,这会导致内存归零。【参考方案6】:
正如其他人指出的那样,您不能使用 free
d 指针做任何事情(否则那是可怕的 undefined behavior,您应该始终避免,请参阅 this) .
在实践中,我建议不要简单地编码
free(ptr);
但总是编码
free(ptr), ptr=NULL;
(因为实际上这有助于捕获一些错误,除了双重free
s)
如果之后不使用ptr
,编译器会跳过NULL
的赋值进行优化
实际上,编译器知道free
和malloc
(因为标准C 库头文件可能会使用适当的function attributes 声明这些标准函数-GCC 和Clang/LLVM 都理解)所以可能是能够优化代码(根据标准规范malloc
& free
.... 987654326@ 或 Linux 上的 musl-libc),因此实际行为由您的 libc
(而不是编译器本身)提供。阅读相应的文档,尤其是 free(3) 手册页。
顺便说一句,在 Linux 上,glibc
和 musl-libc
都是免费软件,因此您可以研究它们的源代码以了解它们的行为。他们有时会使用像mmap(2) 这样的系统调用从内核获取虚拟内存(然后使用munmap(2) 将内存释放回内核),但他们通常会尝试重用以前的free
d 内存以供将来使用malloc
s
在实践中,free
可以 munmap
您的内存(尤其是对于 大 内存 malloc
-ated 区域) - 然后您将获得 SIGSEGV
如果您敢于取消引用(稍后)free
d 指针,但通常(特别是对于 small 内存区域)它只会设法稍后重用该区域。确切的行为是特定于实现的。通常free
不清除或写入刚刚释放的区域。
您甚至可以重新定义(即重新实现)您自己的 malloc
和 free
,也许通过链接一个特殊的库,例如 libtcmalloc,只要您的实现具有与 C99 或 C11 兼容的行为标准说。
在 Linux 上,禁用内存过度使用并使用 valgrind。使用gcc -Wall -Wextra
编译(调试时可能使用-g
;您也可以考虑将-fsanitize=address
传递给最近的gcc
或clang
,至少以寻找一些顽皮的错误。)。
顺便说一句,有时 Boehm's conservative garbage collector 可能有用;您将(在整个程序中)使用GC_MALLOC
而不是malloc
,并且您不会关心free
-ing 内存。
【讨论】:
"实际行为是由你的 libc 提供的"并不完全正确,因为当printf
ing 在free
之后,编译器可以按原样处理这个 UB。
这是我在说编译器可能能够优化代码时所说的
我指的不是优化;相反,它可以 - 虽然很奇怪 - 将 0
分配给该位置。
@BasileStarynkevitch 不会在每次 free() 调用后将指针设置为 NULL 隐藏双重释放错误,这可能会揭示应用程序的一些逻辑不一致?因此,将指针设置为 NULL 既能获得也能放松,对吗?
@browning0: 如果你释放了被赋值为 NULL 的指针,这是定义的行为。【参考方案7】:
您的问题没有一个明确的答案。
首先,释放块的外部行为将取决于它是释放到系统还是作为空闲块存储在进程的内部内存池或 C 运行时库中。在现代操作系统中,您的程序将无法访问“返回给系统”的内存,这意味着它是否被清零的问题没有实际意义。(其余适用于内部内存池中保留的块。)
其次,用任何特定值填充已释放的内存几乎没有意义(因为您不应该访问它),而这种操作的性能成本可能相当可观。这就是为什么大多数实现不做任何事情来释放内存的原因。
1234563一些预先确定的值或模式。 (零,顺便说一句,对于这种值来说,零并不是最佳选择。0xDEADBABE
模式更有意义。)但同样,这仅在库的调试版本中完成,性能影响不是问题。
第四,许多(大多数)流行的堆内存管理实现将使用释放块的一部分用于其内部目的,即在那里存储一些有意义的值。这意味着该块的区域被free
修改。但一般不会“归零”。
当然,所有这些都严重依赖于实现。
一般来说,您最初的想法是完全正确的:在代码的发布版本中,释放的内存块不会受到任何块范围的修改。
【讨论】:
这个答案是正确的,如果 'free' 是否为零是未定义的。例如,在 Windows 上,许多编译器运行时中的“malloc”和“free”已经并且有时仍然是直接使用 Windows API 堆函数实现的,这些函数会将返回的用户内存归零作为操作系统策略的一部分,并且许多调试运行时会清除,填充或以其他方式标记释放的内存以测试错误代码。0xDEADBEEF
更常见。 0xDEADBABE
不欢迎业内女性。
@browning0:虽然我同意那些内存填充调试技术并不总是有帮助,但实际上我发现它比你更有帮助。当您在调试器窗口中看到它们时,不仅相当明显,而且通过触摸块中内存的每个字,放置一个在该内存块被释放时准确停止的观察点也变得微不足道。我发现这样做的能力在查明错误方面非常有效,而不仅仅是在我的代码第一次使用无效指针破坏数据时才看到。
@Celess:当我们从 C 标准库端来看这个时,堆内存管理通常是相当多层的。这就是为什么无法用单一 API 来描述行为的原因,比如操作系统策略。
@AnT 如果这就是您的意思,我不确定我是否试图从任何单一环境的角度来描述零行为作为一个整体。相反,我指出,作为一个实际问题,行为是未定义的,实际上很容易最终被归零,并且这样说是为了支持上述答案。我很确定结果在任何规范中都是未定义的。此外,并非所有运行时实现都是非常“多层”的,而且许多只是我用作示例的底层 OS API 的薄垫片。以上是关于free() 是不是将内存清零?的主要内容,如果未能解决你的问题,请参考以下文章
调用 free 或 delete 是不是曾经将内存释放回“系统”