Using volatile twice in an R-value

前端 未结 2 1020
傲寒
傲寒 2021-02-07 22:36

The statement:

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

Generates a warning C4197 in MSVC v14.1:

2条回答
  •  盖世英雄少女心
    2021-02-07 23:27

    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.

提交回复
热议问题