10:27
@supercat Because the out of bounds memory access can already exist in the program. the reordering moves when it occurs, and thus the effect of the out of bounds impact
@supercat The standard has a set of behaviors that are expected to be defined by the implementation, that is the implementation defined behavior. Undefined behavior is just a removal of any burden of the compiler to document that case. And implementations are a allowed to document undefined behavior as implementation defined, but any programs that are not expected to be dependent on a specific compiler version should not depend on any behavior not explicitly documented by that compiler.
@supercat The problem is 'environment-defined semantics' in practice means defining the machine model, not an abstract machine. What are the 'environment-defined semantics' of writing a jump instruction into the code under execution? Even hoisting a read could change the semantics of a later write by rearranging the code written to.
4 hours later…
14:49
@user1937198 If the programmer knows what reorderings the compiler is allowed to perform, the programmer should be able to judge whether such reorderings would pose a problem. If the "out-of-boounds" storage is RAM which the implementation has received by performing an OS call which the program would expect to use in essentially the same way as storage received from
malloc()
, and the implementation applies the same reordering semantics to it as to any other storage of unknown provenance...
...all will be fine provided that the system doesn't reorder accesses to the storage across calls to unknown functions or volatile-qualified accesses.
Note that free-standing implementations have no means of performing any kind of I/O except by performing accesses to addresses the compiler knows nothing about. On many embedded platforms with a RAM-based interrupt table, it would be possible to write complete programs which can access all the features of the underlying platform without having to use any compiler-specific features or build options...
...beyond being able to set the address ranges used by the implementation for code and data storage, being able to place at least one object or function in a known location, and perhaps being able to manually insert some data at particular addresses in the ROM image.
Most implementations operate on a semantic model where a translator generates some kind of build artifact, which is expected to be fed to an execution environment which abides requirements specified by the translator. The behavior of a program may be decomposed into a sequence of operations that perform loads and stores of various sizes, acquire and release automatic-duration or heap-duration storage. and perform various kinds of arithmetic or logical operations on loaded temporary values.
Many kinds of operations could be specified as "select in Unspecified fashion between using any of the platform's natural means of performing this operation, if it defines any, or a standard means of performing the operation, if the standard defines one. If a platform's natural means of adding two signed numbers will jump to a random address if an overflow occurs at the exact moment some other interrupt arrives, an implementation would not be required to prevent that...
...but otherwise a "canonical" behavior woudl be to behave as though integer arithmetic was performed with arbitrary integers and then truncated after each operation to the result's size. A right-shift
X>>N
when N
is greater than or equal to the width could be defined as an Unspecified choice between (X>>1)>>(N-1)
or X>>(N % bitcount)
[noting that for an expression like (X<<y) |( X>>(32-Y))
both methods of evaluation would yield the same result].
Implementations would be allowed to use any address ranges they have acquired for themselves, but for which C semantics are not defined, in any manner they see fit, with the consequence that actions which disturb such storage could have arbitrary unpredictable effects, and the same principle would apply to abstract data structures.
Aside from the forms of UB that would follow as natural consequences of implementations being able to use private storage however they see fit, or of any action that would cause the environment to behave in a manner contrary to the translator's documented requirements, what other forms of "fundamentally" undefined behavior would there be?
While it may be useful to have implementations deviate from canonical behaviors to assist in diagnostics, that should be accommodated not by classifying as UB situations where diagnostics might be useful, but rather by specifying that "diagnostic" implementations may suspend or termiante program execution for...
any documented reason they see fit. In a new language, there should be distinct ways of specifying operations like
arr[x][y]
depending upon whether y
is expected to index just within a row (in which case trapping for accesses beyond the row would be useful), or arbitrary spans within the larger array (in which case an implementation shouldn't trap)...
1 hour later…
16:35
Additionally, most useful kinds of back-end optimization could be facilitated by an abstraction model that load and store operations to be tagged with one or more identifiers, and allowed compilers to reorder loads and stores as desired provided it honored "acquire" and "release" instructions associated with loads/stores having the indicated identifiers [or all loads/stores].
Instead of compilers having to try to figure out whether there would be any corner cases by which load and store operations could access the same storage without invoking UB, compilers would merely have to check whether any execution paths between a load and store had acquire/release barriers that would preclude reordering. Programmers, meanwhile, would merely have to ensure that language structures would create barriers where needed.
« first day last day (15 days later) »
Transcript for
Jul17
Jul '2318
Aug2
Discussion on answer by André L F S B…
Imported from a comment discussion on langdev.stackexchange.co...