我认为您误读了手册页。假设您正在谈论 Linux 手册页,它声称(正确地)explicit_bzero 和 memset_explicit 和 memset_s 比 memset 和 bzero 更安全(出于某些目的)。它没有声称memset 和bzero 之间存在任何安全差异。不推荐使用bzero 的原因是它是memset 的简单包装器,并且所有¹ C 实现都有memset,因此程序员不妨使用memset。
memset/bzero 和explicit_/_s 变体之间的区别在于禁止编译器优化显式变体。这使得显式变体适用于清理机密数据。例如,考虑以下程序 sn-p:
bzero(password, password_length);
free(password);
只有bzero 或memset,许多现代编译器会看到“哦,你正在写入内存然后释放它。没有办法回读bzero 刚刚写的内容,所以调用bzero 相当于什么都不做。什么都不做比调用bzero 更快,因此我不会为bzero 调用生成任何代码。”
编译器推理的缺陷在于,将内存归零的原因不是程序的已定义行为,而是在后续未定义或未指定行为的情况下会发生什么。从 C 编译器的角度来看,未定义的行为意味着任何事情都可能发生。从安全工程师的角度来看,对于未定义行为(例如缓冲区溢出或 use-after-free)究竟会发生什么是非常重要的。同样,从未初始化的内存中读回的确切内容是未指定的,但对安全工程师来说很重要。安全工程师试图减少此类未定义或未指定行为的安全影响。
所以对于安全工程师来说,memset 的优化是不幸的。一位安全工程师想要保证当内存被释放时,its former contents will not leak out, even, say, due to a buffer overflow。因此explicit_bzero:编译器被指示在此函数返回时将目标内存的内容视为可观察的,因此不允许它们基于程序没有从它读取回优化调用。从语义上讲,explicit_bzero(buffer, length) 等价于
bzero(buffer, length);
for (size_t i = 0; i < length; i++) __observe__(buffer[i]);
其中__observe__ 无效,但仍取决于其参数的值。因此不允许编译器删除对bzero 的调用,因为这样__observe__ 就不会读回正确的值。
显式归零有局限性。手册页强调它不会清除寄存器中变量的副本,但这通常不是一个大问题,因为缓冲区溢出或从未初始化的内存读取最终导致寄存器值泄漏的情况很少见。实践中最大的限制是realloc。当您使用动态分配的内存时,realloc 可能会移动它,并且无法清除旧值。出于这个原因,如果缓冲区的内容是敏感的,则不能在其上使用realloc。
显式归零的另一个限制是它仅适用于程序级别,而不适用于系统级别。数据的副本可能保留在缓存、交换等中。将程序内的内存归零的目的是为了防止程序内的安全漏洞。它不能防止更大的系统妥协。
请注意,编写自己的explicit_bzero 是impossible to do portably。您能做的最好的事情就是让它与有限编译器的有限版本集一起工作,但不能保证下一个版本不会有一个更高级的优化器可以看穿您的尝试。这就是为什么 C11 将其作为标准函数添加为 memset_s。
¹ 差不多。技术上独立的实现不一定要有memset,但它是如此简单和有用的功能,大多数人都会这样做,而且它通常由编译器提供,因此即使在没有通常的 C 运行时构建时也可以使用。