该声明:
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 中,如果语句可以是(假设
v
是 void *
或与 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
。 因此,第二个
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/