【发布时间】:2016-11-08 21:49:33
【问题描述】:
我需要一个函数(例如 WinAPI 中的 SecureZeroMemory)始终将内存归零并且不会被优化掉,即使编译器认为此后再也不会访问内存。似乎是 volatile 的完美候选者。但我实际上在让这个与 GCC 一起工作时遇到了一些问题。这是一个示例函数:
void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;
while (size--)
{
*bytePtr++ = 0;
}
}
足够简单。但是如果你调用它,GCC 实际生成的代码会随着编译器版本和你实际尝试归零的字节数而变化很大。 https://godbolt.org/g/cMaQm2
- GCC 4.4.7 和 4.5.3 永远不会忽略 volatile。
- GCC 4.6.4 和 4.7.3 忽略数组大小为 1、2 和 4 的 volatile。
- GCC 4.8.1 直到 4.9.2 忽略数组大小 1 和 2 的 volatile。
- GCC 5.1 到 5.3 忽略数组大小为 1、2、4、8 的 volatile。
- GCC 6.1 对于任何数组大小都会忽略它(为一致性加分)。
我测试过的任何其他编译器(clang、icc、vc)都会生成人们期望的存储,具有任何编译器版本和任何数组大小。所以在这一点上我想知道,这是一个(相当古老和严重的?)GCC编译器错误,还是标准中对 volatile 的定义不准确,这实际上是符合行为的,使得基本上不可能编写一个可移植的“ SecureZeroMemory”功能?
编辑:一些有趣的观察。
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <atomic>
void callMeMaybe(char* buf);
void volatileZeroMemory(volatile void* ptr, std::size_t size)
{
for (auto bytePtr = static_cast<volatile std::uint8_t*>(ptr); size-- > 0; )
{
*bytePtr++ = 0;
}
//std::atomic_thread_fence(std::memory_order_release);
}
std::size_t foo()
{
char arr[8];
callMeMaybe(arr);
volatileZeroMemory(arr, sizeof arr);
return sizeof arr;
}
The possible write from callMeMaybe() will make all GCC versions except 6.1 generate the expected stores. 在内存栅栏中进行注释也会使 GCC 6.1 生成存储,尽管仅结合可能来自 callMeMaybe() 的写入。
有人还建议刷新缓存。 Microsoft does not try to flush the cache at all in "SecureZeroMemory". 无论如何,缓存很可能很快就会失效,所以这可能没什么大不了的。此外,如果另一个程序正在尝试探测数据,或者将其写入页面文件,则它始终是归零版本。
还有一些关于 GCC 6.1 在独立函数中使用 memset() 的问题。 Godbolt 上的 GCC 6.1 编译器可能会损坏构建,因为 GCC 6.1 似乎为某些人的独立功能生成了一个正常的循环(就像 Godbolt 上的 5.3 一样)。 (阅读 zwol 的答案。)
【问题讨论】:
-
恕我直言,使用
volatile是一个错误,除非另有证明。但很可能是一个错误。volatile指定不足以至于很危险 - 只是不要使用它。 -
@JesperJuhl:不,
volatile适合这种情况。 -
@NathanOliver:那行不通,因为编译器可以优化死存储,即使他们使用
memset。问题是编译器确切地知道memset做了什么。 -
@PaulStelian:这将产生一个
volatile指针,我们想要一个指向volatile的指针(我们不关心++是否严格,但*p = 0是否严格)。 -
@JesperJuhl:没有任何关于 volatile 的详细说明。