在R值中使用volatile两次

该声明:

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  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 4.),因此它不会产生MSVC和ICC的警告? ……没有#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 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的类型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指向的位置是volatile。 (从技术上讲,当它加载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的目的是从编译器中隐藏指针指向非易失性存储器的事实。

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

铸造价值

考虑定义:

 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 -a指针指向内存中的某个东西。 但是成为易失性指针意味着什么? 指针只是一个值; 这是一个地址。 值没有内存,它们不是对象,因此它们不能是volatile。 因此, volatile中的第二个volatile (volatile unsigned char * volatile)对标准C没有影响。它符合C代码,但限定符没有效果。

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

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

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

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

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

至:

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