为什么链接器不抱怨重复的符号?

Why doesn't the linker complain of duplicate symbols?

本文关键字:符号 链接 为什么      更新时间:2023-10-16

我有一个假人。hpp

#ifndef DUMMY
#define DUMMY
void dummy();
#endif

和伪。cpp

#include <iostream>
void dummy() {
std::cerr << "dummy" << std::endl;
}

和使用dummy()

的main.cpp
#include "dummy.hpp"
int main(){
dummy();
return 0;
}

然后我将dummy.cpp编译成三个库,libdummy1.a,libdummy2.a,libdummy.so:

g++ -c -fPIC dummy.cpp
ar rvs libdummy1.a dummy.o
ar rvs libdummy2.a dummy.o
g++ -shared -fPIC -o libdummy.so dummy.cpp
  1. 当我尝试编译main和链接虚拟库

    g++ -o main main.cpp -L. -ldummy1 -ldummy2
    
    链接器没有产生重复符号错误。为什么当我静态地链接两个相同的库时会发生这种情况?
  2. 当我尝试

    g++ -o main main.cpp -L. -ldummy1 -ldummy
    

    也没有重复的符号错误,为什么?

加载器似乎总是选择动态库,而不是在.o文件中编译的代码。

这是否意味着相同的符号总是从.so文件加载,如果它同时在.a.so文件?

这是否意味着静态库中的静态符号表中的符号永远不会与.so文件中的动态符号表中的符号冲突?

在场景1(双静态库)或场景2(静态和共享库)中都没有错误,因为链接器从静态库或第一个共享库中获取了它遇到的第一个对象文件,该文件提供了它尚未获得定义的符号的定义。它只是忽略了同一符号的任何后续定义,因为它已经有了一个好的定义。通常,链接器只从库中获取所需的内容。对于静态库,这是绝对正确的。对于共享库,如果它满足任何缺失的符号,则共享库中的所有符号都可用;对于某些链接器,共享库的符号可能是可用的,但其他版本只记录使用共享库,如果该共享库提供至少一个定义。

这也是为什么你需要在目标文件之后链接库。您可以将dummy.o添加到链接命令中,只要它出现在库之前,就不会有问题。在库之后添加dummy.o文件,您将获得双定义符号错误。

你唯一一次遇到这种双重定义的问题是,如果库1中有一个对象文件定义了dummyextra,库2中有一个对象文件定义了dummyalternative,代码需要extraalternative的定义,那么你有dummy的重复定义,这会造成麻烦。实际上,目标文件可能在单个库中,这会造成麻烦。

考虑:

/* file1.h */
extern void dummy();
extern int extra(int);
/* file1.cpp */
#include "file1.h"
#include <iostream>
void dummy() { std::cerr << "dummy() from " << __FILE__ << 'n'; }
int extra(int i) { return i + 37; }
/* file2.h */
extern void dummy();
extern int alternative(int);
/* file2.cpp */
#include "file2.h"
#include <iostream>
void dummy() { std::cerr << "dummy() from " << __FILE__ << 'n'; }
int alternative(int i) { return -i; }
/* main.cpp */
#include "file1.h"
#include "file2.h"
int main()
{
return extra(alternative(54));
}

由于dummy的双重定义,您将无法从所示的三个源文件链接目标文件,即使主代码没有调用dummy()

关于:

加载器似乎总是选择动态库,而不是在。o文件中编译。

没有;链接器总是尝试无条件地加载目标文件。它在命令行中遇到库时扫描它们,收集所需的定义。如果目标文件位于库之前,则不会出现问题,除非两个目标文件定义了相同的符号("一个定义规则"是否有任何提示?)。如果某些目标文件遵循库,那么如果库定义了后面的目标文件定义的符号,则可能会遇到冲突。注意,当它开始时,链接器正在查找main的定义。它从被告知的每个对象文件中收集已定义的符号和引用的符号,并不断添加代码(来自库),直到所有引用的符号都被定义。

这是否意味着相同的符号总是从.so文件加载,如果它都在.a.so文件?

没有;这取决于先遇到哪一个。如果首先遇到.a,则有效地将.o文件从库复制到可执行文件中(并且忽略共享库中的符号,因为在可执行文件中已经有了它的定义)。如果先遇到.so,则忽略.a中的定义,因为链接器不再寻找该符号的定义-它已经有一个了。

这是否意味着静态库中的静态符号表中的符号与.so文件中的动态符号表中的符号永远不会冲突?

可以有冲突,但遇到的第一个定义将解析链接器的符号。只有当满足引用的代码通过定义其他需要的符号而导致冲突时,它才会发生冲突。

如果我链接2个共享库,我可以得到冲突和链接阶段失败吗?

正如我在评论中指出的:

我的第一反应是"是的,你可以"。这将取决于两个共享库的内容,但我相信您可能会遇到问题。[…]你会如何表现这个问题?事情并不像乍看上去那么容易。证明这个问题需要什么?还是我想多了?…[……]时间去玩一些示例代码

经过一些实验,我的临时经验答案是"不,你不能"(或"不,至少在某些系统上,你不会遇到冲突")。我很高兴我搪塞了。

使用上面显示的代码(2个头文件,3个源文件),并在Mac OS X 10.10.5 (Yosemite)上使用GCC 5.3.0运行,我可以运行:

$ g++ -O -c main.cpp
$ g++ -O -c file1.cpp
$ g++ -O -c file2.cpp
$ g++ -shared -o libfile2.so file2.o
$ g++ -shared -o libfile1.so file1.o
$ g++ -o test2 main.o -L. -lfile1 -lfile2
$ ./test2
$ echo $?
239
$ otool -L test2
test2:
libfile2.so (compatibility version 0.0.0, current version 0.0.0)
libfile1.so (compatibility version 0.0.0, current version 0.0.0)
/opt/gcc/v5.3.0/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.21.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1213.0.0)
/opt/gcc/v5.3.0/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
$

在Mac OS X上通常使用.so作为扩展名(通常是.dylib),但它似乎可以工作。

然后我修改了.cpp文件中的代码,使extra()return之前调用dummy(),alternative()main()也是如此。在重新编译和重建共享库之后,我运行了这些程序。第一行输出来自main()调用的dummy()。然后你得到alternative()extra()按这个顺序产生的另外两行代码,因为return extra(alternative(54));的调用序列要求这样做。

$ g++ -o test2 main.o -L. -lfile1 -lfile2
$ ./test2
dummy() from file1.cpp
dummy() from file2.cpp
dummy() from file1.cpp
$ g++ -o test2 main.o -L. -lfile2 -lfile1
$ ./test2
dummy() from file2.cpp
dummy() from file2.cpp
dummy() from file1.cpp
$

注意,main()调用的函数是第一个出现在它所链接的库中的函数。但是(至少在Mac OS X 10.10.5上)链接器不会遇到冲突。不过请注意,每个共享对象中的代码都调用"自己的"dummy()版本——两个共享库对于哪个函数是dummy()存在分歧。(将dummy()函数放在共享库中的单独目标文件中会很有趣;那么调用哪个版本的dummy()?)但是在所示的极其简单的场景中,main()函数只设法调用dummy()函数中的一个。(注意,如果发现不同平台在这一行为上存在差异,我并不感到惊讶。我已经确定了测试代码的位置。如果你在某些平台上发现不同的行为,请告诉我。