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

Why order(e.g. source.cxx -lstatic) is enforced while linking with static library?

本文关键字:例如 source cxx -lstatic order 强制执行 静态 链接 为什么      更新时间:2023-10-16

与静态库链接时,为什么要强制执行顺序?

g++ -ldynamic -lstatic src.cxx//ERROR

g++ -lstatic src.cxx -ldynamic//ERROR

g++ src.cxx -ldynamic-lstatic//SUCCESS

g++ -ldynamic src.cxx -lstatic//SUCCESS

静态库不能像动态库一样链接(以任何顺序)是否有技术原因?

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

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) 链接行为是老派的。The Ubuntu 17.10 (Debian) 链接行为在 2013 年的 Debian 7 中引入,并逐渐渗透到下。 到 Debian 派生的发行版。如您所见,它取消了区别 在共享库和静态库之间,关于库的需求, 或不需要,在所有输入出现在链接序列中 引用它的文件。它们都必须按依赖顺序出现 (DO1), 共享库和静态库一样。

这归结为发行版如何决定构建他们的 GCC 版本 工具链 - 他们如何选择传递给系统的默认选项 链接器 (ld) 在幕后被一种语言调用时 前端(gccg++gfortran等)为您执行链接。

具体来说,它归结为链接器选项是否--as-needed默认情况下,是否插入到库之前的ld命令行中 入。

如果--as-needed无效,则到达共享库libfoo.so, 然后,无论该链接到目前为止是否累积了任何 对共享库定义的符号的未解析引用。总之 无论任何经证实需要链接它,它都将被链接。也许更远 与随后的投入的联系进程将产生未解决的参考文献libfoo.so解决了这个问题,证明其联系是合理的。但也许不是。它被链接 无论如何。这就是红帽的方式。

如果--as-needed到达libfoo.so时有效,则 当且仅当它导出至少一个符号的定义时,才会链接 在链接中已经产生了未解决的参考,即 事实证明有必要将其链接起来。如果有,它不能最终链接 无需链接它。这就是 Debian 的方式。

具有共享库链接的 RedHat 方式在 Debian 7 之前很普遍。 打破了排名。 但静态库的链接始终符合按需原则 默认情况下。没有适用于静态库--as-needed选项。 相反,--whole-archive: 您需要使用它来覆盖默认行为,并根据需要从静态库链接对象文件。 所以像你这样的人,在RedHat的土地上,观察到这个令人费解的差异:默认情况下,静态库 必须在 DO 中链接;对于共享库,默认情况下,任何顺序都可以。 乡亲们是 Debian 的土地看到如此不同的。

啦?

由于 Redhat 方式有这种令人费解的差异 - 一个绊脚石 外行的联系努力 - 很自然地问为什么,从历史上看, 它是静态库的需要,但不是共享库的需要, 理所当然,以及为什么它仍然在RedHat土地上。

粗略地说,链接器通过以下方式组装程序(或共享库) 以增量方式填充动态依赖项记录(DDR2) 部分和 DDR 的结构,从空开始,并且 最终成为操作系统加载程序可以解析并成功映射的二进制文件 到进程地址空间:例如 ELF 可执行文件或 DSO。(此处的部分是真正的技术术语。动态依赖项记录不是。 为了方便起见,我现在刚刚创造了。

粗略地说,驱动此过程的链接器输入是目标文件,共享库或静态库。但严格来说,它们要么是 对象文件或共享库。因为静态库很简单 恰好是的文件的ar存档 对象文件。就链接器而言,它只是一个对象的序列 可能需要也可能不需要使用的文件,与符号表一起存档 链接器可以通过它查询哪个对象文件(如果有)定义了符号。

当链接器到达目标文件时,始终链接该对象文件进入程序。链接器从不询问它是否需要对象文件 (无论这可能意味着什么)。任何目标文件都是无条件的链接需要,进一步的投入必须满足。

输入对象文件时,链接器必须将其拆解为 它组成其输入部分并将它们合并到程序中的输出部分中。当输入节S出现在一个对象中时 文件,则S节可能会出现在其他目标文件中; 也许都是。链接器必须将所有输入S部分拼接在一起 放入程序中的单个输出S部分,因此最终不会完成S组成输出部分,直到链接完成。

当共享库libfoo.so输入到链接时,链接器输出 将 DDR 放入程序中(如果它决定需要库或不关心)。这本质上是一个备忘录,将在运行时由 加载器,告诉它libfoo.so是进程的依赖关系 正在施工中;因此,它将通过其标准搜索算法定位libfoo.so, 加载它并将其映射到流程中。

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

因此,链接策略有一个值得尊敬的理由来拒绝链接 对象文件,除非需要,但允许共享库的链接 不需要。链接不必要的对象文件会增加任意数量的死物 对程序的权重,在联动上按比例负担。但 如果链接器不必证明需要共享库,则 可以立即将其链接到程序的大部分中,可以忽略不计。如果开发人员选择将共享库添加到链接中,则很有可能需要它。红帽 坎普仍然认为这个理由已经足够好了。

当然,Debian 阵营也有一个值得尊敬的理由。是的,Debian 链接 涉及确定是否libfoo.so的额外努力,当它是 到达,定义在该处存在未解析引用的任何符号 点。但仅通过链接所需的共享库:-

  • 在运行时,加载器避免了加载冗余的浪费 依赖关系,或者确定它们是多余的,以免加载它们。

  • 如果
  • 出现以下情况,则简化与运行时依赖关系相关的包管理 冗余的运行时依赖项在链接时被清除。

  • 开发人员,像你一样,不会被不一致的链接规则绊倒 适用于静态和共享库!- 一个因以下事实而加剧的障碍 链接器命令行中的-lfoo不会显示它是否会解析libfoo.solibfoo.a.

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

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

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

静态库必须根据需要吗

在 RedHat 土地上,共享库免除了 DO,我们在 它的缺失只是链接提到的每个共享库。正如我们所做的那样 从表面上看,这在链接资源和程序规模上是可以容忍的便宜。如果我们 也免除了静态库的 DO,等效策略将 链接提到的每个静态库中的每个对象文件。但 在联系资源和方案自重方面,这是非常昂贵的。

如果我们想摆脱静态库的 DO,但仍然没有链接 不需要的对象文件,链接器如何继续?

也许像这样?

  • 将显式提到的所有对象文件链接到程序中。
  • 链接所有提到的共享库。
  • 查看是否仍有任何未解析的引用。如果是这样,那么——
  • 从提到的所有静态库中提取所有对象文件 到可选对象文件池中。
  • 然后根据需要针对
  • 此可选池进行链接目标文件,成功或失败。

但这样的东西不会飞。链接器符号的第一个定义 看到是链接的那个。这意味着对象文件的顺序 是链接事项,即使在两个不同的链接订单之间,这两个订单都是 成功

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

可能有多个c.o, d.o, e.o, f.o排序 解析所有引用并给我们一个程序。可能是链接, 比如说,e.o首先解析所有未完成的引用并且没有产生新的引用, 给出一个程序;在链接时说c.o首先还可以解决所有未解决的问题引用,但产生一些新的引用,这需要链接一些或 所有d.o, e.o, f.o- 取决于订单 - 以及每个可能的链接 导致另一个不同的程序。

这还不是全部。可能有多个c.o, d.o, e.o, f.o排序,以便在链接某些对象文件后 -点 P- all 以前未完成的引用已解决,但其中:-

  1. 其中一些排序要么在点 P处不生成新的引用,要么只生成某些进一步的链接顺序可以解析的引用。

  2. 其他的在P点产生新的参照,没有进一步的链接顺序可以解析。

因此,每当链接器发现它在较早的某个时间点做出了类型 2 选择时,它都需要 回溯到这一点并尝试当时可用的其他选择之一,它还没有尝试过, 并且只有在尝试所有链接均失败后才会得出结论。 像这样链接到N 个可选对象文件的池将花费成正比的时间 到阶乘N失败。

像现在一样,使用静态库的 DO,我们在链接器命令行按某种顺序排列:

I0, I1, ... In

这相当于对象文件的排序,为了论证,这可能 ressemble:

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

其中[Oi...]可选对象文件(即静态库)的子序列,它将 此时可供链接器使用。

无论我们在编写命令行时是否知道,我们不仅断言此顺序是 一个好的 DO 排序,可以链接到产生一些程序,而且这种排序也产生了我们想要的程序。

我们可能会在第一个计数上弄错( = 链接失败)。我们甚至可能是 误认为第二个( = 平均链接错误)。但是,如果我们不再关心这些的顺序 输入并以某种方式将其留给链接器以找到一个好的 DO 而不是它们,或者证明没有, 然后:

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

这不会发生。

我们不能收到损坏的DO警告吗?

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

除了像现在这样无法实现联系之外。但是给我们这个 附加警告链接器必须证明链接失败 因为对象文件和静态库不在 DO中,而不仅仅是因为 链接中有一些链接中没有定义的引用。而它 只能通过证明 DO 损坏来证明链接失败某些程序可以通过目标文件和静态库的某种排列来链接。 这是一个阶乘尺度的任务,我们不在乎某些程序是否可以链接, 如果我们打算链接的程序不能:链接器没有关于 除了我们给它的输入之外,我们打算链接什么程序,按以下顺序排列 我们给他们。

如果提到任何库,很容易使链接器(或者更合理的是 GCC 前端)发出警告 在命令行中的任何对象文件之前。但它会有一些令人讨厌的价值,因为 这种联系不一定会失败,实际上可能是预期的。 联动。"对象文件之后的库"只是例程的非常好的指导 通过 GCC 前端调用链接器。更糟糕的是,这样的警告只适用于库后的目标文件,而不适用于库之间DO 中断的情况,因此它只会完成部分工作。


[1] 我的缩写。

[2] 也是我的缩写。

[3] 更准确地说,从静态库中提取对象文件是递归的。 链接器提取定义未解析引用它的任何对象文件 已经掌握,或任何新的未解决的引用,在 链接从库中提取的对象文件。

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

这就是将库放在依赖于它的对象文件前面不起作用的原因。

根据经验,始终将库(甚至是动态库)放在命令行的末尾。并按依赖关系排序。如果模块(对象文件或库)A依赖于模块B,请始终将A放在B之前。

相关文章: