为什么链接器不报告重复符号?

12

我有一个dummy.hpp

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

和一个dummy.cpp

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

还有一个 main.cpp 文件,其中使用了 dummy() 函数。

#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. 当我试图编译主程序并链接虚拟库时

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

当我静态链接两个相同的库时,连接器没有产生重复符号错误。为什么会这样呢?

  • 当我尝试

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

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

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

    这是否意味着如果一个符号同时存在于.a.so文件中,它始终从.so文件中加载相同的符号?

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


    @HansPassant,我个人遇到过这样的情况,即链接两个不同的.a库以创建一个.so库,如果两个库中有相同的.o文件,则链接器会报错。但是在上面的示例中却不起作用,真的很奇怪。 - user5362441
    请展示您产生错误的确切命令。通常不应该有错误,这只是链接器的工作方式,所以您在这里做了一些不寻常的事情。 - n. m.
    1个回答

    14
    无论是方案1(双静态库)还是方案2(静态库和共享库),都没有错误,因为链接器会从提供尚未定义的符号定义的第一个静态库或第一个共享库中获取第一个目标文件。 它只会忽略同一符号的后续定义,因为它已经拥有良好的定义。 一般来说,链接器仅从库中获取所需内容。 对于静态库,这是严格正确的。 对于共享库,如果满足任何缺少的符号,则共享库中的所有符号都可用;对于某些链接器,共享库的符号可能始终可用,但其他版本仅在该共享库提供至少一个定义时才记录使用共享库。
    这也是为什么您需要在对象文件之后链接库的原因。 您可以将dummy.o添加到链接命令中,只要它出现在库之前,就不会有问题。 将dummy.o文件添加到库之后,您将获得重复定义的符号错误。
    唯一遇到此双重定义问题的情况是:Library 1中有一个目标文件同时定义了dummy和extra,并且Library 2中有一个目标文件同时定义了dummy和alternative,并且代码需要extra和alternative的定义 - 然后您将具有造成麻烦的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()。然后你会按照 return extra(alternative(54)); 的调用顺序得到另外两行的输出,分别由 alternative()extra() 产生。

    $ 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()函数。(请注意,我不会对此行为在各平台间存在差异感到惊讶。我已经确定了测试代码的位置。如果您发现某些平台上具有不同的行为,请告诉我。)


    如果我链接了两个共享库,是否会出现冲突并导致链接阶段失败? - user5362441
    @Will:请查看更新后的答案。显然不是这样,但可以在您的系统上进行实验。 - Jonathan Leffler
    @Will:我认为使用一个或多个共享库,你就不会遇到链接时间冲突的问题。(你的第二条评论是在我的“查看更新答案”评论之后6秒钟到达的;我想你当时还没有机会查看更新内容。) - Jonathan Leffler
    加载器管理这些事情的方式有所不同。一个问题是,“如果您用另一个只包含单个函数int SomethingCompletelyDifferent() { return 0; }的文件替换原始的libwhichever.so,程序是否仍然可以正常运行?”我怀疑答案是“是”,如果程序使用的符号来自静态库(因为在链接共享库之前已经链接了它)——但如果共享库被列在第一位,则是“否”。您可以玩很多类似的游戏来进行实验。请注意,在操作系统A上有效的内容可能在操作系统B上失败。 - Jonathan Leffler
    你是正确的,通过命令传递的第一个库会解析符号。加载器始终需要一个.so文件来运行可执行文件,如果我将一个.so文件链接到可执行文件中,那么它每次使用其中的符号时都会被调用。 - user5362441
    显示剩余3条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接