A modern CPU willy nilly reorders instructions, stores, and creates new "virtual" registers out of thin air to dissolve stall inducing dependency chains. It can also split instructions, fuse them together and even entirely remove some instructions -- as long as it doesn't affect the end result.
Generally (within limits of CPU memory model) the only thing you're guaranteed is that eventually the result is what you'd expect from sequential execution. That of course applies on the core you're running on, otherwise you naturally need to synchronize with other cores.
Luckily there are tools to find out, which work to some extent, IF you can run it on same CPU model (and really, whole system, incl. memory subsystem!) as what you're interested in.
a = 10;
b = 20;
c = b + 30;
bar = a + b;
x = 1;
y = 2;
z = y + 3;
foo = x + y;
... in order to remove dependency stalls might be executed as: b = 20;
y = 2;
a = 10;
x = 1;
c = b + 30;
z = y + 3;
bar = a + b;
foo = x + y;
This would still be true, even if 'x' and 'a', 'y' and 'b' etc. variables (well, registers) had same name. In that case CPU would just make up new "variables" as required and do the same transformation!Note that several decades ago, there were processors that weren't capable of actually keeping the state correct after a processor exception, so if you got a division-by-0 error, your program counter had advanced by 30 or so. (This is, I believe, part of the reason why traps are undefined behavior in C: if your processor can't guarantee any state after a machine trap, it's impossible to implement a programming language that has even the loosest guarantees).
https://en.wikipedia.org/wiki/Tomasulo_algorithm.
So no, the consistent serialized ISA-conforming state might be reconstructed by replaying after the exception (interrupt) arrives.
Alternatively, interrupt might be served only after execution reaches the next checkpoint.
On x86, a lot of instructions are close matches, yet some are removed entirely (say "xor eax, eax") or fused into one micro-op, (like "cmp #123, eax / je <address>").
Future CPUs might even do some data flow analysis to optimize code even further.
Say speculative "constant" folding based on runtime profile to remove chunks of code from hot inner loops.
Or to replace longer instruction patterns with HW optimized implementation, if that's what it takes to get some extra performance for the next year's model.
See the diagram here https://software.intel.com/sites/default/files/managed/9e/bc...
One of those only worth of time to learn as hardware engineers, but understanding both of them will give you an introduction to Spectre vulnerability.
what do you mean by these two? not sure about the former. as for the latter, of course you can read uninitialized memory, unless you mean something else?
Edit: Some excerpts from the C standard:
6.7.8 Initialization
10 If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate.
3.17.2 indeterminate value
either an unspecified value or a trap representation
3.17.3 unspecified value
valid value of the relevant type where this International Standard imposes no requirements on which value is chosen in any instance
6.2.6 Representation of types
6.2.6.1 General
5 Certain object representations need not represent a value of the object type. … Such a representation is called a trap representation.
This page says value is indeterminate, which is either unspecified or a trap, as you say: https://wiki.sei.cmu.edu/confluence/display/c/EXP33-C.+Do+no...
But.
This part says that reading an indeterminate value is, in fact, undefined behavior (line 11 in the table): https://wiki.sei.cmu.edu/confluence/display/c/CC.+Undefined+...
From 6.2.6.1¶5:
>Certain object representations need not represent a value of the object type. If the stored value of an object has such a representation and is read by an lvalue expression that does not have character type, the behavior is undefined. ... Such a representation is called a trap representation.
A read from uninitialized memory is not always UB.
Per http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_451.htm , the current standard is unclear in some respects, but the latest committee view is that that under the current standard any library function (including memcpy) may exhibit undefined behaviour when called with uninitialized memory, even when the uninitialized memory is of character type or is propagated through values of character type.
int x;
if(x == 0) foo();
if(x != 0) foo();
That is reading the uninitialized value twice. Since it is unspecified, it does not have to be consistent, so you could get the same behavior as non-zero for the first reading, and zero for the second reading. Changing your code to: int x;
if(x == 0) foo();
else foo();
will give different output (same if you use !=).In the compiler discovers UB, the Standard places no requirements of any kind on the program or compiler. It is free to launch missiles, or (more likely) assume this code cannot be reached, and omit it from the program, along with any code that reaches it unconditionally, and any check that would send control that way. Such elision is the basis for many important optimizations.
Implementations are free to define things left undefined by Standards. For example, "#include <unistd.h>" is UB by the ISO Standard, but defined by Posix, which implementations also adhere to.
https://stackoverflow.com/questions/11962457/why-is-using-an...
What is the point of not making it UB? You can't AFAICS possibly rely on any useful way on it behaving 'precisely and consistently' so just make it UB anyway?
int a; printf("%d", (&a + sizeof(int)));
> The value of an object with automatic storage duration is used while it is indeterminate.
But reading the value without using it seems fine?
For the second, https://www.ralfj.de/blog/2019/07/14/uninit.html
(the second one is a bit more Rust focused but the core idea is the same)
Let me quote wikipedia here:
> The Pentium 4 can have 126 micro-operations in flight at the same time. Micro-operations are decoded and stored in an Execution Trace Cache with 12,000 entries, to avoid repeated decoding of the same x86 instructions. Groups of six micro-operations are packed into a trace line. Complex instructions, such as exception handling, result in jumping to the microcode ROM. During development of the Pentium 4, microcode accounted for 14% of processor bugs versus 30% of processor bugs during development of the Pentium Pro.
Like, that's one paragraph, there are others I can cherry pick to show just how complicated the execution of microcode is. Unless you're an actual engineer on processor development and have insider info, I find it highly suspect that you could look at a decently sized chunk of assembly code and really know what happens with the microcode.
Sure you can have a table that says, it takes ballpark about this long. Ok fine, but that's not the same thing as assembly mapping cleanly to what the processor is actually doing.
Micro-ops are part of the micro-architecture of the processor, and are in hardware. They are not patchable and are not software.
But yeah, microcoded instructions are relatively rarely executed.
People have this common misconception that the programmable micro-code is what your CPU is actually executing, and x86 somehow translates into instructions for it, and this was really just because of a conflation of the terms "micro-code" and "micro-op".
Admittedly, Intel isn't the best at this term either. They have several places in the Architecture Manuals where they refer to the "micro-code synthesizer" when they mean "micro-op synthesizer"; this really has nothing to do with the micro-code ROM.
Also AIUI the microcode controls the issuing of the micro-ops.
> It would be way too slow to make that programmable
Then what is the "processor microcode updates" updating? I think this may just be a terminology mixup.
Dunno if this helps, FYI from https://stackoverflow.com/questions/17395557/observing-stale...
...and I can't copy/paste it. In the above link, look for 'embarrassing' by Krazy Glew.
https://twitter.com/uops_info/status/1202950247900684290 https://github.com/llvm-project/llvm/blob/master/lib/Target/...
printf("%x", *(int*)0x12345678);In short, how much does the concept of a "well-defined C program" differ from the concept of a "C program" as implemented in practice? Can we say that most C programs, or even a medium-sized percentage of C programs, are well-defined?
Implicit in the Standard is the notion of invalid pointers. In discussing pointers, the Standard typically refers to “a pointer to an object” or “a pointer to a function” or “a null pointer.” A special case in address arithmetic allows for a pointer to just past the end of an array. Any other pointer is invalid.
An invalid pointer might be created in several ways. An arbitrary value can be assigned (via a cast) to a pointer variable. (This could even create a valid pointer, depending on the value.) A pointer to an object becomes invalid if the memory containing the object is deallocated or moved by realloc. Pointer arithmetic can produce pointers outside the range of an array.
Regardless how an invalid pointer is created, any use of it yields undefined behavior. Even assignment, comparison with a null pointer constant, or comparison with itself, might on some systems result in an exception.
I'm not a language lawyer, but I suspect this means that even initialization to the wrong literal might well be undefined behavior. What makes you confident that it's not, and instead is merely implementation defined?
What if it's not an "invalid pointer", but a pointer to a memory-mapped IO address, ROM, etc? I grew up learning C on 16-bit machines in the early 90's. Hard coded pointer values were very, very common.
> What if it's not an "invalid pointer", but a pointer to a memory-mapped IO address, ROM, etc?
Yes, this is central to the question. And how is the compiler to know? Is it safe to presume that the compiler can't know, and thus can't presume undefined behavior? I think the answer is in the comments you linked where 'supercat' replies to 'Peter Cordes':
The Standard makes no attempt to mandate that all implementations be suitable for low-level systems programming, nor does it in any way imply that it's possible to have a quality implementation that is suitable for low-level or systems programming without it supporting behaviors beyond those mandated by the Standard (and which might not be processed predictably by implementations that aren't suitable for systems programming).
Which is to say, yes, for a compiler implementation to actually be useful for low-level programming, it must behave in a predictable manner when given literal addresses. Unfortunately, it may be possible for a C compiler to be "standards conforming" without actually being useful for this purpose. One can only hope that at least some compilers will continue to "do the right thing" despite that lack of explicit requirements.
It is possible for a C implementation to be conforming witout supporting low-level programming. Sometimes it even makes sense, like if you're running C code on GraalVM's LLVM bitcode interpreter. But there is no trend of "standard" C implementations following this route. While modern C compilers like to aggressively exploit undefined behavior, they generally make reasonable decisions for implementation-defined behavior. In this case, all major compilers will compile
*(int*)0x12345678
to the obvious assembly, and what happens then depends on what your memory map looks like.(Caveat: the compiler will still perform normal optimizations like removing unused loads or redundant stores. If the address actually points to memory-mapped I/O, you need `volatile` to prevent that.)