为什么free()在释放内存前不将内存清零?

13
当我们在C语言中使用free()函数释放内存时,为什么这部分内存没有填充为零?是否有一种好的方法可以确保调用free()时自动填充为零?
我不想冒险将敏感数据留在释放回操作系统的内存中...

3
你可能想要多输入一些内容(请参见“编辑”按钮?)。解释一下你所指的“更多”的具体含义,不要预设我们了解你从哪里来。代码示例可能有所帮助。解释为什么想要实现这个目标可能会有所帮助。 - dmckee --- ex-moderator kitten
1
好吧,我很想就此关闭并继续前进,但如果他实际上是想要询问他所“问”的内容......并且sharptooth的猜测是正确的,我已经编辑了问题以更好地匹配。愿上帝怜悯我的灵魂…… - Shog9
我看不出关闭它的任何理由。无论他问了哪个问题,他显然很困惑,而且两个问题都有好的答案。如果他能回来澄清一下就好了,但我不太相信那会发生。 - Chris Lutz
17个回答

29

在释放内存块时将其清零会需要额外的时间。由于大多数情况下实际上并不需要,因此默认情况下不执行此操作。

如果您真的需要(比如您使用内存来存储密码或加密密钥),在释放块之前调用 memset()。编写一个将 memset()free() 链起来的实用函数也不是问题。


21
实际上,memset并不总是足够的。memset通常是编译器内置的函数,如果编译器确定您不会再次使用数据,则可以将其删除(对于调用free()可能不会发生,但对于栈上缓冲区则完全有可能)。Windows提供了SecureZeroMemory函数,这个函数不会被优化删除。更多信息请参考这里:https://www.securecoding.cert.org/confluence/display/cplusplus/MSC06-CPP.+Be+aware+of+compiler+optimization+when+dealing+with+sensitive+data - Michael

10

free()函数并不会将内存释放回操作系统,它将其释放回进程的堆管理器。为了提高效率,内存不会被清零。

当进程分配虚拟内存时,大多数操作系统会提供一个零页。这可以防止内存从一个进程“泄漏”到另一个进程,并引起像您提到的安全问题。

如果您在进程中有一些数据不想保留在内存中(例如用户密码),则需要自行将其清零。Windows提供了SecureZeroMemory API来实现这个功能。


我会将“出于效率考虑,它不会被清零”替换为“释放后清零会引入性能开销,但某些堆管理实现出于安全原因或存储堆元数据的结果而这样做”。 - Nino Filiu

7
memset(ptr, 0, size);
free(ptr);

我认为您需要这个...


1
将内存设置为0然后释放它的目的是什么? - Avi
11
使用SecureZeroMemory可以对用于存储密码和加密密钥的缓冲区有所帮助。https://dev59.com/4UfRa4cB1Zd3GeqP8FWx - sharptooth

7
为了速度。因为在释放内存后,我们会在设置为零之前将其设置为零。嗯?

有趣的是,MacOS现在可以将内存清零,这样会更快。真是令人惊讶。 - Валерий Заподовников

6

如果你指的是像其他人假设的那样,将释放的内存块的每个字节都设置为0,那么你不能在释放内存块之后这样做。试图这样做会导致未定义的行为。因此,如果你这样做了,那么你对内存分配的理解存在严重误解。

但我猜当你说“我们在释放后将其设置为零”时,你可能是在谈论以下代码:

free(ptr);
ptr = NULL;

如果是这样的话,free不能将ptr设置为NULL的原因是,free只接收来自变量ptr的值。它无法修改ptr,因为您没有将变量ptr本身传递给free。这是C语言设计的一部分 - 当您调用一个传递值的函数时,被调用者无法知道该值是如何计算的,或者在调用者的代码中哪个变量可能包含它。即使可能,为了free而对此语言规则作出例外也是不可取的。
无论如何,并非所有人都会在释放指针后将其清零。有些人认为这是一项良好的安全措施,而其他人则认为不是。但是,无论您如何看待它,该代码都不会清空内存,而只会将指向该内存的指针清零。如果您想编写一个函数来为您清除指针,那么可以:
void free_and_clear(void **pptr) {
    free(*pptr);
    *pptr = NULL;
}

然后像这样使用它:
free_and_clear(&ptr);

请注意,这里传递的是指向变量ptr的指针,而不是ptr的值。因此,free_and_clear可以修改ptr。但这会对使用它的方式施加一些限制,这些限制不适用于free - 您需要一个可修改值的指针,而不仅仅是一个值。

1
你可以写一个宏:#define FREE(x) do { free(x); x = NULL; } while(0);这样,你就可以在不使用“&”运算符的情况下调用它。 - Chris Lutz
1
虽然我害怕宏会对其参数进行两次求值(因为我担心有一天我会在不考虑它们的情况下使用它们),但这是真的。在这种情况下,参数必须是一个左值,这降低了它具有副作用的机会,但仍然存在风险。你可能可以通过临时变量来解决问题,但一旦宏有了临时变量,我总是在想“请,一定有某种方法将其变成静态内联函数”。我认为要求调用者传递指针并不是那么糟糕。 - Steve Jessop
1
@Chris Lutz:非常相关:https://dev59.com/PnM_5IYBdhLWcg3wslfs - moala

6

最初的C语言设计理念是尽可能减少隐式效应。如果程序员希望在指向的内存被释放后将指针清零,那么这就是程序员需要编写的代码。我们中的一些人通常会使用像这样的宏:

#define FREE(P) ((void)(free((P)), (P) = NULL))

当然,如果传递给 FREE 的表达式具有副作用,那么就会引发一系列问题...

请你能否给我们这些凡人举一个副作用的例子? - mLstudent33
对于宏定义而言,在释放指针后将其设置为NULL并不能解决OP的安全问题。指针所指向的内存仍然包含原始数据,即使该内存现在可以重新分配且不再有指针指向它。 - 6equj5

6
如果您希望在释放内存时将其设置为0,则需要在free()之前自行进行操作。如果您尝试在free()之后进行操作,则不能保证它没有被重新分配。例如,您可以使用memset()进行操作。 free()不能保证清除内存,因为C语言不保证malloc()会返回初始化的内存。无论如何,在分配内存后都必须自己进行初始化,因此在free()时清除它是没有意义的。

最近在代码审查中,有人问我:既然你在 free 后不会再访问内存,我们怎么知道编译器不会优化掉 memset() 函数? - Edward Falk

3

C最初是作为系统实现语言而设计的,因此C操作通常尽可能快速和接近底层。设计哲学中的一个关键点是,您可以将多个快速操作合并为一个较慢且更安全的操作,但您无法将较慢和更安全的操作合并为更快的操作。

如果您想要一个零成本函数,可以编写一个并使用它代替free()。如果您关心安全性,我建议这样做。


2
“为什么释放内存后内存没有被设置为0”的非常具体的答案是“因为语言规范没有定义这种行为”。根据 ANSI C 的草案规范:“free 函数导致由 ptr 指向的空间被释放,也就是说,可以进一步分配该空间。”

2

将已释放指针的结果设置为零可能看起来像胡说八道,但是如果稍后无意中访问该指针,则会收到段错误(至少在真实的操作系统中),并且调试器将指出发生这种可怕情况的位置。但正如其他人所指出的,当您稍后调用“free”时,所有“free”都有要释放的地址,没有其他内容。


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