在 C 程序中,当在两个不同实现的不同库中定义相同的函数时会发生什么?

use*_*488 3 c static-libraries

如果我们有一个函数 foo() 在两个不同实现的不同库中具有相同的原型,并且如果我们只包含单个头文件(具有函数声明),如果我们尝试在编译或运行时编译或执行程序会发生什么?

Mik*_*han 6

根据您的标签,您正在询问静态库的链接。链接器不知道也不关心它的输入文件是用什么源语言编译的。C 语言对这个问题无关紧要,但我将使用 C 来说明。

阅读有关静态库Stackoverflow 标签 Wiki,它将解释将您的程序与静态库链接与将您的程序与静态库中存档的 0 个或多个目标文件(即 0 个或多个对象)链接起来完全相同链接器需要为程序中未解析的符号引用提供定义的文件。

一旦链接器p.o 在静态库libx.a中找到了一个目标文件文件,该文件为它提供了foo程序引用的某个符号的定义,它将把目标文件链接libx.a(p.o)到程序中以解析foo. 它不会尝试在链接中出现的任何其他静态库foo中的任何其他目标文件q.o文件中查找任何其他定义。liby.alibx.a

所以,如果有任何其他目标文件q.o中任何其他静态库liby.a ,后来进来的联系比libx.a这还包含了一个定义 foo,即目标文件liby.a(q.o)甚至不会被链接到程序 ,除非链接器需要它提供了一些定义其他符号bar 程序所指的。假设情况并非如此liby.a(q.o),出于链接目的,也可能不存在。

链接器将无法链接来自同一个符号的多个定义libx.a(p.o)以及liby.a(q.o)它是否不需要。它会将定义的第一个目标文件链接 到程序中,然后通过定义.libx.a(p.o)foofoo

这是一个例子:

主文件

extern void foo(void);
extern void bar(void);

int main(void)
{
    foo();
    bar();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

个人电脑

#include <stdio.h>

void foo(void)
{
    printf("%s %s %s\n",__func__,"from",__FILE__);
}
Run Code Online (Sandbox Code Playgroud)

质量控制

#include <stdio.h>

void foo(void)
{
    printf("%s %s %s\n",__func__,"from",__FILE__);
}
Run Code Online (Sandbox Code Playgroud)

遥控

#include <stdio.h>

void bar(void)
{
    printf("%s %s %s\n",__func__,"from",__FILE__);
}
Run Code Online (Sandbox Code Playgroud)

函数foop.c和 中定义q.c

将所有.c文件编译成.o文件:

$ gcc -c main.c p.c q.c r.c
Run Code Online (Sandbox Code Playgroud)

创建三个静态库,分别来自p.o, q.o, r.o

$ ar rcs libx.a p.o
$ ar rcs liby.a q.o
$ ar rcs libz.a r.o
Run Code Online (Sandbox Code Playgroud)

然后链接一个程序,libx.a之前输入liby.a

$ gcc -o prog main.o libz.a libx.a liby.a -Wl,-trace-symbol=foo
/usr/bin/ld: main.o: reference to foo
/usr/bin/ld: libx.a(p.o): definition of foo
Run Code Online (Sandbox Code Playgroud)

诊断链接选项-Wl,-trace-symbol=foo要求链接器告诉我们链接到prog它发现未解析引用foo的位置的文件的名称以及foo定义的文件的名称。您会看到 中foo引用了 ,main.o 并且libx.a(p.o)链接了 提供的定义。fooin 的另一个定义liby.a(q.o)没有链接。这种联系

gcc -o prog main.o r.o p.o
Run Code Online (Sandbox Code Playgroud)

其中包含foofrom的定义p.o,如程序所示:

$ ./prog
foo from p.c
bar from r.c
Run Code Online (Sandbox Code Playgroud)

现在重新链接prog,这次与liby.a之前libx.a

$ gcc -o prog main.o libz.a liby.a libx.a -Wl,-trace-symbol=foo
/usr/bin/ld: main.o: reference to foo
/usr/bin/ld: liby.a(q.o): definition of foo
Run Code Online (Sandbox Code Playgroud)

这一次, 的定义foo链接自liby.a(q.o)。此链接与以下内容完全相同:

gcc -o prog main.o r.o q.o
Run Code Online (Sandbox Code Playgroud)

其中只包含foofrom的定义q.o,如程序所示:

$ ./prog
foo from q.c
bar from r.c
Run Code Online (Sandbox Code Playgroud)

链接器不关心foo您在不同静态库的不同目标文件中提供了多少定义。它只关心 iffoo在程序中被引用,然后foo被它链接到程序的文件定义一次。

如果你强迫链接器链接文件成包含多个定义程序foo,则默认情况下,通常链接器会给你一个多重定义错误和联动会失败,因为不能有一个以上的定义foo在程序。这是一个例子:

qr.c

#include <stdio.h>

void foo(void)
{
    printf("%s %s %s\n",__func__,"from",__FILE__);
}

void bar(void)
{
    printf("%s %s %s\n",__func__,"from",__FILE__);
}
Run Code Online (Sandbox Code Playgroud)

编译该文件:

$ gcc -c qr.c
Run Code Online (Sandbox Code Playgroud)

qr.o在新的静态库中存档:

$ ar rcs libyz.a qr.o
Run Code Online (Sandbox Code Playgroud)

目标文件libyz.a(qr.o)定义了foobar. 所以我们可以像这样链接我们的程序:

$ gcc -o prog main.o libyz.a -Wl,-trace-symbol=foo,-trace-symbol=bar
/usr/bin/ld: main.o: reference to foo
/usr/bin/ld: main.o: reference to bar
/usr/bin/ld: libyz.a(qr.o): definition of foo
/usr/bin/ld: libyz.a(qr.o): definition of bar
Run Code Online (Sandbox Code Playgroud)

它运行如下:

$ ./prog
foo from qr.c
bar from qr.c
Run Code Online (Sandbox Code Playgroud)

但是,如果我们尝试像这样链接它:

$ gcc -o prog main.o libx.a libyz.a -Wl,-trace-symbol=foo,-trace-symbol=bar
/usr/bin/ld: main.o: reference to foo
/usr/bin/ld: main.o: reference to bar
/usr/bin/ld: libx.a(p.o): definition of foo
/usr/bin/ld: libyz.a(qr.o): in function `foo':
qr.c:(.text+0x0): multiple definition of `foo'; libx.a(p.o):p.c:(.text+0x0): first defined here
/usr/bin/ld: libyz.a(qr.o): definition of bar
collect2: error: ld returned 1 exit status
Run Code Online (Sandbox Code Playgroud)

有多重定义foo。那是因为:

  • 链接器需要定义foo并在 中找到第一个libx.a(p.o);所以它将该文件链接到程序中。它不会搜索任何其他人。
  • 链接器需要定义bar并在 中找到第一个libyz.a(qr.o);所以它将该文件链接到程序中。它不会搜索任何其他人。
  • libyz.a(qr.o)包含 的另一个定义foo以及 的定义bar。所以现在已经链接了两个定义foo,这是一个错误。

我说过,如果您让链接器尝试将多个定义符号的文件链接到程序中,默认情况下您会得到多重定义错误。

但是您可以通过告诉链接器符号是弱定义的来避免这种情况,前提是您的链接器理解这个概念(就像 GNU 和 Apple 链接器所做的那样)。

GCC 编译器支持非标准语言扩展, 您可以使用该__attribute__语法向链接器传达符号定义弱的信息。这是一个例子:

qr.c (2)

#include <stdio.h>

void __attribute__((weak)) foo(void)
{
    printf("%s %s %s\n",__func__,"from",__FILE__);
}

void bar(void)
{
    printf("%s %s %s\n",__func__,"from",__FILE__);
}
Run Code Online (Sandbox Code Playgroud)

重新编译:

$ gcc -c qr.c
Run Code Online (Sandbox Code Playgroud)

删除libyz.a并重新创建它:

$ rm libyz.a
$ ar rcs libyz.a qr.o
Run Code Online (Sandbox Code Playgroud)

重试刚刚失败的联动:

$ gcc -o prog main.o libx.a libyz.a -Wl,-trace-symbol=foo,-trace-symbol=bar
/usr/bin/ld: main.o: reference to foo
/usr/bin/ld: main.o: reference to bar
/usr/bin/ld: libx.a(p.o): definition of foo
/usr/bin/ld: libyz.a(qr.o): definition of bar
Run Code Online (Sandbox Code Playgroud)

这一次,没有错误。foofrom的定义libx.a(p.o)是链接的。弱定义 fromlibyz.a(qr.o)被忽略。该程序运行如下:

$ ./prog
foo from p.c
bar from qr.c
Run Code Online (Sandbox Code Playgroud)

如果符号定义不弱,那就是。链接器的规则是:

  • 最多可以链接一个符号的强定义。
  • 如果输入一个或多个弱定义以及一个强定义,则将一个强定义链接起来,而忽略所有弱定义。
  • 如果只输入弱定义,则链接器可以任意选择其中任何一个。(在实践中,它选择第一个找到的那个)。

不要简单地使用弱符号定义来避免让您感到意外的多个定义错误。这样的惊喜意味着您不了解您的联系。分析它并修复它,以便您不再尝试制作包含同一事物的多个定义的程序。

弱符号定义通常由编译器在幕后生成,以实现需要它们的源语言功能(例如全局内联函数定义或 C++ 中的模板实例化)。只有当您确切地理解为什么要让链接器输入同一符号的多个定义时,才可以自己使用它们。


Bas*_*tch 5

发生的情况是特定于实现的(甚至可能在 C11 标准n1570中没有指定,但我让你检查一下;据我所知,该标准不涉及- 只是涉及翻译单元)。我相信你给出的场景是一些未定义的行为,所以你可能会感到害怕(因为任何事情都可以发生)。它可能是实现定义的。

实际上,在我的 Linux 系统上,我会在链接时遇到一些错误(至少对于静态库)。阅读程序库 HowTo链接器的文档ld。请注意,GCC编译器(有时)正在运行ld(带有许多额外的选项,这些选项通常对您来说是隐藏的)。因此,请使用该标志进行调用 (以了解您的命令是如何启动的)。gcc-vldgcc

如果在 Linux 上链接两个共享库,就会变得更有趣。但请阅读 Drepper 的《如何编写共享库》。IIRC,第一个符号定义覆盖第二个。例如,您可以使用jemalloc并链接-ljemalloc隐式的前面-lc两个库都定义了malloc符号)。还要注意插件动态链接器(在 Linux 上,请参阅dlopen(3) & dlsym(3) & ld-linux.so(8))。

Linux也有弱符号,看这个,它的GCC可见性属性。

在Windows(我不知道也从未使用过)上情况可能会有所不同。但是,在链接时您仍然会遇到一些“多重定义符号”错误。

链接以操作系统特定的方式工作。因此,请阅读 Levine 的Linkers and Loaders(其中解释了 Windows 和 Unix 链接器的工作原理,并且细节有所不同)。要了解有关操作系统的更多信息,请阅读操作系统:三个简单的部分(可免费下载)。