隐式函数声明和链接

4

最近我学习了有关C语言中隐式函数声明的知识。主要思想很清楚,但我对这种情况下的链接过程有些困惑。

请考虑以下代码(文件a.c):

#include <stdio.h>

int main() {
    double someValue = f();
    printf("%f\n", someValue);
    return 0;
}

如果我尝试编译它:
gcc -c a.c -std=c99

我看到了一个有关函数 f() 隐式声明的警告。

如果我尝试编译和链接:

gcc a.c -std=c99

我遇到了一个未定义引用错误。但是一切都很好。

然后我添加了另一个文件(文件b.c):

double f(double x) {
    return x;
}

接下来执行以下命令:

gcc a.c b.c -std=c99

令人惊讶的是,一切都成功地链接了起来。当然,在调用./a.out后,我看到了一些垃圾输出。

那么,我的问题是:如何链接具有隐式声明函数的程序?在编译器/链接器的内部发生了什么?

我阅读了许多关于SO的主题,例如这个这个这个,但仍然存在问题。

4个回答

5
首先,自C99开始,隐式函数声明从标准中移除。编译器可能支持编译旧代码,但这不是必须的。引用标准前言,
remove implicit function declaration
话虽如此,根据C11的第6.5.2.2章节规定,
如果使用的函数没有包含原型类型,且在推广后参数的类型与推广后参数不兼容,则其行为是未定义的。
因此,在您的情况下,
- 函数调用本身是隐式声明(自C99以来变得非标准), - 由于函数签名不匹配 [隐式声明的函数被认为具有int返回类型],您的代码会导致未定义行为
值得一提的是,如果你尝试在同一个编译单元中在调用之后定义该函数,由于签名不匹配,你将会得到编译错误。
然而,由于您的函数定义在单独的编译单元中(并且没有原型声明),编译器无法检查签名。在编译之后,连接器采用对象文件,由于链接器中缺乏任何类型检查(并且对象文件中也没有信息),它将愉快地链接它们。最终,它将以成功的编译和链接以及UB结束。

1
感谢您提供详细的答案。 - Edgar Rokjān

3

以下是发生的情况。

  1. 如果没有声明f(),编译器会假设一个隐式声明,如int f(void)。然后愉快地编译a.c
  2. 在编译b.c时,编译器没有任何先前对f()的声明,因此它从f()的定义中推断出来。通常情况下,您会将f()的某些声明放在头文件中,并在a.cb.c中包含它。因为两个文件都看到相同的声明,编译器可以强制执行一致性。它会抱怨不符合声明的实体。但在这种情况下,没有公共原型可供参考。
  3. C中,编译器不会将有关原型的任何信息存储在目标文件中,链接器也不会执行任何一致性检查(它无法执行)。它所看到的只是a.c中的未解析符号f和在b.c中定义的符号f。它愉快地解决了这些符号,并完成了链接。
  4. 尽管运行时会出现问题,因为编译器根据它在那里假定的原型设置了a.c中的调用。这与b.c中的定义不匹配。f()(来自b.c)将从堆栈中获取一个垃圾参数,并将其返回为double,而在a.c中返回时将被解释为int

1
我认为编译器在处理隐式声明时会假定int f()而不是int f(void) - Edgar Rokjān
我会检查,但考虑到在 a.c 中的调用没有参数,假设 int f(void) 似乎更合理,因为 int f() 表示“任意数量的参数”。 - Ziffusion
2
隐式声明在现代C中是无效的,而且是未定义行为。只提供隐式声明如int f();,没有像int f(void);这样显式声明标准。编译器在编译b.c时不会“直觉”到函数的原型,也不需要提前定义。函数定义同时也提供了它的原型。如果之前提供了原型,则进行检查。否则,不会进行检查。 "Common prototype"不是一个概念。原型要么可用,要么不可用。这与编译器如何获取信息无关(通过头文件或实际定义等)。 - P.P
虽然(3)和(4)是好的,但在标准中它被定义为未定义行为 - P.P
@I3x,所有这一切都和问题无关,而且大多数都是挑剔我措辞的方式。但无论如何。 - Ziffusion

2
“带有隐式声明函数的程序如何链接?在编译器/链接器中,我的示例发生了什么?”
自从 C99 标准以来,“implicit int” 规则已被禁止。因此,具有隐式函数声明的程序是无效的。在 C99 之前,如果没有可见的原型,则编译器会使用返回类型为 int 隐式声明一个原型。
“令人惊讶的是,一切都成功地链接起来了。当然,在 ./a.out 调用后,我看到了垃圾输出。”
这是因为您没有原型,编译器会为 f() 隐式声明一个返回类型为 int 的原型。但是 f() 的实际定义返回一个 double 类型。这两种类型不兼容,这就是未定义行为。
这在C89/C90中甚至是未定义的,其中隐式int规则有效,因为隐式原型与实际类型f()返回不兼容。因此,这个例子(使用a.cb.c)在所有C标准中都是未定义的
现在不再有隐式函数声明的用处或有效性。因此,编译器/链接器处理的实际细节只具有历史意义。它追溯到K&R C的预标准时代,当时没有函数原型,并且函数默认返回int。函数原型在C89/C90标准中添加。最重要的是,您必须对所有函数进行原型(或在使用之前定义函数),以使其成为有效的C程序。

这并不完整。问题与隐式声明无关,而是由于链接器对类型一无所知,他找到了一个名为f的函数定义,并适合进行链接。为了防止这样的问题,一些语言使用名称重整来将类型编码到函数的名称中;但在C语言中并非如此。拥有原型并不足够,您需要一致的原型! - Jean-Baptiste Yunès
@Jean-BaptisteYunès,我已经解释了隐式原型的类型与实际类型不兼容,并因此引发了UB。没有所谓的“一致的原型”。原型是关于函数返回类型和参数类型的完整信息。 - P.P
@i3x 但问题的重点(我认为)是为什么链接完成,以及解释了链接过程的细节。 - Ziffusion
@Jean-BaptisteYunès 是的,在这种情况下,主要的误解是关于链接问题。我先后阅读了Sourav和l3x的答案。所以它们在我的脑海中有些混淆。现在Sourav的答案似乎更完整。 - Edgar Rokjān
@Ziffusion 我的意思是它存在未定义行为,但由于 C 语言的古老规则,已经编译和链接。顺便说一下,我刚刚读了你的回答,它并不完全准确。 - P.P
@i3x 我认为你误解了重点。主要原因是:不一致的原型(隐式声明边界效应)和无类型链接。这两者与“C语言古老规则”无关。 - Jean-Baptiste Yunès

1
在编译后,所有类型信息都会丢失(除了调试信息,但链接器不会关注它)。唯一剩下的就是“有一个名为“f”的符号位于地址0xdeadbeef”。头文件的作用是告诉C语言关于符号的类型,包括函数需要什么参数和返回值。如果你将实际的类型与声明的类型(无论是显式还是隐式)不匹配,则会得到未定义的行为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接