“volatile”这个定义是不稳定的,还是GCC有一些标准的合规性问题?

时间:2016-07-06 18:07:38

标签: c++ c gcc standards

我需要一个函数(就像来自WinAPI的SecureZeroMemory)总是将内存归零并且不会被优化掉,即使编译器认为在此之后永远不会再访问内存。似乎是挥发性的完美候选者。但是我实际上遇到了一些与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".无论如何,缓存很可能会很快失效,所以这可能不是什么大问题。此外,如果另一个程序试图探测数据,或者它将被写入页面文件,它将始终是归零版本。

在独立功能中使用memset()也存在一些关于GCC 6.1的问题。 Godbolt上的GCC 6.1编译器可能是一个破坏的构建,因为GCC 6.1似乎为某些人的独立函数生成了一个正常的循环(就像在godbolt上的5.3一样)。 (阅读zwol答案的评论。)

4 个答案:

答案 0 :(得分:80)

海湾合作委员会的行为可能符合要求,即使不是,你也不应该依赖volatile在这些情况下做你想做的事。 C委员会为内存映射硬件寄存器和异常控制流程中修改的变量(例如信号处理程序和volatile)设计了setjmp这是唯一可靠的东西。作为一般用户使用是不安全的,不要优化这一点&#34;注释

特别是,关键点上的标准尚不清楚。 (我已经将你的代码转换为C; 不应该这里是C和C ++之间的任何分歧。我还手动完成了在可疑之前发生的内联优化,以显示编译器&#34;在那时看到的内容。)

extern void use_arr(void *, size_t);
void foo(void)
{
    char arr[8];
    use_arr(arr, sizeof arr);

    for (volatile char *p = (volatile char *)arr;
         p < (volatile char *)(arr + 8);
         p++)
      *p = 0;
}

内存清除循环通过volatile限定的左值访问arr,但arr本身声明volatile。因此,至少可以说C编译器可以推断出循环所做的存储是&#34;死&#34;,并完全删除循环。 C基本原理中的文字暗示委员会意味着要求保留这些商店,但标准本身实际上并没有像我读到的那样提出要求。

有关标准执行或不需要的内容的更多讨论,请参阅Why is a volatile local variable optimised differently from a volatile argument, and why does the optimiser generate a no-op loop from the latter?Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?GCC bug 71793

有关委员 volatile所针对的内容的更多信息,请在C99 Rationale中搜索单词&#34; volatile&#34;。 John Regehr的论文&#34; Volatiles are Miscompiled&#34;详细说明了生产编译器如何不满足程序员对volatile的期望。 LLVM团队的一系列论文&#34; What Every C Programmer Should Know About Undefined Behavior&#34;没有具体触及volatile,但会帮助您了解现代C编译器是如何以及为什么&#34;便携式汇编程序&#34;。

关于如何实现一个你希望volatileZeroMemory做什么的函数的实用问题:无论标准需要什么或者需要什么,最明智的做法是假设您无法使用volatile可以依赖的替代方案,因为如果它不起作用,它会打破太多其他东西:

extern void memory_optimization_fence(void *ptr, size_t size);
inline void
explicit_bzero(void *ptr, size_t size)
{
   memset(ptr, 0, size);
   memory_optimization_fence(ptr, size);
}

/* in a separate source file */
void memory_optimization_fence(void *unused1, size_t unused2) {}

但是,您必须确保memory_optimization_fence在任何情况下都不会内联。它必须位于自己的源文件中,并且不得进行链接时优化。

还有其他选项,依赖于编译器扩展,在某些情况下可以使用,并且可以生成更严格的代码(其中一个出现在此答案的前一版本中),但没有一个是通用的。

(我建议调用函数explicit_bzero,因为它在多个C库中以该名称提供。名称至少有四个其他竞争者,但每个竞争者只被一个C采用库)。

你也应该知道,即使你可以让它发挥作用,也可能还不够。特别要考虑

struct aes_expanded_key { __uint128_t rndk[16]; };

void encrypt(const char *key, const char *iv,
             const char *in, char *out, size_t size)
{
    aes_expanded_key ek;
    expand_key(key, ek);
    encrypt_with_ek(ek, iv, in, out, size);
    explicit_bzero(&ek, sizeof ek);
}

假设硬件具有AES加速指令,如果expand_keyencrypt_with_ek是内联的,则编译器可以将ek完全保留在向量寄存器文件中 - 直到调用{ {1}},它强制它将敏感数据复制到堆栈只是为了擦除它,更糟糕的是,它还没有对仍然存在于其中的键做一件坏事。矢量寄存器!

答案 1 :(得分:15)

  

我需要一个功能(如WinAPI中的SecureZeroMemory)总是将内存归零,并且不会被优化掉,

这是标准函数memset_s的用途。

关于volatile的这种行为是否符合要求,这有点难以说明,而且said长期以来一直受到bug的困扰。

一个问题是规范说&#34;对易失物体的访问严格按照抽象机的规则进行评估。&#34;但这只是指“易失性对象”,而不是通过添加了volatile的指针访问非易失性对象。显然,如果编译器可以告诉你并没有真正访问易失性对象,那么它根本不需要将对象视为易失性。

答案 2 :(得分:2)

我将这个版本作为可移植的C ++提供(尽管语义略有不同):

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    volatile unsigned char* bytePtr = new (ptr) volatile unsigned char[size];

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

现在您可以对易失性对象进行写访问,而不仅仅是访问通过对象的易失性视图创建的非易失性对象。

语义上的区别在于它现在正式结束了占用内存区域的任何对象的生命周期,因为内存已被重用。因此,在将其内容归零后访问该对象现在肯定是未定义的行为(以前在大多数情况下它将是未定义的行为,但肯定存在一些例外)。

要在对象的生命周期内而不是在结束时使用此归零,调用者应使用展示位置new重新放回原始类型的新实例。

使用值初始化可以缩短代码(尽管不太清楚):

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    new (ptr) volatile unsigned char[size] ();
}

并且在这一点上它是一个单行并且几乎不保证辅助功能。

答案 3 :(得分:0)

应该可以通过在右侧使用volatile对象来编写函数的可移植版本,并强制编译器将存储保留到数组中。

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<a href="#">Has # at end</a>
<a href="http://www.google.com">FOO</a>
<a href="http://www.google.com">BAR</a>

void volatileZeroMemory(void* ptr, unsigned long long size) { volatile unsigned char zero = 0; unsigned char* bytePtr = static_cast<unsigned char*>(ptr); while (size--) { *bytePtr++ = zero; } zero = static_cast<unsigned char*>(ptr)[zero]; } 对象声明为zero,确保编译器不会对其值进行任何假设,即使它总是计算为零。

最终赋值表达式从数组中的volatile索引读取,并将值存储在volatile对象中。由于无法优化此读取,因此可确保编译器必须生成循环中指定的存储。