为什么链接库的顺序有时会导致 GCC 中的错误?

Why does the order in which libraries are linked sometimes cause errors in GCC?

提问人:Landon 提问时间:9/5/2008 最后编辑:Konrad RudolphLandon 更新时间:11/10/2022 访问量:248713

问:

为什么链接库的顺序有时会导致 GCC 中的错误?

GCC 链接器

评论

1赞 tripleee 1/5/2015
另请参阅现在 stackoverflow.com/questions/7826448/... -- TLDR 最近更改为更严格的行为(相对)。gcc

答:

4赞 titanae 9/5/2008 #1

我见过很多次,我们的一些模块链接了超过 100 个代码库以及系统和第三方库。

根据不同的链接器,HP/Intel/GCC/SUN/SGI/IBM/等,您可以获得未解析的函数/变量等,在某些平台上,您必须列出两次库。

在大多数情况下,我们使用库、核心、平台、不同抽象层的结构化层次结构,但对于某些系统,您仍然必须按照 link 命令中的顺序进行操作。

一旦你找到了解决方案,就把它记录下来,这样下一个开发人员就不必再次解决它了。

我的老讲师曾经说过,“高内聚和低耦合”,今天仍然如此。

141赞 casualcoder 1/4/2009 #2

GNU ld 链接器是所谓的智能链接器。它将跟踪前面的静态库使用的函数,从其查找表中永久删除那些未使用的函数。结果是,如果过早链接静态库,则该库中的函数在链接行的后面不再可用于静态库。

典型的 UNIX 链接器从左到右工作,因此将所有依赖库放在左侧,将满足这些依赖项的库放在链接行的右侧。您可能会发现某些库依赖于其他库,而其他库则依赖于它们。这就是它变得复杂的地方。当涉及到循环引用时,请修复您的代码!

评论

3赞 Mike 9/5/2008
这是只有 gnu ld/gcc 的东西吗?或者这在链接器中很常见吗?
2赞 MSalters 4/28/2009
显然,更多的Unix编译器也有类似的问题。MSVC 并非完全没有这些问题,但它们似乎并没有那么糟糕。
6赞 Michael Kohne 8/29/2009
MS 开发工具往往不会显示这些问题,因为如果您使用全 MS 工具链,它最终会正确设置链接器顺序,并且您永远不会注意到该问题。
21赞 Michael Burr 4/24/2012
MSVC 链接器对此问题不太敏感,因为它将在所有库中搜索未引用的符号。如果多个库具有该符号,则库顺序仍会影响解析符号。来自 MSDN:“库也按命令行顺序进行搜索,但需要注意以下事项:首先在该库中搜索从库中引入对象文件时未解析的符号,然后从命令行和 /DEFAULTLIB(指定默认库)指令中搜索以下库,然后搜索命令行开头的任何库”
23赞 jww 8/28/2018
“......智能链接器......“ - 我相信它被归类为”单次通过“链接器,而不是”智能链接器”。
691赞 Johannes Schaub - litb 1/4/2009 #3

(请参阅此答案的历史以获得更详细的文本,但我现在认为读者更容易看到真正的命令行)。


以下所有命令共享的通用文件

// a depends on b, b depends on d
$ cat a.cpp
extern int a;
int main() {
  return a;
}

$ cat b.cpp
extern int b;
int a = b;

$ cat d.cpp
int b;

链接到静态库

$ g++ -c b.cpp -o b.o
$ ar cr libb.a b.o
$ g++ -c d.cpp -o d.o
$ ar cr libd.a d.o

$ g++ -L. -ld -lb a.cpp # wrong order
$ g++ -L. -lb -ld a.cpp # wrong order
$ g++ a.cpp -L. -ld -lb # wrong order
$ g++ a.cpp -L. -lb -ld # right order

链接器从左到右搜索,并随时记录未解析的符号。如果库解析符号,则它采用该库的目标文件来解析符号(在本例中为 b.o out of libb.a)。

静态库之间的依赖关系是相同的 - 首先需要符号的库,然后是解析符号的库。

如果一个静态库依赖于另一个库,但另一个库又依赖于前一个库,则存在一个循环。您可以通过用 和 括起来循环依赖库来解决此问题,例如(您可能需要转义括号,例如 和)。然后,链接器会多次搜索这些包含的库,以确保循环依赖项得到解决。或者,您可以多次指定库,以便每个库都位于彼此之前:。-(-)-( -la -lb -)-\(-\)-la -lb -la

链接到动态库

$ export LD_LIBRARY_PATH=. # not needed if libs go to /usr/lib etc
$ g++ -fpic -shared d.cpp -o libd.so
$ g++ -fpic -shared b.cpp -L. -ld -o libb.so # specifies its dependency!

$ g++ -L. -lb a.cpp # wrong order (works on some distributions)
$ g++ -Wl,--as-needed -L. -lb a.cpp # wrong order
$ g++ -Wl,--as-needed a.cpp -L. -lb # right order

这里是一样的 - 库必须遵循程序的目标文件。与静态库相比,这里的区别在于,您不需要关心库之间的依赖关系,因为动态库会自行整理它们的依赖关系

最近的一些发行版显然默认使用链接器标志,该标志强制程序的目标文件位于动态库之前。如果传递了该标志,则链接器将不会链接到可执行文件实际不需要的库(并且它会从左到右检测到这一点)。我最近的 archlinux 发行版默认不使用此标志,因此它没有因未遵循正确的顺序而出现错误。--as-needed

在创建前者时省略 against 的依赖关系是不正确的。届时,您将需要在链接时指定库,但实际上并不需要整数本身,因此不应关心自己的依赖项。b.sod.soaabb

下面是一个示例,说明如果您错过了指定依赖项libb.so

$ export LD_LIBRARY_PATH=. # not needed if libs go to /usr/lib etc
$ g++ -fpic -shared d.cpp -o libd.so
$ g++ -fpic -shared b.cpp -o libb.so # wrong (but links)

$ g++ -L. -lb a.cpp # wrong, as above
$ g++ -Wl,--as-needed -L. -lb a.cpp # wrong, as above
$ g++ a.cpp -L. -lb # wrong, missing libd.so
$ g++ a.cpp -L. -ld -lb # wrong order (works on some distributions)
$ g++ -Wl,--as-needed a.cpp -L. -ld -lb # wrong order (like static libs)
$ g++ -Wl,--as-needed a.cpp -L. -lb -ld # "right"

如果你现在研究二进制文件有什么依赖关系,你会注意到二进制文件本身也依赖于 ,而不仅仅是它应该依赖的。如果以后依赖于另一个库,则需要重新链接二进制文件,如果这样做。如果其他人在运行时加载 use(考虑动态加载插件),调用也会失败。所以真的也应该是一个。libdlibblibblibbdlopen"right"wrong

评论

13赞 7/11/2010
重复上述步骤,直到所有符号都解析为止,呃 - 你会认为它们可以管理拓扑排序。LLVM 本身有 78 个静态库,具有谁知道什么依赖项。没错,它也有一个脚本来找出编译/链接选项 - 但你不能在所有情况下都使用它。
9赞 Johannes Schaub - litb 8/19/2011
@Steve这就是程序 + 的作用。但有时没有顺序,如果你有循环引用。然后,您只需循环浏览库列表,直到所有问题都得到解决。lordertsort
14赞 8/19/2011
@Johannes - 确定最大强连接分量(例如Tarjans算法),然后对分量的(固有的非循环)二图进行拓扑排序。每个组件都可以被视为一个库 - 如果需要组件中的任何一个库,则依赖循环将导致需要该组件中的所有库。所以不,真的没有必要为了解决所有问题而循环遍历所有库,也不需要笨拙的命令行选项——使用两种众所周知的算法的一种方法可以正确处理所有情况。
12赞 jorgen 10/25/2013
我想为这个出色的答案添加一个重要的细节:使用“-( archives -)” 或 “--start-group archives --end-group” 是解决循环依赖关系的唯一可靠方法,因为每次链接器访问存档时,它都会仅拉入(并注册)解析当前未解析符号的目标文件.因此,CMake 在依赖关系图中重复连接组件的算法可能偶尔会失败。(有关更多详细信息,另请参阅 Ian Lance Taylor 关于链接器的优秀博客文章。
5赞 Andrew Falanga 9/5/2014
精彩的文章:它得到了我的赞成票。我会在解决循环依赖关系的部分中添加内容,如果他们想修改,我会把它留给原始发帖人,当使用 gcc 传递 和 选项时,您必须在 as in 之前。在引用的手册页中提到了它,但在这里快速提及也很有帮助。--start-group--end-group-Wl,<option>-Wl,--start-group <archives> -Wl,--end-group
4赞 David Cournapeau 4/28/2009 #4

链接顺序当然很重要,至少在某些平台上是这样。我见过以错误的顺序与库链接的应用程序崩溃(错误意味着 A 在 B 之前链接,但 B 依赖于 A)。

63赞 Lumi 7/16/2011 #5

下面是一个示例,可以清楚地说明在涉及静态库时 GCC 的工作方式。因此,让我们假设我们有以下场景:

  • myprog.o- 包含功能,取决于main()libmysqlclient
  • libmysqlclient- 静态的,为了示例(当然,您更喜欢共享库,因为它很大);在;并依赖于来自libmysqlclient/usr/local/liblibz
  • libz(动态)

我们如何将其联系起来?(注意:使用 gcc 4.3.4 在 Cygwin 上编译的示例)

gcc -L/usr/local/lib -lmysqlclient myprog.o
# undefined reference to `_mysql_init'
# myprog depends on libmysqlclient
# so myprog has to come earlier on the command line

gcc myprog.o -L/usr/local/lib -lmysqlclient
# undefined reference to `_uncompress'
# we have to link with libz, too

gcc myprog.o -lz -L/usr/local/lib -lmysqlclient
# undefined reference to `_uncompress'
# libz is needed by libmysqlclient
# so it has to appear *after* it on the command line

gcc myprog.o -L/usr/local/lib -lmysqlclient -lz
# this works
6赞 yundorri 8/9/2011 #6

您可以使用 -Xlinker 选项。

g++ -o foobar  -Xlinker -start-group  -Xlinker libA.a -Xlinker libB.a -Xlinker libC.a  -Xlinker -end-group 

几乎等于

g++ -o foobar  -Xlinker -start-group  -Xlinker libC.a -Xlinker libB.a -Xlinker libA.a  -Xlinker -end-group 

小心!

  1. 组内的顺序很重要! 下面是一个示例:调试库有一个调试例程,但非调试 库有一个相同的弱版本。您必须将调试库 FIRST,否则将解析为非调试版本。
  2. 您需要在组列表中的每个库前面加上 -Xlinker
9赞 M.M 8/11/2013 #7

一个让我绊倒的快速提示:如果您将链接器调用为“gcc”或“g++”,那么使用“--start-group”和“--end-group”不会将这些选项传递给链接器——也不会标记错误。如果您的库顺序错误,它只会使未定义符号的链接失败。

您需要将它们写成“-Wl,--start-group”等,以告诉 GCC 将参数传递给链接器。

12赞 eckes 3/21/2014 #8

另一种替代方法是指定库列表两次:

gcc prog.o libA.a libB.a libA.a libB.a -o prog.x

这样做,您不必为正确的顺序而烦恼,因为引用将在第二个块中解析。

51赞 SvaLopLop 4/5/2015 #9

如果添加到链接器标志中,则它不关心它们处于哪个顺序,也不关心是否存在循环依赖项。-Wl,--start-group

在Qt上,这意味着添加:

QMAKE_LFLAGS += -Wl,--start-group

节省了大量的时间,而且它似乎并没有减慢链接的速度(无论如何,这比编译花费的时间要少得多)。

评论

5赞 leanid.chaika 9/17/2021
它之所以有效,是因为/usr/bin/ld: missing --end-group; added as last command line option
0赞 Smar 10/18/2022
虽然看起来它并不总是有效,因为缺少.--end-group
0赞 casualcoder 1/27/2023
它有效,但使链接器成为哑链接器,它只是链接所有内容。
0赞 Dražen Grašovec 12/21/2023 #10

将文件放在哪里供链接器解析很重要。

一般规则是将文件放在被引用的位置,在文件被定义之前。symbolsymbol

要了解错误的原因,您必须了解链接器的工作原理:

  • 传递给链接器的所有文件从左到右处理

  • 链接器将未定义的符号从文件收集到未定义的池中 符号

  • 如果存档中的对象无法解析池中的任何符号 未定义的符号,然后它就掉了

在案例中:

gcc -o libfunc2.so -shared -fPIC -lfunc1 func2.o

我们将定义符号的存档库放在引用它的目标文件之前。因此,libfunc1.a 不会解析任何符号并被丢弃。show1()func2.o

GCC -l 库选项:

在命令中编写此选项的位置会有所不同;链接器按指定的顺序搜索和处理库和对象文件。

因此foo.o -lz bar.o

在文件之后但在 之前搜索库。如果引用库中的函数,则可能无法加载这些函数。zfoo.obar.obar.oz