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

Why doesn't the linker complain of duplicate symbols?

我有一个dummy.hpp

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

和一个dummy.cpp

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

和一个使用 dummy()

的 main.cpp
#include "dummy.hpp"
int main(){

    dummy();
    return 0;
}

然后我把dummy.cpp编译成三个库,libdummy1.alibdummy2.alibdummy.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 和 link 虚拟库时

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

    linker 没有产生重复符号错误。当我 link 静态地使用两个相同的库时,为什么会发生这种情况?

  2. 当我尝试

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

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

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

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

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

场景 1(双静态库)或场景 2(静态和共享库)都没有错误,因为 linker 从静态库或第一个共享库中获取第一个目标文件,它遇到的提供了它尚未定义的符号的定义。它只是忽略了同一符号的任何后续定义,因为它已经有了一个好的定义。一般而言,linker 只从库中获取它需要的东西。对于静态库,这是绝对正确的。对于共享库,如果满足任何缺失的符号,则共享库中的所有符号都可用;对于某些 link 用户,共享库的符号可能无论如何都可用,但其他版本仅在共享库提供至少一个定义时才记录使用共享库。

这也是您需要 link 库在目标文件之后的原因。您可以将 dummy.o 添加到 linking 命令中,只要它出现在库之前,就没有问题。在库之后添加 dummy.o 文件,你会得到双重定义的符号错误。

唯一一次你 运行 遇到这种双重定义的问题是,如果库 1 中有一个目标文件同时定义了 dummyextra,并且库 1 中有一个目标文件定义了 dummyalternative 的库 2,并且代码需要 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 的双重定义,您将无法 link 显示的三个源文件中的目标文件,即使主代码没有调用 [=23] =].

关于:

The loader seems always to choose dynamic libs and not compiled in the .o files.

否; linker 总是试图无条件地加载目标文件。它在命令行上遇到库时扫描它们,收集它需要的定义。如果目标文件在库之前,则没有问题,除非两个目标文件定义了相同的符号('one definition rule' 响铃了吗?)。如果某些目标文件跟在库之后,如果库定义了后面的目标文件定义的符号,您可能会 运行 发生冲突。请注意,当它开始时,linker 正在寻找 main 的定义。它从被告知的每个目标文件中收集已定义的符号和引用的符号,并不断添加代码(从库中)直到定义了所有引用的符号。

Does it means the same symbol is always loaded from .so file, if it is both in .a and .so file?

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

Does it mean that symbols in static symbol table in a static library are never in conflict with those in dynamic symbol table in .so file?

您可能会遇到冲突,但遇到的第一个定义会解析 linker 的符号。如果满足引用的代码通过定义所需的其他符号引起冲突,它只会 运行 发生冲突。

If I link 2 shared libs, can I get conflicts and the link phase failed?

正如我在评论中指出的那样:

My immediate reaction is "Yes, you can". It would depend on the content of the two shared libraries, but you could run into problems, I believe. […cogitation…] How would you show this problem? … It's not as easy as it seems at first sight. What is required to demonstrate such a problem? … Or am I overthinking this? … […time to go play with some sample code…]

经过一些实验,我的临时经验答案是 "No, you can't"(或 "No, on at least some systems, you don't run into a conflict")。我很高兴我搪塞了。

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

$ 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() 调用的函数是第一个出现在 link 所用的库中的函数。但是(至少在 Mac OS X 10.10.5 上)linker 不会 运行 发生冲突。但是请注意,每个共享对象中的代码都调用 dummy() 的 'its own' 版本——两个共享库之间对于哪个函数是 dummy() 存在分歧。 (如果 dummy() 函数位于共享库中单独的对象文件中会很有趣;那么 dummy() 的哪个版本被调用?)但是在显示的极其简单的场景中, main()函数设法只调用 dummy() 函数之一。 (请注意,如果发现平台之间的这种行为存在差异,我不会感到惊讶。我已经确定了我测试代码的位置。如果您在某些平台上发现不同的行为,请告诉我。)