C 2018 5.1.2.3 6 says:
The least requirements on a conforming implementation are:
Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine.
At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
The input and output dynamics of interactive devices shall take place as specified in 7.21.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.
This is the observable behavior of the program.
On the face of it, this does not include the exit status of the program.
Regarding exit(status)
, 7.22.4.4 5 says:
Finally, control is returned to the host environment. If the value of
status
is zero orEXIT_SUCCESS
, an implementation-defined form of the status successful termination is returned. If the value ofstatus
isEXIT_FAILURE
, an implementation-defined form of the status unsuccessful termination is returned. Otherwise the status returned is implementation-defined.
The standard does not tell us this is part of the observable behavior. Of course, it makes no sense for this exit
behavior to be a description purely of C’s abstract machine; returning a value to the environment has no meaning unless it is observable in the environment. So my question is not so much whether the exit status is observable as whether this is a defect in the C standard’s definition of observable behavior. Or is there text somewhere else in the standard that applies?
I think it’s possible to piece this together to see the answer fall under the first bullet point of § 5.1.2.3.6:
Accesses to volatile objects are evaluated strictly according to the rules of the abstract machine
Looking further, § 3.1 defines “access” as:
to read or modify the value of an object
and § 3.15 defines an “object” as:
region of data storage in the execution environment, the contents of which can represent values
The standard, curiously, contains no definition of “volatile object”. It does contain a definition of “an object that has volatile-qualified type” in § 6.7.3.6:
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.
It seems not unreasonable to infer that an object that has volatile-qualified type has that qualification precisely to inform the compiler that it is, in fact, a volatile object, so I don’t think it’s stretching things too far to use this wording as the basis of a definition for “volatile object” itself, and define a volatile object as an object which may be modified in ways unknown to the implementation or have other unknown side effects.
§ 5.1.2.3.2 defines “side effect” as follows:
Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
So I think we can piece this together as follows:
Returning the exit status to be returned to the host environment is
clearly a change in the state of the execution environment, as the
execution environment after having received, for example,
EXIT_SUCCESS
, is necessarily in a different state that it would be
had it received, for example, EXIT_FAILURE
. Returning the exit
status is therefore a side effect per § 5.1.2.3.2
exit()
is a function that does this, so calling exit()
is therefore
itself also a side effect per § 5.1.2.3.2.
The standard obviously gives us no details of the inner workings of
exit()
or of what mechanism exit()
will use to return that value to
the host environment, but it’s nonsensical to suppose that accessing
an object will not be involved, since objects are regions of data
storage in the execution environment whose contents can represent
values, and the exit status is a value.
Since we don’t know what, if anything, the host environment will do in response to that change in state (not least because our program will have exited before it happens), this access has an unknown side effect, and the object accessed is therefore a volatile object.
Since calling exit()
accesses a volatile object, it is observable
behavior per § 5.1.2.3.6.
This is consistent with the normal understanding of objects that have volatile-qualified types, namely that we can’t optimize away accesses to such objects if we cannot determine that no needed side effects will be omitted because the observable behavior (in the normal everyday sense) may be affected if we do. There is no visible object of volatile-qualified type in this case, of course, because the volatile object is question is accessed internally by exit()
, and exit()
obviously need not even be written in C. But there seems undoubtedly to be a volatile object, and § 5.1.2.3 refers specifically (three times) to volatile objects, and not to objects of volatile-qualified type (and other than a footnote to § 6.2.4.2, this is the only place in the standard volatile objects are referred to.)
Finally, this seems to be the only reading that renders § 5.1.2.3.6 intelligible, as intuitively we’d expect the “observable behavior” of C programs using only the facilities described by the standard to be that which loosely:
which seems to be essentially what § 5.1.2.3.6 is trying to get at.
Edit
There seems to be some little controversy in the comments apparently centered around the ideas that the exit status may be passed in registers, and that registers cannot be objects. This objection (no pun intended) is trivially refutable:
Objects can be declared with the register
storage class specifier, and such objects can be designated by lvalues;
Memory-mapped registers, fairly ubiquitous in embedded programming, provide as clear a demonstration as any that registers can be objects, can have addresses, and can be designated by lvalues. Indeed, memory-mapped registers are one of the most common uses of volatile
-qualified types;
mmap()
shows that even file contents can sometimes have addresses and be objects.
In general, it's a mistake to believe that objects can only reside in, or that "addresses" can only refer to, locations in core memory, or banks of DRAM chips, or anything else that one might conventionally refer to as "memory" or "RAM". Any component of the execution environment that's capable of storing a value, including registers, could be an object, could potentially have an address, and could potentially be designated by an lvalue, and this is why the definition of "object" in the standard is cast in such deliberately wide terms.
Additionally, sections such as § 5.3.2.1.9 go to some lengths to draw a distinction between "values of the actual objects" and "[values] specified by the abstract semantics", indicating that actual objects are real things existing in the execution environment distinct from the abstract machine, and are things which the specification does indeed closely concern itself with, as the definition of "object" in § 3.15 makes clear. It seems untenable to maintain a position that the standard concerns itself with such actual objects up to and only up to the point at which a standard library function is invoked, at which point all such concerns evaporate and such matters suddenly become "outside C".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With