【问题标题】:Implicit function declarations and linkage隐式函数声明和链接
【发布时间】:2016-01-04 19:06:55
【问题描述】:

最近我了解了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 的主题,例如 thisthisthis one,但仍然有问题。

【问题讨论】:

    标签: c linkage function-declaration implicit-declaration


    【解决方案1】:

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

    • 移除隐式函数声明

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

    如果函数是用不包含原型的类型定义的,那么 提升后的参数与之后的参数不兼容 促销,行为未定义。

    所以,在你的情况下,

    • 函数调用本身是隐式声明(自 C99 起成为非标准),

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

    只是补充一点参考,如果你试图在same编译单元中定义函数调用之后,你会得到一个编译错误,因为不匹配的签名。

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

    【讨论】:

    • 感谢您的详细解答。
    【解决方案2】:

    这就是正在发生的事情。

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

    【讨论】:

    • 我认为编译器在处理隐式声明时假定int f() 而不是int f(void)
    • 我会检查,但鉴于a.c 中的调用没有参数,假设int f(void) 是有意义的,因为int f() 表示any number of arguments
    • 隐式声明在现代 C 中无效,是 UB。没有像int f(void); 那样提供隐式的。它只提供像int f(); 这样的声明,编译器在编译b.c 时并不“直观”,并且不需要之前定义原型。函数定义还提供了它的原型。如果之前提供了原型,则会对其进行检查。否则,不会。 “通用原型”不是一个东西。原型是否可用。编译器如何获取该信息(通过头文件或实际定义等)无关紧要。
    • 虽然 (3) 和 (4) 都很好,但它在标准中被简单地定义为 未定义的行为
    • @I3x 所有这些都是问题的外围,主要是在挑剔我表达某些事情的方式。但无论如何。
    【解决方案3】:

    如何链接具有隐式声明函数的程序?在我的示例中,在编译器/链接器的底层会发生什么?

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

    自 C99 起无效。在此之前,如果一个可见原型不可用,那么编译器会隐式声明一个返回类型为int

    令人惊讶的是,所有内容都已成功链接。当然之后 ./a.out 调用我看到一个垃圾输出。

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

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

    隐式函数声明不再有用或无效。因此,编译器/链接器如何处理的实际细节仅具有历史意义。它可以追溯到没有函数原型的 K&R C 的准标准时代,函数默认返回 int。在 C89/C90 标准中,函数原型被添加到 C 中。归根结底,您必须拥有有效 C 程序中所有函数的原型(或在使用前定义函数)。

    【讨论】:

    • 这不完整。这个问题与隐式声明无关,而是由于链接器对类型一无所知,他找到了一个名为f的函数的定义,它适用于链接。为了防止这样的问题,一些语言使用名称修饰将类型编码到函数的名称中;但在 C 中情况并非如此。拥有原型是不够的,您需要一致的原型!
    • @Jean-BaptisteYunès 我已经解释了隐式原型的类型和实际的类型不兼容,因此调用了 UB。没有“一致的原型”之类的东西。原型是关于函数返回类型及其参数的完整信息。
    • @i3x 但问题的很大一部分(我认为)是为什​​么链接完成,以及链接过程的详细信息解释了这一点。
    • @Jean-BaptisteYunès 是的,在这种情况下,主要的误解是关于链接问题。因此,我阅读了 Sourav 和 l3x 的答案。所以他们在我的脑海里混杂了一点。现在,Sourav 的回答似乎更完整了。
    • @Ziffusion 我是说它是 UB,它是根据 C 的古老规则编译和链接的。顺便说一句,我刚刚看了你的答案,并不完全准确。
    【解决方案4】:

    编译后,所有类型信息都会丢失(可能在调试信息中除外,但链接器不会注意这一点)。唯一剩下的是“在地址 0xdeadbeef 处有一个名为“f”的符号”。

    标题的意义在于告诉 C 符号的类型,包括,对于函数,它需要什么参数以及它返回什么。如果您将真实的与您声明的(显式或隐式)不匹配,您将获得未定义的行为。

    【讨论】:

      猜你喜欢
      • 2020-09-23
      • 1970-01-01
      • 2021-01-20
      • 2014-06-28
      • 1970-01-01
      • 2015-07-26
      • 2022-01-16
      • 2011-10-05
      相关资源
      最近更新 更多