在R值中使用volatile两次

问题描述 投票:12回答:2

该声明:

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

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

警告C4197:'volatile unsigned char * volatile':忽略强制转换中的顶级volatile

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节)正确编译,以便它不会产生MSVC和ICC的警告? ......没有#ifdef ......

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

c++ c gcc visual-c++
2个回答
10
投票

Conclusion

要使代码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;

Why Qualify a Pointer as 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中,那么当我们编写*p = 0时会发生什么?正如R..指出的那样,如果编译器可以看到p指向x,它知道被修改的对象不是易失性的,因此如果编译器可以以其他方式优化掉分配,则不需要编译器实际写入内存。这是因为C标准在访问易失性对象方面定义了volatile,而不仅仅是通过具有“指向volatile事物的指针”类型的指针来访问内存。

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

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

因此,如果代码是:

volatile unsigned char *pnt_ = pnt;
size_t i = (size_t) 0U;

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

然后,每当编译器看到pnt实际上指向非易失性存储器并且在稍后写入之前未读取该存储器时,编译器可以在优化期间移除该代码。但是,如果代码是:

volatile unsigned char *volatile pnt_ = pnt;
size_t i = (size_t) 0U;

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

然后,在循环的每次迭代中,编译器必须:

  • 从分配给它的内存中加载pnt_
  • 计算目的地地址。
  • 将零写入该地址(除非编译器遇到确定地址非常不稳定的特殊麻烦)。

因此,第二个volatile的目的是从编译器中隐藏指针指向非易失性存储器的事实。

虽然这实现了作者的目标,但是它具有强制编译器在循环的每次迭代中重新加载指针并且阻止编译器通过一次写入目标几个字节来优化循环的不期望的效果。

Casting a Value

考虑定义:

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

我们在上面已经看到,p定义为volatile unsigned char * volatile是完成作者目标所必需的,尽管它是C中缺点的一个不幸的解决方法。但是,演员如何,(volatile unsigned char * volatile)

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

鉴于演员阵容在那里,问题是第二个volatile是否有任何意义。 C标准明确指出“与限定类型相关联的属性仅对于左值的表达式有意义。”(C 2011 [N1570] 6.7.3 4.)

volatile意味着编译器未知的东西可以改变对象的值。例如,如果程序中有volatile int a,则表示a标识的对象可以通过编译器不知道的某种方式进行更改。它可以通过计算机上的某些特殊硬件,调试器,操作系统或其他方式进行更改。

volatile修改一个对象。对象是存储器中可以表示值的数据存储区域。

在表达式中,我们有价值观。例如,某些int值为3,5或-1。值不能波动。它们不是存储在内存中;它们是抽象的数学价值。 3号永远不会改变;它总是3。

演员(volatile unsigned char * volatile)说要将某些内容转换成易失性unsigned char的易失性指针。可以指向一个volatile unsigned char指针指向内存中的某个东西。但是,作为一个易失性指针是什么意思?指针只是一个值;这是一个地址。值没有内存,它们不是对象,因此它们不能是volatile。所以演员volatile中的第二个(volatile unsigned char * volatile)对标准C没有影响。它符合C代码,但是限定符没有效果。


3
投票

从根本上没有办法表达作者想要表达的内容。某些编译器正确地将代码的第一个版本优化为零,因为底层对象unsigned char x[4]不是易失性的;通过指针到易失性来访问它并不会让它变得不稳定。

代码的第二个版本是一个实现了作者想要的黑客攻击,但是需要付出相当大的额外成本,而且在现实世界中可能适得其反。如果(在一个非玩具,充实的例子中)数组x只被使用,使得编译器能够将它完全保存在寄存器中,memset_s_volatile_pnt中的hack会强制它被溢出到堆栈上的实际内存中,然后才被破坏,memset_s_volatile_pnt将无法做任何事情来摆脱原始登记册中的副本。实现相同目标的更便宜的方法是在memset上调用正常的x,然后将x传递给外部函数,该函数的定义是编译器无法看到的(最安全的,是不同共享库中的外部函数)。

C中不能表示安全存储器清除;它需要编译器/语言级扩展。在C + POSIX中执行此操作的最佳方法是在单独的进程中对敏感数据进行所有处理,其生命周期仅限于需要敏感数据的持续时间,并依赖于内存保护边界以确保它永远不会泄漏到其他任何位置。

但是,如果您只想摆脱警告,解决方案很简单。只需更改:

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

至:

volatile unsigned char * volatile p = (volatile unsigned char *)v;
© www.soinside.com 2019 - 2024. All rights reserved.