Reputation: 6796
This'll be a long one, as to contextualise it and provide as much info as I can, I must meander through various links and quotes - as is often the only way once we enter the C/C++ Standard Rabbit Hole. If you have better citations or any other improvements to this post, please let me know. But to summarise up front, you can blame @zwol for me posting this ;-) and the aim is to find the truth from among two propositions:
volatile *
or volatile &
must refer to an object originally declared volatile
in order to have volatile
semantics?volatile
-qualified object through a volatile
pointer/reference sufficient/supposed to make said accesses behave as if the object was declared volatile
?And either way, if (as it seems) the wording is somewhat ambiguous compared to the intent - can we get that made clear in the Standards themselves?
The 1st of these mutually exclusive interpretations is more commonly held, and that's not entirely without basis. However, I hope to show that there's a significant amount of "reasonable doubt" in favour of the 2nd - especially when we go back to some prior passages in the Rationale and WG Papers.
volatile
Yesterday's popular question Is the definition of “volatile” this volatile, or is GCC having some standard compliancy problems? arose by assuming a volatile
reference would confer volatile
behaviour on a non-volatile
referent - but finding that it did not, or did to varying degrees and in an unpredictable way.
The accepted answer initially concluded that only the declared type of the referent mattered. This and most comments seemed to agree that equivalent principles are in play as we know well for const
: the behaviour would only be volatile
(or defined at all) if the reference has the same cv-qualification as the referred object:
The key word in that passage is object.
volatile sig_atomic_t flag;
is a volatile object.*(volatile char *)foo
is merely an access through a volatile-qualified lvalue and the standard does not require that to have any special effects. – zwol
This interpretation appears to be quite widely held, as seen in the responses to this similar-but-hopefully-not-duplicate question: Requirements for behavior of pointer-to-volatile pointing to non-volatile object But there's uncertainty even there: right after the answer says 'no', it then says 'maybe'! Anyway...let's check the Standard to see what the 'no's are based on.
C11, N1548, §6.7.3: Whereas it's clear that it's UB to access an object defined with volatile
or const
type via a pointer that doesn't share said qualifier...
6 If an attempt is made to modify an object defined with a
const
-qualified type through use of an lvalue with non-const
-qualified type, the behavior is undefined. If an attempt is made to refer to an object defined with avolatile
-qualified type through use of an lvalue with non-volatile-
qualified type, the behavior is undefined.(133)
...the Standard doesn't seem to explicitly mention the opposite scenario, namely for volatile
. Moreover, when summarising volatile
and operations thereon, it now talks about an object that has volatile
-qualified type:
7 An object that has
volatile
-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously.(134) What constitutes an access to an object that hasvolatile
-qualified type is implementation-defined.
Are we to assume "has" is equivalent to "was defined with"? or can "has" refer to a combination of object and reference qualifiers?
A commenter summed up the issue with this sort of wording well:
From n1548 §6.7.3 ¶6 the standard uses the phrase "object defined with a volatile-qualified type" to distinguish it from "lvalue with volatile-qualified type". It's unfortunate that this "object defined with" versus "lvalue" distinction does not carry forward, and the standard then uses "object that has volatile-qualified type", and says that "what constitutes access to an object that has volatile-qualified type is implementation-defined" (which could have said "lvalue" or "object defined with" for clarity). Oh well. – Dietrich Epp
Paragraph 4 of the same section seems to be less frequently quoted but might well be relevant, as we'll see in the next section.
volatile
pointer/reference intended to confer volatile
semantics on its dereference?The aforementioned answer has a comment wherein the author cites an earlier statement by the Committee that cast doubt on the 'reference must match referent' idea:
Interestingly, there is one sentence in there [C99 Rationale for
volatile
] that implies that the committee meant for*(volatile T*)x
to force that one access tox
to be treated as volatile; but the actual wording of the standard does not achieve this. – zwol
We can find a bit more info on this bit of the Rationale, from the 2nd aforementioned thread: Requirements for behavior of pointer-to-volatile pointing to non-volatile object
On the other hand, this post quotes from the 6.7.3 of the Rationale for International Standard--Programming Languages--C:
A cast of a value to a qualified type has no effect; the qualification (volatile, say) can have no effect on the access since it has occurred prior to the case. If it is necessary to access a non-volatile object using volatile semantics, the technique is to cast the address of the object to the appropriate pointer-to-qualified type, then dereference that pointer.
—philipxy
And from that Bytes thread, we're referred to C99 s6.7.3 p3 - a.k.a. C11's p4 - and this analysis:
The paragraph in question is just before section 6.7.3.1 in the rationale document. If you also need to quote from the standard document itself, cite 6.7.3 p3:
The properties associated with qualified types are meaningful only for expressions that are lvalues.
The expression
(volatile WHATEVER) non_volatile_object_identifier
is not an lvalue, hence the 'volatile' qualifier is meaningless.Conversely, the expression
* (volatile WHATEVER *) & non_volatile_object_identifier
is an lvalue (it may be placed on the left side of an assignment statement), so the property of the 'volatile' qualifier has its intended meaning in this case.—Tim Rentsch
There is a very specific demonstration supporting this idea, with specific regard to the 1st linked question, in WG Paper N1381. This introduced the Annexed memset_s()
to do what that OP wanted - guarantee non-elided filling of memory. In discussing possible implementations, it seems to support the idea - by omitting to state any requirement - that using a volatile
pointer to alter a non-volatile
object should generate code based on the qualifier of the pointer, regardless of that of the referred object...
- Platform-independent ' secure-memset' solution:
void *secure_memset(void *v, int c , size_t n) {
volatile unsigned char *p = v;
while (n--) *p++ = c;
return v;
}
This approach will prevent the clearing of memory from being optimized away, and it should work on any standard-compliant platform.
...and that compilers not doing this are on notice...
There has been recent notice that some compilers violate the standard by not always respecting the
volatile
qualifier.
That was exhausting. There's certainly a lot of room for interpretation here, depending on which documents you happen to have read vs which not, and how you choose to interpret a lot of words that aren't specific enough. It seems clear that something is amiss: either:
I'd hope that we can do better than all the ambiguity and speculation that seems to have surrounded this in the past - and get a more conclusive statement put on record. To that end, any further sources and thoughts from experts would be very welcome.
Upvotes: 29
Views: 1516
Reputation: 15158
Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?
volatile
doesn't mean the same thing in C & C++. The C++ standard makes accesses through volatile lvalues observable. [1] It says that it intends this to be the same as C behaviour. And that is the behaviour described in the C Rationale. Nevertheless the C Standard says that accesses to a volatile-declared objects are observable. (Note that accessing a volatile-declared object via a non-volatile lvalue is undefined.)
However. There is a defect report which essentially has committee agreement (although still open) that the Standard should say, and that the intent has always been, and that implementations have always reflected, that it is not the volatility of an object that matters (per the Standard) but of the volatility of (the lvalue of) an access (per the Rationale).
Defect Report Summary for C11 Version 1.10 Date: April 2016 DR 476 volatile semantics for lvalues 04/2016 Open
Of course, what is done about observable behaviour is implementation-dependent.
There really isn't any ambiguity. It's just that people can't believe that the C Standard behaviour could be what it is, because that is not the historical usage pre-volatile
(when address-literal lvalues were taken to be of volatile objects), as intended by the Rationale, as implemented by compilers before and since, as interpreted and described by the C++ Standard, as corrected in the DR. Similarly, the standard is clear in that it doesn't say that non-volatile accesses are observable, so they're not. (And "side effect" is a term used in defining the evaluation partial order.)
[1] Or at least hopefully it does now. From a comment from underscore_d:
For C++, see also P0612R0: NB comment CH 2: volatile, which was adopted this month to clean up some leftover talk about "volatile objects" in the C++ Standard, when really accesses through volatile glvalues are what it meant (as, presumably/hopefully, what C meant).
Upvotes: 8
Reputation: 81247
The Standard makes no attempt to define all of the behaviors that would be necessary in a useful implementation. The rationale explicitly recognizes the possibility that an implementation can be simultaneously conforming and almost totally useless.
The Standard categorizes the semantics of volatile
accesses as implementation-defined, and does not in any way, shape, or form require that an implementation must to define them usefully. It would thus not be unreasonable to say that provided documented and actual behaviors agree, implementation of volatile
semantics like those of gcc would make an implementation non-conforming, but would merely make it useless for purposes for which it might otherwise have been suitable.
Note that gcc is often used on platforms where it may be possible to configure an arbitrary region of address space behave somewhat like an I/O device and then later behave like ordinary RAM. It may thus be necessary to ensure that certain operations are very precisely sequenced even though the sequencing of most other operations wouldn't matter; requiring that all operations on something must be treated as volatile
in order to have any operations thus treated does not seem a good recipe for optimization.
What I find bizarre is that people have become so interested over the last few years in whether the Standard allows compilers to implement useless semantics for some constructs, for the purpose of improving performance of code that doesn't require that constructs be useful, when such an approach would seem inferior in almost every way to implementing useful semantics by default but providing programmers who don't need such semantics to waive them using command line switches or #pragma directives. If a program includes a [hypothetical] #pragma gcc_loose_volatile
directive, gcc can do whatever it likes regardless of how one would otherwise interpret the Standard's requirements regarding volatile
, and if it doesn't include such a directive, useless semantics would be useless regardless of whether the Standard prohibited them or not.
Upvotes: 2
Reputation: 52602
An object is either volatile or not. Referencing an object using a volatile reference will produce code that is correct whether the object is volatile or not.
Upvotes: -1
Reputation: 69902
converted to answer because I think a thoughtful non-answer may help discover the truth here.
I guess the underlying question is "how abstract do we expect the memory model to be?". By qualifying a non-vol pointer as volatile, we seem to be asking the compiler to "write to the I/O or memory directly". That's ok, but if the compiler has previously deduced that the "memory" need not exist, what should it do? Backtrack and create the memory, or ignore you?
It seems to me that these following two cases are very different in intent:
volatile unsigned char * const uart_base = (volatile unsigned char *)0x10000;
This is clearly intended to inform the compiler that there is a uart memory-mapped at address 0x10000.
void *secure_memset(void *v, int c , size_t n) {
volatile unsigned char *p = v;
while (n--) *p++ = c;
return v;
}
This is clearly intended to ensure that the memory at v to (int*)v + n is actually modified before the function returns.
However, whether a call to this function could be elided if it was deduced that the memory at v was never needed is unclear.
I would argue that if previously in the program, the memory has been deduced not to need to exist at all, then I would be unsurprised if the call were elided, regardless of the cast to volatile.
Thanks. Because the address is taken, isn't the object required to occupy memory?
gcc seems to agree with you:
#include <cstdint>
#include <cstring>
void * clearmem(void* p, std::size_t len)
{
auto vp = reinterpret_cast<volatile char*>(p);
while (len--) {
*vp++ = 0;
}
return p;
}
struct A
{
char sensitive[100];
A(const char* p)
{
std::strcpy(sensitive, p);
}
~A() {
clearmem(&sensitive[0], 100);
}
};
void use_privacy(A a)
{
auto b = a;
}
int main()
{
A a("very private");
use_privacy(a);
}
yields:
clearmem(void*, unsigned long):
leaq (%rdi,%rsi), %rax
testq %rsi, %rsi
je .L4
.L5:
movb $0, (%rdi)
addq $1, %rdi
cmpq %rax, %rdi
jne .L5
.L4:
xorl %eax, %eax
ret
use_privacy(A):
leaq -120(%rsp), %rax
leaq 100(%rax), %rdx
.L10:
movb $0, (%rax)
addq $1, %rax
cmpq %rdx, %rax
jne .L10
ret
main:
leaq -120(%rsp), %rax
leaq 100(%rax), %rdx
.L13:
movb $0, (%rax)
addq $1, %rax
cmpq %rdx, %rax
jne .L13
leaq -120(%rsp), %rax
leaq 100(%rax), %rdx
.L14:
movb $0, (%rax)
addq $1, %rax
cmpq %rdx, %rax
jne .L14
leaq -120(%rsp), %rax
leaq 100(%rax), %rdx
.L15:
movb $0, (%rax)
addq $1, %rax
cmpq %rdx, %rax
jne .L15
xorl %eax, %eax
ret
clang doesn't elide the construction of the private arrays, so I can't draw any conclusions there.
Upvotes: 6