327

I stumbled over an interesting question in a forum a long time ago and I want to know the answer.

Consider the following C function:

f1.c

#include <stdbool.h>

bool f1()
{
    int var1 = 1000;
    int var2 = 2000;
    int var3 = var1 + var2;
    return (var3 == 0) ? true : false;
}

This should always return false since var3 == 3000. The main function looks like this:

main.c

#include <stdio.h>
#include <stdbool.h>

int main()
{
    printf( f1() == true ? "true\n" : "false\n");
    if( f1() )
    {
        printf("executed\n");
    }
    return 0;
}

Since f1() should always return false, one would expect the program to print only one false to the screen. But after compiling and running it, executed is also displayed:

$ gcc main.c f1.c -o test
$ ./test
false
executed

Why is that? Does this code have some sort of undefined behavior?

Note: I compiled it with gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.

6
  • 11
    Others have mentioned you need a prototype because your functions are in separate files. But even if you copied f1() into the same file as main(), you'd get some weirdness: While it is correct in C++ to use () for an empty parameter list, in C that is used for a function with a not-yet-defined parameter list (it basically expects a K&R-style parameter list after the )). To be correct C, you should change your code to bool f1(void).
    – uliwitness
    Commented Apr 8, 2016 at 12:21
  • 1
    The main() could be simplified to int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; } – this would show the discrepancy better.
    – Palec
    Commented Apr 10, 2016 at 12:05
  • @uliwitness What about K&R 1st ed. (1978) when there was no void?
    – Ho1
    Commented Apr 11, 2016 at 16:39
  • @uliwitness There were not true and false in K&R 1st ed., so there were not such problems at all. It was just 0 and non-zero for true and false. Isn't it? I don't know if prototypes were available at that time.
    – Ho1
    Commented Apr 11, 2016 at 16:45
  • 2
    K&R 1st Edn preceded prototypes (and the C standard) by more than a decade (1978 for the book vs 1989 for the standard) — indeed, C++ (C with Classes) was still in the future when K&R1 was published. Also, prior to C99, there was no _Bool type and no <stdbool.h> header. Commented Apr 13, 2016 at 6:50

4 Answers 4

412

As noted in other answers, the problem is that you use gcc with no compiler options set. If you do this, it defaults to what is called "gnu90", which is a non-standard implementation of the old, withdrawn C90 standard from 1990.

In the old C90 standard there was a major flaw in the C language: if you didn't declare a prototype before using a function, it would default to int func () (where ( ) means "accept any parameter"). This changes the calling convention of the function func, but it doesn't change the actual function definition. Since the size of bool and int are different, your code invokes undefined behavior when the function is called.

This dangerous nonsense behavior was fixed in the year 1999, with the release of the C99 standard. Implicit function declarations were banned.

Unfortunately, GCC up to version 5.x.x still uses the old C standard by default. There is probably no reason why you should want to compile your code as anything but standard C. So you have to explicitly tell GCC that it should compile your code as modern C code, instead of some 25+ years old, non-standard GNU crap.

Fix the problem by always compiling your program as:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 tells it to make a half-hearted attempt to compile according the (current) C standard (informally known as C11).
  • -pedantic-errors tells it to whole-heartedly do the above, and give compiler errors when you write incorrect code which violates the C standard.
  • -Wall means give me some extra warnings that might be good to have.
  • -Wextra means give me some other extra warnings that might be good to have.
21
  • 22
    This answer is overall correct, but for more complicated programs -std=gnu11 is much more likely to work as expected than -std=c11, due to any or all of: needing library functionality beyond C11 (POSIX, X/Open, etc) that is available in the "gnu" extended modes but suppressed in the strict conformance mode; bugs in system headers that are hidden in the extended modes, e.g. assuming availability of nonstandard typedefs; unintentional use of trigraphs (this standard misfeature is disabled in "gnu" mode).
    – zwol
    Commented Apr 7, 2016 at 14:51
  • 7
    For similar reasons, while I generally encourage use of high warnings levels I cannot support the use of warnings-are-errors modes. -pedantic-errors is less troublesome than -Werror but both can and do cause programs to fail to compile on operating systems that are not included in the original author's testing, even when there is no actual problem.
    – zwol
    Commented Apr 7, 2016 at 14:55
  • 8
    @Lundin On the contrary, the second problem I mentioned (bugs in the system headers that are exposed by strict conformance modes) is ubiquitous; I have done extensive, systematic testing, and there are no widely used operating systems that do not have at least one such bug (as of two years ago, anyway). C programs that require only the functionality of C11, with no further additions, are also the exception rather than the rule in my experience.
    – zwol
    Commented Apr 7, 2016 at 15:10
  • 6
    @joop If you use standard C bool/_Bool then you can write your C code in a "C++-esque" way, where you assume that all comparisons and logical operators return a bool like in C++, even though they return an int in C, for historical reasons. This has the great advantage that you can use static analysis tools to check for type safety of all such expressions, and expose all kinds of bugs at compile-time. It is also a way to express intent in the form of self-documenting code. And less importantly, it also saves a few bytes of RAM.
    – Lundin
    Commented Apr 8, 2016 at 13:47
  • 8
    Note that most of the new stuff in C99 came from that 25+ year old GNU crap.
    – Shahbaz
    Commented Apr 10, 2016 at 16:04
148

You don't have a prototype declared for f1() in main.c, so it is implicitly defined as int f1(), meaning it is a function that takes an unknown number of arguments and returns an int.

If int and bool are of different sizes, this will result in undefined behavior. For example, on my machine, int is 4 bytes and bool is one byte. Since the function is defined to return bool, it puts one byte on the stack when it returns. However, since it's implicitly declared to return int from main.c, the calling function will try to read 4 bytes from the stack.

The default compilers options in gcc won't tell you that it's doing this. But if you compile with -Wall -Wextra, you'll get this:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

To fix this, add a declaration for f1 in main.c, before main:

bool f1(void);

Note that the argument list is explicitly set to void, which tells the compiler the function takes no arguments, as opposed to an empty parameter list which means an unknown number of arguments. The definition f1 in f1.c should also be changed to reflect this.

6
  • 2
    Something I used to do in my projects (when I still used GCC) was add -Werror-implicit-function-declaration to GCC's options, that way this one doesn't slip past anymore. An even better choice is -Werror to turn all warnings into errors. Forces you to fix all warnings when they show up.
    – uliwitness
    Commented Apr 8, 2016 at 12:41
  • 2
    You also shouldn't use empty parenthesis because doing so is an obsolescent feature. Meaning they can ban such code in the next version of the C standard.
    – Lundin
    Commented Apr 8, 2016 at 13:52
  • 1
    @uliwitness Ah. Good information for those coming from C++ who only dabble in C. Commented Apr 8, 2016 at 18:00
  • The return value is usually not put into the stack, but into a register. See Owen's answer. Also, you usually never put one byte into the stack, but a multiple of the word size.
    – rsanchez
    Commented Apr 9, 2016 at 22:00
  • Newer versions of GCC (5.x.x) give that warning without the extra flags.
    – Overv
    Commented Apr 12, 2016 at 19:20
39

I think it's interesting to see where the size-mismatch mentioned in Lundin's excellent answer actually happens.

If you compile with --save-temps, you will get assembly files that you can look at. Here's the part where f1() does the == 0 comparison and returns its value:

cmpl    $0, -4(%rbp)
sete    %al

The returning part is sete %al. In C's x86 calling conventions, return values 4 bytes or smaller (which includes int and bool) are returned via register %eax. %al is the lowest byte of %eax. So, the upper 3 bytes of %eax are left in an uncontrolled state.

Now in main():

call    f1
testl   %eax, %eax
je  .L2

This checks whether the whole of %eax is zero, because it thinks it's testing an int.

Adding an explicit function declaration changes main() to:

call    f1
testb   %al, %al
je  .L2

which is what we want.

0
27

Please compile with a command such as this one:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

Output:

main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
     printf( f1() == true ? "true\n" : "false\n");
     ^
cc1.exe: all warnings being treated as errors

With such a message, you should know what to do to correct it.

Edit: After reading a (now deleted) comment, I tried to compile your code without the flags. Well, This led me to linker errors with no compiler warnings instead of compiler errors. And those linker errors are more difficult to understand, so even if -std-gnu99 is not necessary, please try to allways use at least -Wall -Werror it will save you a lot of pain in the ass.

0

Not the answer you're looking for? Browse other questions tagged or ask your own question.