关于gcc:这个C函数应该总是返回false,但事实并非如此

This C function should always return false, but it doesn’t

很久以前我在一个论坛上偶然发现了一个有趣的问题,我想知道答案。

考虑以下C函数:

F1.C

1
2
3
4
5
6
7
8
9
#include <stdbool.h>

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

这应该总是返回false,因为var3 == 3000main函数如下:

主C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdbool.h>

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

由于f1()应该总是返回false,所以我们希望程序在屏幕上只打印一个错误。但在编译和运行之后,还会显示Executed:

1
2
3
4
$ gcc main.c f1.c -o test
$ ./test
false
executed

为什么会这样?此代码是否有某种未定义的行为?

注:我是用gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2编译的。


如其他答案所述,问题是您使用的gcc没有设置编译器选项。如果您这样做,它将默认为所谓的"GNU90",这是1990年撤销的旧C90标准的非标准实现。

在旧的C90标准中,C语言存在一个主要缺陷:如果在使用函数之前没有声明原型,那么它将默认为int func ()(其中( )表示"接受任何参数")。这改变了函数func的调用约定,但并没有改变实际的函数定义。由于boolint的大小不同,因此在调用函数时,代码会调用未定义的行为。

这种危险的胡说八道的行为是在1999年修正的,随着C99标准的发布。隐式函数声明被禁止。

不幸的是,GCC到5.x.x版仍然默认使用旧的C标准。可能没有理由要将代码编译为标准C之外的任何代码。因此,您必须明确地告诉GCC,它应该将代码编译为现代C代码,而不是25岁以上的非标准GNU垃圾代码。

通过将程序编译为:

1
gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11告诉它要半心半意地按照(当前的)C标准(非正式地称为c11)编译。
  • -pedantic-errors告诉它要全心全意地做上述工作,并且在编写违反C标准的错误代码时会给编译器带来错误。
  • -Wall的意思是给我一些额外的警告,这可能是好事。
  • -Wextra的意思是给我一些其他可能有好处的额外警告。


您没有在main.c中为f1()声明原型,因此它隐式定义为int f1(),这意味着它是一个接受未知数量参数并返回int的函数。

如果intbool的大小不同,将导致行为不明确。例如,在我的机器上,int是4个字节,bool是1个字节。因为函数被定义为返回bool,所以当它返回时,会在堆栈上放置一个字节。但是,由于它被隐式声明为从main.c返回int,调用函数将尝试从堆栈中读取4个字节。

gcc中的默认编译器选项不会告诉您它正在这样做。但是,如果使用-Wall -Wextra编译,您将得到:

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

要解决此问题,请在main.c中的f1前面添加一个声明,该声明位于main之前:

1
bool f1(void);

请注意,参数列表显式设置为void,它告诉编译器函数不接受参数,而空参数列表则意味着未知数量的参数。f1.c中的f1定义也应该改变以反映这一点。


我觉得有趣的是,看看Lundin的优秀答案中提到的大小不匹配实际上发生在哪里。

如果使用--save-temps编译,您将获得可以查看的程序集文件。下面是f1()进行== 0比较并返回其值的部分:

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

返回部分为sete %al。在C的x86调用约定中,返回值4字节或更小(包括intbool通过寄存器%eax返回。%al%eax的最低字节。因此,%eax的上3个字节处于不受控制的状态。

现在在main()中:

1
2
3
call    f1
testl   %eax, %eax
je  .L2

这将检查整个EDOCX1[6]是否为零,因为它认为它正在测试一个int。

添加显式函数声明会将main()更改为:

1
2
3
call    f1
testb   %al, %al
je  .L2

这就是我们想要的。


请使用如下命令编译:

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

输出:

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

有了这样的信息,您应该知道如何纠正它。

编辑:在阅读(现在已删除)注释后,我试图编译没有标志的代码。嗯,这导致了链接器错误,没有编译器警告,而不是编译器错误。而那些链接器错误更难理解,所以即使不需要-std-gnu99,请尽可能多地使用-Wall -Werror,这会让你省去很多麻烦。