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

coo*_*451 87 c c++ standards gcc

我需要一个函数(就像来自WinAPI的SecureZeroMemory)总是将内存归零并且不会被优化掉,即使编译器认为在此之后永远不会再访问内存.似乎是挥发性的完美候选者.但是我实际上遇到了一些与GCC合作的问题.这是一个示例函数:

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

    while (size--)
    {
        *bytePtr++ = 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

很简单.但是,如果你调用它,GCC实际生成的代码会因编译器版本和你实际尝试为零的字节数而有很大不同.https://godbolt.org/g/cMaQm2

  • GCC 4.4.7和4.5.3永远不会忽略volatile.
  • 对于数组大小为1,2和4,GCC 4.6.4和4.7.3忽略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;
}
Run Code Online (Sandbox Code Playgroud)

callMeMaybe()的可能写入将使除6.1之外的所有GCC版本生成预期的存储.在内存栏中进行注释也会使GCC 6.1生成存储,尽管只能与callMeMaybe()的可能写入一起使用.

有人还建议刷新缓存.Microsoft 在"SecureZeroMemory"中根本尝试刷新缓存.无论如何,缓存很可能会很快失效,所以这可能不是什么大问题.此外,如果另一个程序试图探测数据,或者它将被写入页面文件,它将始终是归零版本.

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

zwo*_*wol 81

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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

有关标准执行或不需要的更多讨论,请参阅为什么volatile变量局部变量与volatile参数的优化不同,为什么优化器会从后者生成无操作循环?,通过易失性引用/指针访问声明的非易失性对象是否会在所述访问时赋予易失性规则?GCC bug 71793.

有关委员会的想法的 更多信息volatile,请在C99理由中搜索"volatile"一词.John Regehr的论文" Volatiles is Miscompiled "详细说明volatile了生产编译器可能无法满足程序员的期望.LLVM团队的一系列文章" 每个C程序员应该知道的关于未定义行为的内容 "并未特别涉及,volatile但将帮助您了解现代C编译器不是 "便携式汇编程序"的方式和原因.


关于如何实现一个能够做你想做的功能的实际问题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) {}
Run Code Online (Sandbox Code Playgroud)

但是,您必须确保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);
}
Run Code Online (Sandbox Code Playgroud)

假设具有AES加速指令的硬件,如果expand_key并且encrypt_with_ek是内联的,编译器可能能够ek完全保留在向量寄存器文件中 - 直到调用explicit_bzero,这迫使它将敏感数据复制到堆栈上以便擦除它,并且,更糟糕的是,对于仍然位于向量寄存器中的键没有做一件坏事!

  • @IwillnotexistIdonotexist该段落中的关键词是_object_.`volatile sig_atomic_t flag;`是一个volatile _object_.`*(volatile char*)foo`只是一个通过volatile限定的lvalue_的_access,标准不需要具有任何特殊效果. (15认同)
  • 如何将6.7.3(7)的`volatile`定义为_ [...]因此,任何涉及此类对象的表达式都应严格按照抽象机的规则进行评估,如5.1中所述. 2.3.**此外,在每个序列点,最后存储在对象中的值应与抽象机**规定的值一致,除非由前面提到的未知因素修改.什么构成对具有volatile限定类型的对象的访问是实现定义的._? (10认同)
  • 这很有意思......我有兴趣看到委员会的评论. (6认同)
  • 标准说明了什么标准必须满足才能成为"合规"的实施.它没有努力描述给定平台上的实现必须满足哪些标准才能成为"好"实现或"可用"实现.GCC对"volatile"的处理可能足以使其成为"合规"实现,但这并不意味着它足以"好"或"有用".对于许多类型的系统编程,在这些方面应该被认为是非常缺乏的. (3认同)
  • C规范也直接说*"实际实现不需要评估表达式的一部分,如果它可以推断出它的值没有被使用并且没有产生所需的副作用(**包括由调用函数或访问函数引起的任何副作用)挥发性物体**)."*(强调我的). (3认同)
  • 那么GCC在"原始"函数中使用memset呢?Memset可能会使用16,8或4字节存储,是否有可能弄乱内存映射寄存器? (2认同)
  • 您对VLAIS(结构中的VLA)的使用是一个非常糟糕的主意,因为在非gcc编译器上主动不支持它,否则将实现gcc扩展.而不是复杂类型的"m"约束,只需使用内存地址的"r"约束和"内存"`clobber.然后编译器不能对asm对指向对象(或任何其他可达内存)的作用做出任何假设,因此无法对其进行优化. (2认同)
  • @R ..答案已经指出VLAIS只适用于GCC,只适用于C.至于你的选择,我们已经不止一次地讨论过了,但我可能没有充分说过这个:我不认为gcc或者"非gcc编译器以其他方式实现gcc扩展"可以指望用你认为它应该被解释的方式解释它. (2认同)
  • @zwol:标准非常清楚地表明,标准合规并不意味着质量,并且实现完全有可能符合标准并且无用.我建议说,挥发性访问的效果是"实现定义的",这意味着记录限定符没有任何影响的编译器将属于"兼容但无用"的类别.gcc的人应该对标准规定的内容不太感兴趣,并且对使某些内容变得有用的内容更感兴趣. (2认同)
  • @supercat好的,但是你没有告诉_me_我还不知道的任何东西,而且_I_绝对没有办法解决它.因此,除非您有实际的变更建议,否则我们可以放弃它吗? (2认同)

bam*_*s53 15

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

这就是标准功能memset_s的用途.


至于挥发性的这种行为是否符合,这有点难以说,而且据说挥发性长期以来一直受到错误的困扰.

一个问题是规范说"根据抽象机器的规则严格评估对易失性对象的访问".但这只是指'volatile对象',而不是通过添加了volatile的指针访问非易失性对象.显然,如果编译器可以告诉你实际上并没有访问volatile对象,那么就不需要将对象视为volatile.

  • 另外,将`memset_s`描述为C11标准是一种夸大其词.它是附件K的一部分,它在C11中是可选的(因此在C++中也是可选的).基本上所有的实现者,包括微软,其首先想法(!),都拒绝接受它; 最后我听说他们正在谈论在C-next中废弃它. (13认同)
  • @ cooky451在某些圈子里,微软因为基本上每个人的反对而强迫进入C标准而臭名昭着,然后自己也没有费心去实施它.(最令人震惊的例子是C99放宽了允许基本类型的`size_t`的规则.Win64 ABI不符合C90.那本来是......不是_ok_,但并不可怕. ..如果MSVC实际上及时收到了像'uintmax_t`和'%zu`这样的C99,但是他们_didn't_. (8认同)
  • 值得注意的是,有趣的是,这个函数是针对C11标准化的,但不适用于C++ 11,C++ 14或C++ 17.从技术上讲,它不是C++的解决方案,但我同意从实际角度看这似乎是最好的选择.在这一点上,我确实想知道GCC的行为是否符合要求.编辑:实际上,VS 2015没有memset_s,所以它并不是那么便携. (5认同)
  • 注意:这是C11标准的一部分,尚未在所有工具链中提供. (4认同)
  • @ cooky451我以为[C++ 17引用了C11标准库](http://stackoverflow.com/a/38060437/3484570)(见第二个杂项). (2认同)