Quick Summary: The C compiler optimizer replaces printf("Hello World!\n") with puts("Hello World!\n") and the implicit return from main() changes from 13 (the return value of printf) to 10 (the return value of puts)
This sounds like a compiler optimizing bug, in that if the expected behavior of main without a return is that it will return the result of the last function call, then the last function in the main does use its return value and that should not be optimized.
There is no 'printf'
71–80 of 158 posts
Re: There is no 'printf'
#72Earlier quoted context omitted.
Optimizers have to keep the same input/output pairs unless there is undefined behavior. In the second function the truth table looks like: in | out ---------- 0b000 | 1 0b001 | 0 0b010 | 1 0b011 | 0 0b100 | don't care . . . MAX | don't care The compiler just chooses the most efficient way it knows to get the filled out entries correct which happens to be: in | ~in[0] ---------- 0b000 | 1 0b001 | 0 0b010 | 1 0b011 | 0…
The first function (the `n == 0 || !isEven(n+1)` recursive function) has defined behavior for negative numbers. That's probably why it compiled to an even number check.
[0]: I did the proof below for anyone interested:
int isEven(unsigned int n) {
return n == 0 || !isEven(n + 1);
}
# assumptions
MAX + 1 --> 0 # from the C standard
MAX is odd # true for most machines
# base case
isEven(0) --> True # trivial short circuit
# base case
isEven(MAX):
==> return MAX == 0 || !isEven(MAX+1) # MAX is not zero
==> return False || !isEven(MAX+1) # MAX + 1 rolls over
==> return False || !isEven(0)
==> return False || !True
==> return False || False
==> return False
==> isEven(MAX) --> False
# recursive steps
isEven(n) where 0 return n == 0 || !isEven(n + 1) # n cannot be zero
==> return False || !isEven(n + 1) # False || anything is tautological
==> return !isEven(n + 1)
which will turn into an inverter chain of length MAX - n until you reach isEven(MAX)
- an odd length inverter chain is the same as a single inversion
- an even length inverter chain is the identity
(can be proven by induction trivially)
isEven(n) where 0 return !isEven(n + 1) # MAX - n is odd, when n is even; replace with single inversion before isEven(MAX)
==> return !isEven(MAX)
==> !(False) --> True
isEven(n) where 0 return !isEven(n + 1) # MAX - n is even, when n is odd; replace with identity of isEven(MAX)
==> return isEven(MAX)
==> False --> False
All cases of n are accounted for so the function is correct (and equivalent to return n & 1 == 0).
[1]: Yup clang just recognizes this optimization: https://gcc.godbolt.org/z/Tc1MTa6njRe: There is no 'printf'
#73Re: There is no 'printf'
#74> But what if you're not using C99 or newer? If you're using C90, but under an implementation that supports C99, that implementation should obey all the new rules in all areas where there is no conflict between C90 and C99. The ISO C90 standard is obsolescent, so the fact that dropping off the end of main with no return value is an unspecified termination status is an obsolescent requirement (or non-requirement). It…
Regardless of how you think a modern compiler in C99 mode should behave, the fact is that "gcc -std=c90" generates code that returns an arbitrary value from main if no return or exit is executed. clang doesn't do this as far as I can tell -- though it's difficult to be sure, since returning a status of 0 is valid C90 behavior.
Re: There is no 'printf'
#75Even on freestanding environments they can and will generate calls to memcpy and memmove. That's insane...
Re: There is no 'printf'
#76Earlier quoted context omitted.
My guess: since overflowing int is UB, and the only value of n that stops the recursion is zero, the compiler assumes that n must be zero and checks accordingly. That doesn’t explain why it uses test dil, 1 instead of test dil, dil or cmp 0 or whatever.
The compiler cannot assume that much, because the argument is a signed integer (negative integers will not overflow and do have well-defined behaviour).
Re: There is no 'printf'
#77> But what if you're not using C99 or newer? If you're using C90, but under an implementation that supports C99, that implementation should obey all the new rules in all areas where there is no conflict between C90 and C99. The ISO C90 standard is obsolescent, so the fact that dropping off the end of main with no return value is an unspecified termination status is an obsolescent requirement (or non-requirement). It…
Regardless of how you think a modern compiler in C99 mode should behave, the fact is that "gcc -std=c90" generates code that returns an arbitrary value from main if no return or exit is executed. clang doesn't do this as far as I can tell -- though it's difficult to be sure, since returning a status of 0 is valid C90 behavior.
More than 22 years ago, ISO C introduced this requirement for a reason. The reason was almost certainly the desire to fix that issue for as many C programs as possible, and that there was no intent that there be an exceptions for C programs that happen to request C90 compatibility in their accompanying Makefile.
An indeterminate termination status causes real problems like, oh, if those programs are run out of POSIX script in "set -e" mode, the script will randomly terminate at that point. It has consequences.
This requirement plugged a hole; when those programs are recompiled with compiler which adopts the requirement, that issue is fixed.
From the developer's perspective, using GCC in C90 mode does mean they should be prepared to conform to C90 and not do things like that falling off the end of main if there is any risk that the termination status matters. In my original comment above, I made remarks to that effect.
But that developer isn't the only stake holder. A developer working in C90 can still accidentally forget the return, and some downstream user who doesn't even program in C is affected by that.
We also have to think from the point of view of a that downstream user building and operating a program received from such a developer. The user wants a program that has a successful termination status when it terminates normally an de facto successfully; the user doesn't care whether that comes from the compiler, or whether the program takes care of it.
It's pretty shoddy from that not to come from the compiler, almost a quarter century after it was declared that, in the C language, "int main(void) {}" is now a successfully terminating program!
When you request dialect compatibility from an implementation, you don't want old bugs to come back, unless they are specifically required. Your downstream user doesn't want that, certainly.
If you really want old bugs, you go get the actual old compiler; -std=c90 passed to GCC 11 does not mean "emulate every detail of GCC 1.x".
Re: There is no 'printf'
#78Re: There is no 'printf'
#79Compiler optimization is really annoying sometimes. It's allowed to assume that functions with the same name as standard library functions behave according to standard. They'll swap out printf calls for puts if they don't have a format. The compiler just knows it can do that. Sure, it optimizes things but it gets to the point the code no longer reflects what's written on the source file. Try to hook into printf and i…
If I write printf("Hello, world\n"), all I care about is that those characters are written to the standard output stream when I run the program -- and that's all the language standard specifies. I rarely even look at the assembly or machine code.
As someone else mentioned, "gcc -fno-builtin" inhibits optimizations like this. Other compilers are likely to have similar options.
Re: There is no 'printf'
#80> But what if you're not using C99 or newer? If you're using C90, but under an implementation that supports C99, that implementation should obey all the new rules in all areas where there is no conflict between C90 and C99. The ISO C90 standard is obsolescent, so the fact that dropping off the end of main with no return value is an unspecified termination status is an obsolescent requirement (or non-requirement). It…
A compatibility with a code which depends on a behaviour of a previous version of the compiler is a reason enough.
If compatibility is dropped you now have a very subtle bug which is hard to discover and understand unless you read about that particular GNUism before. It's also a perfect setup for Schrödinger's bug.