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

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

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

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

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

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

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

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

为什么不能将链接库设为通用(可以通过提及 while compiling/linking 例如静态:-ls 和动态:-ld 等)?

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

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

根据经验,始终将库(即使是动态的)放在命令行的末尾。并按依赖顺序排列。如果模块(目标文件或库)A 依赖模块 B,请始终将 A 放在 B.

之前

As Needed Linux linkage

分裂

你的例子:

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

当它变得不同时。现在 linkage 无法解析 either foo - 来自 共享库 libfoo.so - 或 bar - 来自静态库 libbar.a。和 解决我们需要的问题:

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

with all 在目标文件之后提到的库 - main.o - 那 引用他们定义的符号。

Centos-7 (RedHat) link年龄行为 old-school。 Ubuntu 17.10 (Debian) link年龄行为是在 2013 年的 Debian 7 中引入的,并且逐渐减少 Debian-derived 发行版。如你所见,它取消了区别 在共享库和静态库之间,关于库的需求, 或者不需要,出现在 linkage 序列 after 所有输入 引用它的文件。它们都必须按依赖顺序出现 (DO1), 共享库和静态库一样。

这归结为发行版如何决定构建他们的 GCC 版本 工具链——他们如何选择传递给系统的默认选项 linker (ld) 在被其中一种语言调用时在幕后 front-ends(gccg++gfortran等)为你执行一个linkage。

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

如果--as-needed没有生效那么一个共享库libfoo.so就到达了, 那么它将被 linked 不管 linkage 到目前为止是否累积了任何 对共享库定义的符号的未解析引用。简而言之, 它将被 link 编辑,无论是否证明 需要 到 link 它。也许越远 link时代的进步,到后续的输入,将产生未解决的引用 libfoo.so 解决了,证明了它的 link 年龄。但也许不是。它得到 linked 无论如何。这就是 RedHat 的方式。

如果--as-needed在到达libfoo.so时生效,那么它 当且仅当它导出至少一个符号的定义时才会被 linked 已经 在 link 年龄累积了未解决的引用,即 已证明需要 link 它。它不能结束 linked 如果有 不需要 link 它。这就是 Debian 的方式。

RedHat 使用共享库的方式 linkage 在 Debian 7 之前很流行 打破了队伍。但是静态库的linkagealways符合as needed原则 默认。没有适用于静态库的 --as-needed 选项。 相反,--whole-archive: 无论需要如何,您都需要使用它来覆盖静态库中的默认行为和 link 目标文件。 所以像你这样的人,在 RedHat 领域,观察到这个令人费解的区别:默认静态库 必须在 DO 中 linked;对于共享库,默认情况下任何顺序都可以。 人们在 Debian 土地上看到如此不同。

怎么了?

由于 Redhat 方式有这个令人费解的差异 - 一个绊脚石 link外行的年龄努力 - 从历史上看,很自然地会问 为什么 , 对于静态库,它是 as needed,但是对于共享库,not as needed, 理所当然,以及为什么它仍然在 RedHat 领域。

粗略地简化,linker 通过以下方式组装程序(或共享库) 增量填充 部分 动态依赖记录 (DDRs2) 部分和 DDR 的结构从空开始 最终成为 OS 加载程序可以解析并成功映射的二进制文件 进入进程地址space:例如ELF executable 或 DSO。 ( 这是一个真正的技术术语。 动态依赖记录 不是。 为了方便起见,我现在才创造出来。)

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

当 linker 到达目标文件时,该目标文件 alway linked 进入程序。 linker 从不询问是否需要目标文件 (无论这意味着什么)。任何目标文件都是 linkage 的无条件 source 需要,进一步的输入必须满足。

输入目标文件时,linker 必须将其拆解成 它组成的 input 部分并将它们合并到 output 程序中的部分。当一个输入段S出现在一个对象中时 文件中,S 部分很可能会出现在其他目标文件中; 也许他们所有人。 linker 必须将所有输入 S 部分拼接在一起 进入程序中的单个输出 S 部分,因此最终没有完成 组成输出部分 S 直到 linkage 结束。

当共享库 libfoo.so 输入到 linkage 时,linker 输出 程序中的 DDR(如果它决定需要或不关心该库)。这本质上是一份备忘录,将在运行时由 加载程序,告诉它 libfoo.so 是进程的依赖项 建设中;因此它将通过其标准搜索算法找到 libfoo.so, 加载它并将其映射到进程中。

使用目标文件是相对昂贵的 linkage;消费 共享库相对便宜 - 特别是如果 linker 不 必须事先弄清楚共享库是否需要。 目标文件的 input/output 部分处理通常比写出 DDR 更麻烦。 但比努力更重要的是,linking 目标文件通常会使程序显着 更大,可以使它任意大。 Link共享库添加 只有一个DDR,这总是一个小东西。

因此 linkage 策略有一个 respectable 理由来拒绝 linking 一个目标文件,除非它是需要的,但要容忍共享库的 linking 不需要。 Link使用一个不必要的目标文件会增加任意数量的死机 计划的权重,link 年龄的负担。但 如果 linker 不必证明 需要 共享库,那么它 可以 link 它很快就可以忽略不计地添加到程序的主体中。如果开发人员选择将共享库添加到 linkage,那么很可能需要它。红帽 camp还是觉得理智够了。

Debian 阵营当然也有重新规范table 的理由。是的,一个 Debian linkage 涉及确定是否 libfoo.so 的额外工作,当它是 reached,定义任何有未解析引用的符号 观点。但是仅 linking 所需的共享库:-

  • 在运行时,加载程序避免了加载冗余的浪费 依赖项,或者弄清楚它们是多余的以便不加载它们。

  • 关于运行时依赖性的包管理被简化了,如果 多余的运行时依赖项在 link 时间被淘汰。

  • 像您一样的开发人员,不要被不一致的link年龄规则绊倒 对于静态库和共享库! - 由于以下事实而加剧的障碍 -lfoo 在 linker 命令行中没有显示它是否会解析 至 libfoo.solibfoo.a.

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

现在考虑 linker 如何使用静态库 libabc.a - 目标文件列表 a.o, b.o, c.o按需原则被应用,如下所示:当linker到达libabc.a时, 它手头有 0 个或多个未解析的符号引用 从它已经 linked 的另外 0 个目标文件和共享库转发 进入程序。 linker 的问题是:Are there any object files in 此存档为任何这些未解析的符号引用提供定义? 如果有 0 个这样的未解决的引用,那么答案很简单 No。所以 无需查看存档。 libabc.a 传过去了。 linker 移动 到下一个输入。如果它手头有一些未解析的符号引用,那么 linker 检查由存档中的目标文件定义的符号。它 仅提取那些提供所需符号定义的目标文件(如果有的话) 3 并将这些目标文件输入到 linkage,就好像它们是单独的一样 在命令行中命名并且 libabc.a 根本没有被提及。然后它移动 它到下一个输入,如果有的话。

很明显,静态库的as needed 原则意味着 DO。不 目标文件将从静态库中提取并 linked 除非未解决 引用目标文件中的某个符号ines 已经积累了一些 目标文件(或共享库)已经 linked.

静态库必须根据需要 ?

在 RedHat 领域,共享库免除了 DO,我们在 它的缺席只是 link 每个提到的共享库 。正如我们已经 可见,这在 linkage 资源和程序大小方面是相当便宜的。要是我们 也放弃了静态库的 DO,等效的策略是 是对link提到的每个静态库中的每个目标文件。但 在 link 时代资源和程序自重中,这是非常昂贵的。

如果我们希望静态库没有 DO,但仍然没有 link 没有必要的目标文件,linker 如何继续?

大概是这样的?:-

  • Link 程序中明确提到的所有目标文件。
  • Link 提到的所有共享库。
  • 查看是否还有未解决的引用。如果是这样,那么 -
  • 从提到的所有静态库中提取所有目标文件 放入 可选 对象文件池中。
  • 然后在需要的基础上根据这个可选的池进行link年龄 目标文件,成功或失败。

但是这样的东西是飞不起来的。 linker先定义一个符号 看到的是 linked 的那个。这意味着目标文件的顺序 linked 很重要,即使在两个不同的 linkage order 之间 都是 成功.

说目标文件 a.o, b.o 已经被 linked;未解决的引用 保留,然后 link 用户可以选择可选的目标文件 c.o, d.o, e.o, f.o 继续。

可能 不止一个 排序 c.o, d.o, e.o, f.o 解析所有引用并给我们一个程序。 linking 可能是这种情况, 比如说,e.o 首先解析所有未完成的引用并且不产生新的引用, 给出一个程序;而 linking 说 c.o 首先也会解决所有未解决的问题 参考文献 but 产生一些新的,这需要 linking 一些或 所有 d.o, e.o, f.o - 取决于顺序 - 每个可能的 linkage 导致另一个不同的程序。

这还不是全部。 c.o, d.o, e.o, f.o 的顺序可能不止一个,这样,在 linked 某个目标文件之后 - point P - all 先前未解决的引用已解决,但其中:-

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

  2. 其他人在 P 点产生新的引用,没有进一步的 linkage 顺序可以解析。

所以,每当 linker 发现它在较早的时候做出了第 2 类选择时,它就需要 回到那个点并尝试当时可用的其他选择之一,它还没有尝试过, 并且仅在 linkage 尝试所有方法均未成功时得出结论。 Link像这样针对 N 个可选目标文件池将花费时间成比例 阶乘 N 失败。

像我们现在一样为静态库使用 DO,我们在 linker 命令行以某种顺序排列:

I0, I1, ... In

这等同于目标文件的排序,为了论证可能 重新组合:

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

其中 [Oi...] 可选的 个目标文件(即静态库)的 sub-sequence 那时 linker 可用。

我们是否不知道当我们编写命令行时,我们不仅断言这个命令是 一个好的 DO 排序,可以 linked 产生 some 程序,而且 this 排序产生我们打算的程序。

我们可能在第一次计数时弄错了(= link年龄失败)。我们甚至可能 第二个错误(=平均link年龄错误)。但是如果我们不再关心这些的顺序 输入并把它留给 linker 以某种方式找到一个好的 DO,或者证明没有, 然后:

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

这不会发生。

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

在评论中你问为什么 linker 不能至少 warn 我们如果我们 静态目标文件和静态库不在 DO 中。

那将是对 linkage 失败的补充,就像现在一样。但要给我们这个 附加警告 linker 必须证明 linkage 失败 因为目标文件和静态库不在 DO 中,而且不仅仅是因为 linkage 中有引用,linkage 中没有任何内容定义。而它 只能通过证明 linkage 由于 DO 损坏而失败 某些程序 可以link 由目标文件和静态库的某些排列组合而成。 那是一个 factorial-scale 任务,我们不关心 一些 程序是否可以 linked, 如果我们打算到link的程序不能是:linker没有关于 我们打算 link 什么程序,除了我们给它的输入,顺序是 我们给他们。

如果提到任何库,很容易让 linker(或者更合理地说,GCC 前端)发出警告 在命令行中的任何目标文件之前。但它会有一些令人讨厌的价值,因为 这样的 linkage 不一定会失败,实际上可能是预期的 link年龄。 "Libraries after object files" 只是很好的日常指导 通过 GCC 前端调用 linker。更糟糕的是,这样的警告只适用于库之后的目标文件,而不适用于库之间DO损坏的情况,所以它只会做一些的工作。


[1]我的缩写。

[2]也是我的缩写。

[3] 更准确地说,从静态库中提取目标文件是递归的。 linker 提取任何定义未解析引用的目标文件 已经掌握,任何新的未解决的引用 link正在从库中提取目标文件。