在R值中使用volatile两次 [英] Using volatile twice in an R-value

查看:123
本文介绍了在R值中使用volatile两次的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

声明:

pre $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $

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


警告C4197:'volatile unsigned char * volatile':忽略转换中的顶级易失性


2011 C标准(第[N1570] 6.7.3节)指出:与限定类型相关的属性仅对表达式有意义,即为l值,因此该类型中的顶级易失性是忽略并产生这个警告。



这段代码的作者指出,它没有违反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);

$ / code>

...其中他表示函数f1()编译为无(只是一个ret指令),但f1_volatile_pnt()会编译成执行预期工作的指令。



问题:是否有方法可以正确写入代码,以便GCC按照2011 C标准正确编译它(部分[N1570] 6.7.3 4.),因此它不会生成MSVC和ICC的警告? ...没有#ifdef ...



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

使代码易失性unsigned char * volatile p =(volatile unsigned char * volatile)v; 在C或C ++中编译时没有警告,并保留作者的意图,删除剧集中的第二个 volatile

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

C中不需要强制转换,但问题是MSVC中没有警告就会编译代码,它编译为C ++,而不是C,因此需要强制转换。仅在C语言中,如果语句可以是(假设 v void * 或者与<$
$ b $ pre $ $ $ $ $ $ $>易失性字符*易失性p = v; code> p



为什么要将指针限定为易失性



原始来源包含以下代码:

  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 的数组。如果使用 volatile 定义 x ,它是一个易失性对象,编译器总是实现写入;如果 x 没有用volatile定义,但我们在优化过程中从不删除它们。 把它的地址放在一个指针 p 中,该指针的类型为指向volatile unsigned char 的指针,当我们写 * p = 0 ?正如 R 所指出的,如果编译器可以看到 p 指向 x ,它知道被修改的对象不是易失性的,因此编译器不需要实际写入内存,否则它可以优化分配。这是因为C标准在访问易失性对象方面定义了 volatile ,而不仅仅是通过一个类型为指向volatile的指针的指针访问内存。



为了确保编译器实际写入 x ,此代码的作者声明 p 易变。这意味着,在 * 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 的目的是从编译器中隐藏指针指向非易失性存储器的事实。 / p>

虽然这达到了作者的目标,但它具有强制编译器在循环的每次迭代中重新加载指针并防止编译器通过写入来优化循环的不利影响到目的地一次几个字节。



铸造一个价值



考虑定义:

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

我们已经在上面看到 p 作为挥发性无符号字符*挥发性是完成作者的目标所必需的,尽管它是C中缺点的一个不幸的解决方法。但是,怎么样(volatile unsigned char * volatile)



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



考虑到演员在那里,问题是问第二个 volatile 是否有任何意义。 C标准明确规定:与限定类型相关的属性仅对于左值的表达式才有意义。(C 2011 [N1570] 6.7.3 4。) code> volatile 表示编译器未知的东西可以改变对象的值。例如,如果程序中存在 volatile int a ,那意味着可以更改由 a 标识的对象通过编译器不知道的某种方式。它可以通过计算机上的某些特殊硬件,调试器,操作系统或其他方式进行更改。

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

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



演员(volatile unsigned char * volatile)指向易失性unsigned char的指针。可以指向 volatile unsigned char - 指针指向内存中的某些东西。但是这是一个易变的指针是什么意思?指针只是一个值;它是一个地址。值没有内存,它们不是对象,所以它们不能变化。因此,在标准C中,cast (volatile unsigned char * volatile)中的第二个 volatile 不起作用。它符合C代码,但限定符不起作用。


The statement:

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

Generates a warning C4197 in MSVC v14.1:

Warning C4197: 'volatile unsigned char *volatile ': top-level volatile in cast is ignored

The 2011 C standard (section [N1570] 6.7.3 4.) states: "The properties associated with qualified types are meaningful only for expressions, that are l-values", thus the top-level volatile in this cast is ignored and generates this warning.

The author of this code states, that it doesn't violate the C standard and is required to prevent some GCC optimizations. He illustrates the problem with the code at: 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);
}

...where he shows that the function f1() compiles to nothing (just a ret instruction) but f1_volatile_pnt() compiles to instructions that do the intended job.

QUESTION: Is there a way to properly write this code so that it is compiled correctly by GCC and in accordance with the 2011 C standard (section [N1570] 6.7.3 4.) so it does not generate the warning with MSVC and ICC ? ...without #ifdef...

For the context of this problem, see: https://github.com/jedisct1/libsodium/issues/687

解决方案

Conclusion

To make the code volatile unsigned char * volatile p = (volatile unsigned char * volatile) v; compile in C or in C++ without warnings and while retaining the author’s intent, remove the second volatile in the cast:

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

The cast is unnecessary in C, but the question asks that the code be compilable without warning in MSVC, which compiles as C++, not C, so the cast is needed. In C alone, if the statement could be (assuming v is void * or is compatible with the type of p):

volatile unsigned char * volatile p = v;

Why Qualify a Pointer as Volatile

The original source contains this code:

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

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

The apparent desire of this code is to ensure that memory is cleared for security purposes. Normally, if C code assigns zero to some object x and never reads x before a subsequent assignment or program termination, the compiler will, when optimizing, remove the assignment of zero. The author does not want this optimization to occur; they apparently intend to ensure that memory is actually cleared. Clearing memory can reduce opportunities for an attacker to read the memory (through side channels, by exploiting bugs, by gaining physical possession of the computer, or other means).

Suppose we have some buffer x that is an array of unsigned char. If x were defined with volatile, it is a volatile object, and the compiler always implements writes to it; it never removes them during optimization.

On the other hand, if x is not defined with volatile, but we put its address in a pointer p that has a type pointer to volatile unsigned char, what happens when we write *p = 0? As R.. points out, if the compiler can see that p points into x, it knows that the object being modified is not volatile, and therefore the compiler is not required to actually write to memory if it can otherwise optimize away the assignment. This is because the C standard defines volatile in terms of accessing volatile objects, not merely accessing memory through a pointer that has a type of "pointer to volatile something."

To ensure the compiler actually writes to x, the author of this code declares p to be volatile. What this means is that, in *p = 0, the compiler cannot know that p points into x. The compiler is required to load the value of p from whatever memory it has assigned for p; it must assume p may have changed from the value that pointed into x.

Further, when p is declared volatile unsigned char *volatile p, the compiler must assume that the place pointed to by p is volatile. (Technically, when it loads the value of p, it could examine it, discover it is in fact pointing into x or some other memory known not to be volatile, and then treat it as non-volatile. But this would be an extraordinary effort by the compiler, and we can assume it does not happen.)

Therefore, if the code were:

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

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

then, whenever the compiler can see that pnt in fact points to non-volatile memory and that memory is not read before it is later written, the compiler may remove this code during optimization. However, if the code is:

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

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

then, in each iteration of the loop, the compiler must:

  • Load pnt_ from the memory allocated for it.
  • Calculate the destination address.
  • Write zero to that address (unless the compiler goes to the extraordinary trouble of determining the address is non-volatile).

Thus, the purpose of the second volatile is to hide from the compiler the fact that the pointer points to non-volatile memory.

Although this accomplishes the author’s goal, it has the undesired effects of forcing the compiler to reload the pointer in each iteration of the loop and preventing the compiler from optimizing the loop by writing to the destination several bytes at a time.

Casting a Value

Consider the definition:

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

We have seen above that the definition of p as volatile unsigned char * volatile is needed to accomplish the author’s goal, although it is an unfortunate workaround to shortcomings in C. However, what about the cast, (volatile unsigned char * volatile).

First, the cast is unnecessary, as the value of v will be automatically converted to the type of p. To avoid the warning in MSVC, the cast can simply be removed, leaving the definition as volatile unsigned char * volatile p = v;.

Given that the cast is there, the question asks whether the second volatile has any meaning. The C standard explicitly says "The properties associated with qualified types are meaningful only for expressions that are lvalues." (C 2011 [N1570] 6.7.3 4.)

volatile means something unknown to the compiler can change the value of an object. For example, if there is a volatile int a in the program, that means the object identified by a can be changed by some means not known to the compiler. It could be changed by some special hardware on the computer, by a debugger, by the operating system, or by other means.

volatile modifies an object. An object is a region of data storage in memory that can represent values.

In expressions, we have values. For example, some int values are 3, 5, or −1. Values cannot be volatile. They are not storage in memory; they are abstract mathematical values. The number 3 can never change; it is always 3.

The cast (volatile unsigned char * volatile) says to cast something to be a volatile pointer to a volatile unsigned char. It is fine to point to a volatile unsigned char—a pointer points to something in memory. But what does it mean to be a volatile pointer? A pointer is just a value; it is an address. Values do not have memory, they are not objects, so they cannot be volatile. So the second volatile in the cast (volatile unsigned char * volatile) has no effect in standard C. It is conforming C code, but the qualifier has no effect.

这篇关于在R值中使用volatile两次的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆