代码之家  ›  专栏  ›  技术社区  ›  Sitesh

为什么在链接静态库时强制执行订单(例如source.cxx-lstatic)?

  •  0
  • Sitesh  · 技术社区  · 6 年前

    在与静态库链接时,为什么要强制执行该命令?

    g++-ldynamic-lstatic src。cxx//错误

    g++-L静态src。cxx-ldynamic//错误

    g++src。cxx-L动态 -lstatic公司 //成功

    g++-L动态src。cxx-lstatic//成功

    静态库不能像动态库那样(按任何顺序)链接,有什么技术原因吗?

    为什么链接库不能成为通用库(可以在编译/链接时提及,例如对于静态: -ls 对于动态: -ld 等等。)?

    2 回复  |  直到 4 年前
        1
  •  1
  •   Mike Kinghan    6 年前

    这个 根据需要 Linux链接中的分歧

    您的示例:

    g++ -ldynamic -lstatic src.cxx # ERROR
    
    g++ -ldynamic src.cxx -lstatic # SUCCESS
    

    表示您的linux发行版属于RedHat家族。让我们确认一下 比如CentOS 7:

    $ cat /proc/version
    Linux version 3.10.0-693.el7.x86_64 (builder@kbuilder.dev.centos.org) \
    (gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC) ) \
    #1 SMP Tue Aug 22 21:09:27 UTC 2017
    
    
    $ cat foo.c
    #include <stdio.h>
    
    void foo(void)
    {
        puts(__func__);
    }
    
    $ cat bar.c
    #include <stdio.h>
    
    void bar(void)
    {
        puts(__func__);
    }
    
    $ cat main.c
    extern void foo(void);
    extern void bar(void);
    
    int main(void)
    {
        foo();
        bar();
        return 0;
    }
    
    $ gcc -Wall -fPIC -c foo.c
    $ gcc -shared -o libfoo.so foo.o
    $ gcc -Wall -c bar.c
    $ ar cr libbar.a bar.o
    $ gcc -Wall -c main.c
    $ gcc -o prog -L. -lfoo -lbar main.o -Wl,-rpath=$(pwd)
    main.o: In function `main':
    main.c:(.text+0xa): undefined reference to `bar'
    collect2: error: ld returned 1 exit status
    # :(
    $ gcc -o prog -L. -lfoo main.o -lbar -Wl,-rpath=$(pwd)
    $ # :)
    $ ./prog
    foo
    bar
    

    所以你就在那里。

    现在,让我们在Debian氏族发行版上查看它:

    $ cat /proc/version
    Linux version 4.13.0-32-generic (buildd@lgw01-amd64-016) \
    (gcc version 7.2.0 (Ubuntu 7.2.0-8ubuntu3)) \
    #35-Ubuntu SMP Thu Jan 25 09:13:46 UTC 2018
    

    在这里,一切都是一样的:

    $ gcc -o prog -L. -lfoo -lbar main.o -Wl,-rpath=$(pwd)
    main.o: In function `main':
    main.c:(.text+0x5): undefined reference to `foo'
    main.c:(.text+0xa): undefined reference to `bar'
    collect2: error: ld returned 1 exit status
    

    当它变得不同时。现在链接无法解决 任何一个 foo -从 共享库 libfoo.so -或 bar -从静态库 libbar.a . 和 要解决这一问题,我们需要:

    $ gcc -o prog -L. main.o -lfoo -lbar -Wl,-rpath=$(pwd)
    $ ./prog
    foo
    bar
    

    具有 全部的 目标文件后提到的库- main.o -那个 参考他们定义的符号。

    Centos-7(RedHat)链接行为是老生常谈。Ubuntu 17.10(Debian) Debian 7于2013年引入了链接行为,并逐步推广 到Debian派生的发行版。如你所见,它废除了这种区别 在共享库和静态库之间, 或不需要显示在链接序列中 之后 所有输入 引用它的文件。它们都必须按依赖关系顺序出现(DO 1. ), 共享库和静态库相似。

    这取决于发行版决定如何构建GCC版本 工具链-他们如何选择传递给系统的默认选项 链接器( ld )当它被一种语言调用时 前端( gcc , g++ , gfortran 等)为您执行链接。

    具体来说,这取决于链接器选项 --as-needed 是否默认插入到 ld公司 库之前的命令行 已插入。

    如果 --根据需要 则不是有效的共享库 利福。所以 已到达, 然后,它将被链接,无论链接到目前为止是否已累积任何 对共享库定义的符号的未解析引用。简言之 它将被链接,无论任何 需要 链接它。也许更进一步 链接的进程到后续输入将导致未解析的引用 那个 利福。所以 解决,证明其关联。但也许不是。它被链接 无论如何那是红帽子路。

    如果 --根据需要 利福。所以 到达后 仅当导出至少一个符号的定义时才会链接 未解析引用所指向的 已经 在联动中累计,即。 事实证明,需要将其联系起来。如果存在 不需要链接它。这就是Debian的方式。

    在Debian 7之前,共享库链接的RedHat方式一直很流行 打破了等级。但是静态库的链接 总是 符合 根据需要 道德原则 默认情况下。没有 --根据需要 应用于静态库的选项。 相反,情况恰恰相反, --whole-archive : 您需要使用它来覆盖默认行为,并从静态库链接对象文件,而不管需要什么。 所以像你这样的人,在RedHat land,观察到了这个令人费解的区别:默认情况下,静态库 必须在DO中链接;对于共享库,默认情况下可以执行任何顺序。 乡亲们是德比安的土地看到了这样的差别。

    那又怎样?

    因为Redhat way有着令人费解的区别——它是 外行的联系努力——问是很自然的 为什么? ,历史上, 是的 根据需要 对于静态库,但是 不按需要 对于共享库, 当然,这也是为什么它仍在红帽区。

    链接器将程序(或共享库)组装为 增量填充 部分 动态依赖关系记录 (DDR 2. )在中 一种由节和DDR组成的结构,从空开始,然后 最终成为一个二进制文件,OS加载器可以解析并成功映射该文件 进入进程地址空间:例如ELF可执行文件或DSO。( 部分 这是一个真正的技术术语。 动态依赖关系记录 不是。 为了方便起见,我刚刚创造了。)

    粗略地说,驱动此过程的链接器输入是对象文件, 共享库或静态库。但是 严格地 我说,他们是 任何一个 对象文件或共享库 . 因为静态库 一 ar archive 碰巧是 对象文件。就链接器而言,它只是一个对象序列 可能需要或不需要使用的文件,与符号表一起存档 链接器可以通过它查询哪些对象文件(如果有的话)定义了符号。

    当链接器到达一个对象文件时,该对象文件是 总是 链接的 进入程序。链接器从不询问 需要 对象文件 (不管这意味着什么)。任何对象文件都是无条件的 来源 悬挂机构的 进一步的投入必须满足的需求。

    输入对象文件时,链接器必须将其分解为 这个 输入 并将其合并到 输出 程序中的节。当输入节 S 显示在一个对象中 文件,很可能是 S 将出现在其他对象文件中; 也许他们都是。链接器必须将所有输入缝合在一起 S 部分 转换为单个输出 S 程序中的部分,所以它没有最终完成 组成输出部分 S 直到联动完成。

    当共享库 利福。所以 是链接的输入,链接器输出 一个DDR到程序中(如果它决定需要库,或者不在乎)。这基本上是一个备忘录,在运行时由 装载机,告诉它 利福。所以 是流程的依赖关系 在建工程所以它会找到 利福。所以 通过其标准搜索算法, 加载它并将其映射到流程中。

    使用对象文件是一种相对昂贵的链接;消费 共享库相对便宜-尤其是如果链接器没有 必须事先弄清楚共享库是否 需要 . 对象文件的输入/输出部分处理通常比写出DDR更麻烦。 但比工作更重要的是,链接一个对象文件通常会使程序变得非常重要 更大,以及 可以 任意放大。链接共享库添加 只有一个DDR,这总是一件小事。

    因此,有一个值得尊重的理由,即链接策略可以拒绝 对象文件,除非需要,但允许链接共享库 没有必要。链接不必要的对象文件会添加任意数量的死区 程序的权重,在连杆上按比例加载。但是 如果链接器不必证明共享库 需要 ,然后它 可以在一瞬间将其链接到程序的大部分中,这一点可以忽略不计。如果开发人员选择将共享库添加到链接中,那么很可能需要它。红帽队 坎普仍然认为这个理由足够好了。

    Debian阵营当然也有一个值得尊敬的理由。是的,Debian链接 需要额外的努力来确定 利福。所以 ,当它是 到达时,定义在该位置有未解析引用的任何符号 指向但仅通过链接所需的共享库:-

    • 在运行时,加载程序避免了加载冗余 依赖项,或者找出它们是冗余的,以便不加载它们。

    • 如果 冗余的运行时依赖关系在链接时被消除。

    • 像您这样的开发人员不会被不一致的链接规则绊倒 对于静态库和共享库!-更糟糕的是 -lfoo 在链接器中,命令行不会显示它是否将解析 到 利福。所以 libfoo.a .

    分裂中的每一方都有更棘手的利弊。

    现在考虑链接器如何使用静态库, libabc.a -对象文件列表 a.o, b.o, c.o . 这个 根据需要 应用的原则如下:当链接器到达 libabc。一 , 它手头有0个或多个携带的未解析符号引用 从已链接的其他0个对象文件和共享库转发 进入程序。链接者的问题是: 中是否有任何对象文件 该归档文件为任何这些未解析的符号引用提供定义? 如果有0个这样的未解析引用,那么答案很简单 . 所以 无需查看档案。 libabc。一 已通过。链接器移动 转到下一个输入。如果它手头有一些未解析的符号引用,则 链接器检查由存档中的对象文件定义的符号。它 仅提取那些提供所需符号定义的对象文件(如果有) 3. 并将这些对象文件输入到链接,就像它们是单独的一样 在命令行中命名,并且 libabc。一 根本没有提到。然后它移动 它将转到下一个输入(如果有)。

    很明显 根据需要 静态库的原则意味着DO。不 对象文件将从静态库中提取并链接,除非未解析 对对象文件定义的某个符号的引用已从 对象文件(或共享库) 已经 已链接。

    静态库必须是 根据需要 ?

    在RedHat land,共享图书馆在哪里可以免除DO,我们在哪里可以做什么 它的缺席只是 链接提到的每个共享库 . 正如我们所做的 可见,这在链接资源和程序大小方面相当便宜。如果我们 对于静态库,同样放弃DO,等效的策略将 收件人: 链接所提到的每个静态库中的每个对象文件 . 但是 这在链接资源和程序自重方面成本过高。

    如果我们想为静态库释放DO,但仍然 链接 如果不需要对象文件,链接器如何继续?

    也许是这样-

    • 将所有明确提到的对象文件链接到程序中。
    • 链接所有提到的共享库。
    • 查看是否仍存在任何未解析的引用。如果是,那么-
    • 从前面提到的所有静态库中提取所有对象文件 进入一个 可选择的 对象文件。
    • 然后在 根据需要 此可选池的基础 对象文件,成功或失败。

    但这样的事不会发生。链接器 sees是链接的。这意味着对象文件的顺序 是相互联系的事物,即使是在两个不同的联系顺序之间 都是 成功的 .

    假设对象文件 a.o, b.o 已链接;未解析的引用 保留,然后链接器可以选择可选的对象文件 c.o, d.o, e.o, f.o 继续。

    可能有 不止一个 订购 c、 o、d.o、e.o、f.o 那个 解析所有引用并为我们提供一个程序。可能的情况是, 说 e.o 首先解决所有未解决的引用,不产生新引用, 给出一个程序;链接时说 c.o first还解决所有未解决的问题 参考文献 但是 生成一些新的,需要链接一些或 全部 d.o, e.o, f.o -取决于顺序-每个可能的悬挂机构 导致了另一个不同的计划。

    还不止这些。可能有多个订单 c、 o、d.o、e.o、f.o 这样,在链接某个对象文件之后- P点 -全部 解决之前未解决的参考,但:-

    1. 其中一些订单在某一点上不会产生新的引用 P 或者只生成其他链接顺序可以解析的引用。

    2. 其他的会在这一点上产生新的引用 P 没有进一步的链接顺序可以解决。

    因此,每当链接器发现它在早些时候做出了类型2的选择时,它都需要 要回到这一点并尝试当时可用的其他选择之一, 它还没有试过 , 只有在尝试了所有方法都不成功的情况下,才能得出这种联系失败的结论。 像这样链接到 N 可选对象文件将按比例花费时间 到阶乘 N 失败。

    像现在一样,使用DO for静态库,我们指定对象文件和/或静态库 Ij公司 在 链接器命令行的顺序如下:

    I0, I1, ... In
    

    这等同于对象文件的排序,为了论证,可能 注释:

    O0, O1, [02,... O2+j], O2+j+1, [O2+j+2,... O2+j+k] ...
    

    哪里 [Oi...] 是的子序列 可选择的 对象文件(即静态库)将 此时链接器可用。

    不管我们是否知道 当我们编写命令行时,我们不仅要断言 与产量相关的良好做法排序 一些 程序,还有 排序生成我们需要的程序 打算 .

    我们可能在第一个计数(=链接失败)上出错。我们甚至可能 第二个错误(=平均链接错误)。但如果我们不再关心这些的顺序 输入并将其交给链接器,以便找到一个好的解决方法,或者证明没有, 然后:

    • 实际上我们已经不再关心 哪个程序 如果有的话,我们会得到的。
    • 我们已经不再关心这种联系是否会在任何可行的时间终止。

    这是不会发生的。

    难道我们就不能得到一个坏掉的东西的警告吗?

    在一条评论中,你问为什么链接器至少不能 警告 如果我们的 静态对象文件和静态库不在DO中。

    除此之外,这还将破坏这种联系,就像现在一样。但是给我们这个 附加警告链接器必须证明链接失败 因为对象文件和静态库不在DO中 ,而不仅仅是因为 链接中存在链接中没有定义的引用。还有它 只能通过证明 一些程序 可以通过对象文件和静态库的一些排列来链接。 这是一个阶乘规模的任务,我们不在乎 一些 可以链接程序, 如果程序 打算 链接不能是:链接器没有关于 除了我们提供的输入,我们打算链接什么程序 我们给他们。

    如果提到任何库,很容易让链接器(或者更合理地说,GCC前端)发出警告 在命令行中的任何对象文件之前。但它会有一些讨厌的价值,因为 这种联系不一定会失败,事实上可能是有意的 悬挂机构。“对象文件之后的库”是很好的日常指南 通过GCC前端调用链接器。更糟糕的是,这样的警告只适用于 库之后的对象文件 不适用于DO断裂的情况 在库之间 ,因此它只能完成部分工作。


    [1] 我的缩写。

    [2] 也是我的缩写。

    [3] 更准确地说,从静态库提取对象文件是递归的。 链接器提取定义未解析引用的所有对象文件 已经准备好了, 在 链接从库中提取的对象文件。

        2
  •  1
  •   Some programmer dude    6 年前

    链接器加载库时 static ,它将查看是否需要其中的任何符号。它将使用所需的符号,并丢弃其余的符号。这当然意味着,如果不需要库中的任何符号,那么所有符号都将被丢弃。

    这就是为什么将库放在依赖它的对象文件之前将不起作用的原因。

    根据经验,始终将库(甚至动态库)放置在命令行的末尾。按依赖关系的顺序。If模块(对象文件或库) A 取决于模块 B ,始终放置 A. 之前 B .