7 ms·
#include <stdio.h> #include <stdlib.h> int shift(int x, int n) { return x << n; } int main(int argc, char **argv) { printf("%d\n", shift(1, 32));
by mirashii 4d ago
#include <stdio.h>
#include <stdlib.h>
int shift(int x, int n) { return x << n; }
int main(int argc, char **argv) {
printf("%d\n", shift(1, 32)); /* n == width: UB */
return 0;
}
This program exhibits UB in Fil-C, and you can see that the optimizer does different things at -O0 (outputs 1) and -O1/-O2/-O3 (outputs 0). Since this creates poison, which Fil-C doesn't remove, you can use it to construct all kinds of weird things.
static void loop(void) {
int s = shift(1, 32);
int n = 0;
for (int i = 0; i < s + 3; i++)
n++;
printf("[loop] iterations=%d (s+3=%d)\n", n, s + 3);
}
static void sw(void) {
switch (shift(1, 32)) {
case 0: puts("[switch] case 0"); break;
case 1: puts("[switch] case 1"); break;
default: puts("[switch] default"); break;
}
}
int main(int argc, char **argv) {
loop();
sw();
return 0;
}
In Fil-C -O0, this gives 4 iterations of the loop and executes sw(). At any higher optimization level, it turns loop() into an infinite loop and drops sw() from the binary entirely.
- tialaramex 4d agoNeither of those sound like Undefined Behaviour to me, they're maybe unspecified but they don't sound undefined at all - are you confusing Undefined Behaviour with "I wanted it to do something else" ? You said "explode" earlier and so I was expecting something a bit more dramatic than "Unsurprisingly Fil-C has unspecified results for some expressions".
- mirashii 4d agoYou seem to be trying to apply some colloquial definition of undefined behavior. Undefined behavior is a very specific, defined term. The shift(1, 32) call is by definition undefined behavior (See 6.5.7 in the C standards from C99 up). The behavior of the program itself when undefined behavior is invoked is allowed to be _anything_. I've simply demonstrated here that the compiler is using the fact that there is undefined behavior to perform optimizations that would not be allowed without undefined behavior. Those optimizations are allowed to result in the program doing anything at the compiler's whim.
- tialaramex 4d agoI guess I've learned all I was going to, Fil-C does exactly what I understood and some crazy people will insist somehow that doesn't count. Good luck to you.
- uecker 4d agoTo decide what is defined or not, you need to reference the relevant specification. The specification is not ISO C, but ISO C plus additional guarantees by Fil-C.
- mirashii 4d agoI reference C's here because it gives the best idea of what clang/LLVM itself is going to consider UB and the bulk of the optimization semantics (where they haven't been changed by Fil-C), since Fil-C is really a fork with some additional passes and transforms built-in.
- uecker 4d agoFil-C is a fork of clang that explicitly defines all these things to have bounded behavior, so ignoring exactly this - the whole point of Fil-C - makes no sense.
- mirashii 4d agoBut what are "all these things"? It does not define away all of the UB that C/LLVM has for sure, nor does it turn all UB into crashes, which I demonstrated above and is the inaccurate description of Fil-C that spawned this. But beyond that, it's my belief in all of this is that leaving some UB behavior while trying to state a global correctness property puts those guarantees at risk. I spent a few minutes poking just to see if my gut is right here, and already, here's an example of UB being used in an optimization by the compiler that leaves a fil safety check at on -O0 but drops it at higher optimization levels. I find it difficult to believe that all of the complex interactions of every optimization pass in the presence of even this subset of UB are guaranteed not to violate these memory safety promises. #include <stdio.h> #include <stdlib.h> __attribute__((noinline)) static void poke(int *p, int k) { int n = 32 + (k & 15); /* always >= 32: shifting an int by >= 32 is UB */ p[(1 << n) * 20] = 0x41414141; /* on x86 the CPU computes index 40, out of bounds */ } int main(int argc, char **argv) { int *p = calloc(16, sizeof(int)); poke(p, argc); puts("after poke"); return 0; }