This nonsense again. I don't get this "undefined behavior" cliche. It seems it became fashionable for some people to parrot it like a mantra as a form of signaling. Undefined behavior just refers to something that is not covered by the international standard, and therefore doesn't exist nor should be used, but an implementation may offer implementation-specific behavior.
I had the impression that some things are UB because they can't specify a single behavior that would be efficient across all platforms C targets.
According to the spec, it literally can. The compiler is free to replace your entire program with unrelated functionality. It can ever do a different thing each time it compiles the program.
There are implementation specific behaviors (# of bits in a char), which are different.
Implementation-defined behavior limits portability in the way you describe, but UB's not the same thing. IB should behave the same way in the same implementation. UB doesn't have to.
Undefined is out of the scope of the language entirely. Using a non-existent index into an array, for example. While you might reasonably expect the program will just look past the end, there is not guarantee it will do so. Optimizing compilers, in particular will assume such a thing cannot happen, and can assume a code branch that does something like this is impossible to reach and discard it entirely.
No one will "fix" such an optimization bug, because the code behind it is valid for ASTs that may have been put into that form from conforming code generated by macros and branches that wouldn't be called. There's nothing to fix.
You're telling it to do something impossible, and it's assuming it can't happen.
An example is what the C standard calls "trap representations": Bit patterns which fit into the space occupied by a specific type, but which will cause a hardware trap (exception, interrupt, what have you) if you actually store them in a variable of that type. The only type which cannot have trap representations is unsigned char. Basically, what it amounts to is this: C compilers don't compile to a runtime, they compile to raw machine code with, perhaps, a standard library. If you do something the hardware doesn't like when your program runs, well, the C compiler is long gone by that point and the C standard makes no guarantees.
More prosaically, storing to a location beyond the end of an array might not cause a segfault. It might corrupt some other array, it might cause a hardware crash, it might even corrupt the program's machine code. Because C is explicitly a language for embedded hardware, with no MMUs, no W^X protection, and no OSes, the C standard can say very little about such things.
Actually I didn't. My point was rather obvious: the whole point of the standards specifying UB is precisely to let implementations define the behavior themselves.
That's literally the definition of implementation-defined behavior.
Undefined behavior really means undefined; in terms of the C language, there are no constraints on behavior. Sure, you might get a result one way on one implementation, but if you rely on that you're technically writing a dialect of C, and need to let the compiler know using flags.
Legalistically, I guess it could (the behavior isn't defined, after all), but typically the optimizer makes some valid-only-if-the-code-is deductions and things snowball from there....
This used to be the case. Signed integer overflow for instance is undefined because some CPUs go bananas when you try that. Other platform performed 2's complement just fine, and we used to be able to rely on this.
No longer.
See, the standard doesn't say "implementation defined". It doesn't say "undefined on platforms that go bananas, implementation defined otherwise". It says "undefined" period.
Signed integer overflow is undefined on all platforms, even your modern x86-64 CPU. Compiler writers interpreted it as a licence to assume it never happens, to help optimisations. For instance:
int x = whatever;
x += small_increment;
if (x < 0) { // check for overflow
abort(); // security shut down
}
proceed(x); // overflow didn't happen, we're safe!
Here's what the compiler thinks: int x = whatever;
x += small_increment;
if (x < 0) { // only true if signed overflow -> false
abort(); // dead code
}
proceed(x);
Then the compiler simply deletes your security check: int x = whatever;
x += small_increment;
proceed(x);
Don't listen to Chandler Carruth, nasal demons are real. Some undefined behaviours can encrypt your whole hard drive, assuming they're exploitable by malicious inputs.Now, if you used
unsigned int x = whatever;
...
if(x < 0)
There would be an obvious case for removing that if. $ cat undefined.c
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int x = INT_MAX;
if (x+1 > x) {
printf("%d > %d\n", x+1, x);
} else {
printf("overflow!\n");
}
}
$ gcc --version
gcc (Ubuntu 7.2.0-8ubuntu3.2) 7.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc undefined.c && ./a.out
overflow!
$ gcc -O3 undefined.c && ./a.out
-2147483648 > 2147483647 $ cat undefined.c
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int x = INT_MAX;
if (x+1 < 0) {
printf("%d < 0\n", x+1);
} else {
printf("overflow!\n");
}
}
$ gcc --version
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc undefined.c && ./a.out
-2147483648 < 0
$ gcc -O3 undefined.c && ./a.out
overflow!
The security check is gone all the same.There are cases where the compiler removes misguided overflow checks, since "perform UB, then check whether it happened" doesn't actually work, but your example is not such a case.
This raises an interesting point, because in many cases, assuming 2's complement and wrapping around, checking for overflow after the fact, as opposed to preventing it from happening in the first place, is actually easier. (And actually works if you use the `-fwrap` flag.)
The right thing should be easier to do than the wrong thing. It's a shame this is not the case here.
This is entirely different than implementation defined in that a conforming compiler has to document the behavior they implement and do it consistently.
For example, there are quite a few people who would like to see a C where you can't actually write this:
// int x, y;
if(++x < y) { ... }
...because, well, the behavior of integer overflow is undefined in C, so that code could technically do anything, even though it seems perfectly innocent, especially when coming from a checked language.Of course, you can't do anything in the C standard to require that this code work as-is, because the C standard applies to architectures where mutually-exclusive things happen under integer overflow. But you can always just disallow it completely, and require that people use intrinsics that are explicit about what overflow behavior they expect (where that behavior reduces to plain output on target architectures that follow it, and to a shim on target architectures that don't. You know, like floating-point support, or atomics.)
I don't believe that's the case for plenty of reasons, such as:
- compilers already do that ( yeah, it's one of those RTFM things. See for example GCC's undefined behavior sanitizer)
- the standard already specifies exactly what it is left undefined, thus it's a compiler-related issue (see point above)
Let's face it: some people mindlessly parrot the"undefined behaviour" mantra just for show.
I believe 99% of what people care about the C standard doing, re: UB handling, is requiring compilers to make certain behaviours the default, rather than hidden behind different flags that C newbies who don't understand UB (who thus code most of the bad C!) won't ever set.
well, no they don't. UBSan is at run-time because most UB is impossible to catch at compile-time.
No compiler is going to go out of its way to compile an increment into the machine's usual increment instruction and an additional overflow check that does whatever, just because it can. It's going to compile it into the machine's usual increment instruction and what happens on overflow is what happens naturally.
It's as absurd as claiming that even "x + y" can invoke undefined behaviour, because while the standard allows it, any compiler that compiles such an addition to anything other than the machine's addition instruction (i.e. with implementation-defined effects), to speak nothing of adding the additional(!) checks to deliberately do something else, is clearly not benefiting anyone.
"In theory, there is no difference between theory and practice. In practice, there is." Pure fearmongering, IMHO.
In order to get maximum performance, the compiler is allowed to assume that the programmer doesn't invoke undefined behavior. In other words, it can replace code with something that is equivalent in the presence of UB, but does something totally different in the absence of UB. See e.g. https://blog.regehr.org/archives/767 for some examples of how this can go wrong. (My favorite is the third one.)
What people are actually worried about is when the compiler starts removing - not adding - seemingly unrelated code in a hard to reason about fashion. And compilers absolutely will go out of their way to do this in the name of optimization and performance. And it will do this because it got smart enough to prove that the "unrelated" code can't run without first technically invoking undefined behavior, at which point it can jump to the wild conclusion that it must never actually execute (or that it can remove the code even if it does, because it's legal for the compiler to do anything after invoking undefined behavior - including not execute that code!)
Sometimes the removed code is important security checks, leading to CVEs, hotpatches, etc. - this is not theoretical, and is not remotely new at this point: https://www.grsecurity.net/~spender/exploits/cheddar_bay/exp...
It also makes reporting compiler bugs annoying, as you first have to definitively prove to yourself and the compiler guys that you've actually got a compiler bug, rather than a compiler "feature" of aggressive optimization within the letter of the C++ standard. It's only out of pure stubbornness that https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84658 got reported upstream, I was assuming it was UB in our codebase most of the way down and thus INVALID as a compiler bug...
But perhaps CVEs and expected behavior being borderline indistinguishable from compiler bugs to most C and C++ programmers I know is just "fear mongering" as you say. IMNSHO, it's not \o/
See https://blogs.msdn.microsoft.com/oldnewthing/20140627-00/?p=... for a very detailed example.
See https://blog.regehr.org/archives/1307 for some strict aliasing examples.
The problem is that you can do everything in portable C that you can do with undefined behavior in C, and often in a more straightforward fashion.[1] The compiler won't tell you that you are doing it wrong; there are many examples, tutorials, and books that encourage you to do the wrong thing. The undefined behavior will work until you switch to a different system. Why allow the wrong thing to continue to happen?
[1] A very good example of this is endianness:
https://news.ycombinator.com/item?id=16189110
https://commandcenter.blogspot.se/2012/04/byte-order-fallacy...
Undefined behavior just refers to something that is not covered by the international standard, and therefore doesn't exist nor should be used, but an implementation may offer implementation-specific behavior
Indeed. Even the standard itself, to quote its definition of undefined behaviour (emphasis mine):
"behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message)."
The fact that the standard "imposes no requirements" should not be taken as carte blanche to completely ignore the intent of the programmers and do entirely unreasonable things "just because you can", yet unfortunately quite a few in the compiler/programming language community think that way.
It's why I'm very encouraging of more programmers writing their own compilers and exploring new techniques, to move away from that suffocating and divisive culture. Compilers should be helping users, not acting aggressively against them just because it's allowed by one standards committee.
Other language communities that put correctness before performance at any cost, don't share this mentality, including the compiler writers.
Could you please provide a code snippet of this kind? Hard for me to visualize otherwise. Thanks.
typedef int (*my_func_ptr_t)(const void*,const void*);
typedef union { my_func_ptr_t func_ptr; int* data; } my_union_t;
void my_func (my_union_t param);
// Now exploiding it
void my_func (union { int (*func_ptr)(const void*,const void*); int* data; } param);
Of course this is very basic example, but there is hope to make it into an IOCC entry.