该声明:

volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;

在 MSVC v14.1 中生成警告 C4197:



2011 C 标准(第 [N1570] 6.7.3 4. 节)指出:“与限定类型关联的属性仅对表达式有意义,即 l 值”,因此此强制转换中的顶级 volatile 被忽略并生成这个警告。

此代码的作者声明,它不违反 C 标准,并且需要防止某些 GCC 优化。
他用代码说明了问题:https://godbolt.org/g/xP4eGz
#include <stddef.h>

static void memset_s(void * v, size_t n) {
  volatile unsigned char * p = (volatile unsigned char *)v;
  for(size_t i = 0; i < n; ++i) {
    p[i] = 0;
  }
}

void f1() {
  unsigned char x[4];
  memset_s(x, sizeof x);
}

static void memset_s_volatile_pnt(void * v, size_t n) {
  volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;
  for(size_t i = 0; i < n; ++i) {
    p[i] = 0;
  }
}

void f1_volatile_pnt() {
  unsigned char x[4];
  memset_s_volatile_pnt(x, sizeof x);
}

...在那里他表明函数 f1() 编译为空(只是一个 ret 指令)但 f1_volatile_pnt() 编译为执行预期工作的指令。

问题 :有没有办法正确编写此代码,以便 GCC 正确编译它并符合 2011 C 标准(第 [N1570] 6.7.3 MS 4.),因此它不会在 VC 中生成警告国际刑事法院? ...没有#ifdef ...

有关此问题的上下文,请参阅:https://github.com/jedisct1/libsodium/issues/687

最佳答案

结论

为了使代码 volatile unsigned char * volatile p = (volatile unsigned char * volatile) v; 在没有警告的情况下在 C 或 C++ 中编译,同时保留作者的意图,删除类型转换中的第二个 volatile:

volatile unsigned char * volatile p = (volatile unsigned char *) v;

在 C 中不需要强制转换,但问题要求代码在 MSVC 中可以编译而不发出警告,MSVC 编译为 C++,而不是 C,因此需要强制转换。仅在 C 中,如果语句可以是(假设 vvoid * 或与 p 的类型兼容):
volatile unsigned char * volatile p = v;

为什么将指针限定为 volatile

original source 包含以下代码:
volatile unsigned char *volatile pnt_ =
    (volatile unsigned char *volatile) pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

此代码的明显目的是确保出于安全目的清除内存。通常,如果 C 代码将零分配给某个对象 x 并且在后续赋值或程序终止之前从未读取 x,编译器将在优化时删除零的赋值。作者不希望发生这种优化;他们显然打算确保实际清除内存。清除内存可以减少攻击者读取内存的机会(通过旁道、利用漏洞、获得计算机的物理所有权或其他方式)。

假设我们有一些缓冲区 x,它是一个 unsigned char 的数组。如果 x 被定义为 volatile ,则它是一个 volatile 对象,编译器总是实现对它的写入;在优化过程中它永远不会删除它们。

另一方面,如果 x 没有用 volatile 定义,但我们把它的地址放在一个指针 p 中,它的类型为 pointer to volatile unsigned char,那么当我们写 0x251812411 时会发生什么?正如 R.. 指出的那样,如果编译器可以看到 *p = 0 指向 p ,则它知道被修改的对象不是 volatile 的,因此编译器不需要实际写入内存,否则如果它可以优化分配。这是因为 C 标准在访问 volatile 对象方面定义了 x,而不仅仅是通过具有“指向 volatile 事物的指针”类型的指针访问内存。

为确保编译器实际写入 volatile ,此代码的作者声明 x 为 volatile 。这意味着,在 p 中,编译器无法知道 *p = 0 指向 p 。编译器需要从它分配给 x 的任何内存中加载 p 的值;它必须假设 p 可能已经从指向 p 的值改变了。

此外,当 x 被声明为 p 时,编译器必须假设 volatile unsigned char *volatile p 指向的位置是易失的。 (从技术上讲,当它加载 p 的值时,它可以检查它,发现它实际上指向 p 或其他一些已知的非 volatile 内存,然后将其视为非 volatile 。但这将是一项非凡的努力由编译器,我们可以假设它不会发生。)

因此,如果代码是:
volatile unsigned char *pnt_ = pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

然后,每当编译器可以看到 x 实际上指向非 volatile 内存并且在稍后写入之前未读取该内存时,编译器可能会在优化期间删除此代码。但是,如果代码是:
volatile unsigned char *volatile pnt_ = pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

然后,在循环的每次迭代中,编译器必须:
  • 从分配给它的内存中加载 pnt
  • 计算目的地址。
  • 将零写入该地址(除非编译器遇到非常麻烦的确定地址是非 volatile 的)。

  • 因此,第二个 pnt_ 的目的是向编译器隐藏指针指向非 volatile 存储器的事实。

    尽管这实现了作者的目标,但它会产生不良影响,即强制编译器在循环的每次迭代中重新加载指针,并阻止编译器通过一次向目标写入几个字节来优化循环。

    类型转换值(value)

    考虑定义:
    volatile unsigned char * volatile p = (volatile unsigned char * volatile) v;
    

    我们在上面已经看到,需要将 volatile 定义为 p 来实现作者的目标,尽管这是对 C 中缺点的不幸解决方法。 但是,转换 volatile unsigned char * volatile 呢。

    首先,强制转换是不必要的,因为 (volatile unsigned char * volatile) 的值将自动转换为 v 的类型。为了避免 MSVC 中的警告,可以简单地删除强制转换,将定义保留为 p

    鉴于类型转换在那里,问题是第二个 volatile unsigned char * volatile p = v; 是否有任何意义。 C 标准明确指出“与限定类型关联的属性仅对左值表达式有意义。” (C 2011 [N1570] 6.7.3 4.)
    volatile 意味着编译器未知的东西可以改变对象的值。例如,如果程序中有一个 volatile,这意味着 volatile int a 标识的对象可以通过某种编译器不知道的方式进行更改。它可以通过计算机上的某些特殊硬件、调试器、操作系统或其他方式进行更改。
    a 修改了 对象 。对象是内存中可以表示值的数据存储区域。

    在表达式中,我们有值。例如,一些 volatile 值是 3、5 或 -1。值不能是可变的。它们不是存储在内存中;它们是抽象的数学值。数字 3 永远不会改变;它总是 3。

    强制转换 int 表示将某些东西强制转换为指向 volatile 无符号字符的 volatile 指针。指向一个 (volatile unsigned char * volatile) 是没问题的——一个指向内存中的东西的指针。但是作为一个 volatile 指针是什么意思呢?指针只是一个值;它是一个地址。值没有内存,它们不是对象,因此它们不能是 volatile。因此,强制转换 volatile unsigned char 中的第二个 volatile 在标准 C 中没有影响。它符合 C 代码,但限定符没有影响。

    关于c++ - 在 R 值中使用 volatile 两次,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/49084430/

    10-16 06:15