隐式函数声明和链接

Implicit function declarations and linkage

最近我了解了 C 中的隐式函数声明。主要思想很清楚,但我在理解这种情况下的 linkage 过程时遇到了一些麻烦。

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

#include <stdio.h>

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

如果我尝试编译它:

gcc -c a.c -std=c99

我看到关于函数隐式声明的警告 f()

如果我尝试编译 link:

gcc a.c -std=c99

我有一个未定义的引用错误。所以一切都很好。

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

double f(double x) {
    return x;
}

并调用下一个命令:

gcc a.c b.c -std=c99

令人惊讶的是,一切都 link 成功了。当然,在 ./a.out 调用之后,我看到了垃圾输出。

所以,我的问题是:具有隐式声明函数的程序如何 linked?在 compiler/linker 的幕后,我的示例中发生了什么?

我阅读了很多关于 SO 的主题,例如 this, this and this one,但仍然有问题。

编译后,所有类型信息都丢失了(调试信息中可能除外,但链接器不会注意它)。唯一剩下的是地址 0xdeadbeef 处的 "there is a symbol called "f"。

headers 的要点是告诉 C 符号的类型,包括对于函数,它需要什么参数以及它是什么 returns。如果你将真实的与你声明的不匹配(无论是显式还是隐式),你会得到未定义的行为。

首先,自 C99 以来,函数的隐式声明已从标准中删除。编译器 可能 支持编译遗留代码,但这不是强制性的。引用标准前言,

  • remove implicit function declaration

也就是说,根据 C11,章节 §6.5.2.2

If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined.

所以,在你的情况下,

  • 函数调用本身是隐式声明(从C99开始变成non-standard),

  • 并且由于函数签名不匹配 [假定函数的隐式声明具有 int return 类型],您的代码调用 undefined behavior.

只是为了增加一点参考,如果你试图在 same 编译单元 after 调用中定义函数,你由于签名不匹配,将会出现编译错误。

但是,您的函数是在单独的编译单元中定义的(并且缺少原型声明),编译器无法检查签名。编译后,链接器获取目标文件,并且由于链接器中没有任何 type-checking(目标文件中也没有信息),很高兴链接它们。最后,它将以 成功 编译和链接 and UB 告终。

How are programmes with implicitly declared functions are linked? And what happens in my example under the hood of compiler/linker?

自 C99 以来,implicit int 规则已被 C 标准禁止。因此,具有隐式函数声明的程序是无效的

自 C99 起无效。在此之前,如果可见原型不可用,则编译器隐式声明一个 int return 类型。

Surprisingly everything is linked successfully. Of course after ./a.out invocation I see a rubbish output.

因为您没有原型,编译器为 f() 隐式声明了一个具有 int 类型的原型。但是f()的实际定义是return一个double。这两种类型不兼容,这是 undefined behaviour.

即使在 C89/C90 中也是未定义的,其中隐式 int 规则是有效的,因为隐式原型与实际类型 f() return 不兼容。所以这个例子是(a.cb.c)在所有 C 标准中是 undefined

隐式函数声明不再有用或无效。因此,compiler/linker 如何处理的实际细节仅具有历史意义。它可以追溯到 K&R C 的 pre-standard 时代,它没有函数原型和默认函数 return int。在 C89/C90 标准中,函数原型被添加到 C 中。底线是,您必须为有效 C 程序中的所有函数提供原型(或在使用前定义函数)。

这是正在发生的事情。

  1. 如果没有 f() 的声明,编译器会假设一个像 int f(void) 这样的隐式声明。然后愉快地编译a.c.
  2. 在编译b.c时,编译器没有对f()的任何预先声明,所以它从f()的定义中直觉。通常你会在头文件中放置一些 f() 的声明,并将其包含在 a.cb.c 中。因为这两个文件将看到相同的声明,所以编译器可以强制一致性。它将抱怨与声明不匹配的实体。但是在这种情况下,没有共同的原型可以参考。
  3. C中,编译器不在对象文件中存储任何关于原型的信息,并且linker 不执行任何一致性检查(它不能)。它所看到的只是 a.c 中未解析的符号 fb.c 中定义的符号 f。它愉快地解决了符号,并完成了 link.
  4. 尽管如此,事情在 运行 时发生了故障,因为编译器根据它在那里假定的原型在 a.c 中设置了调用。这与 b.c 中的定义不匹配。 f()(来自 b.c)将从堆栈中获取一个垃圾参数,return 它作为 double,将在 [=44] 上被解释为 int =] 在 a.c.