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

Lan*_*don 429 linker gcc

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

Joh*_*itb 540

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


以下所有命令共享的公共文件

$ cat a.cpp
extern int a;
int main() {
  return a;
}

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

$ cat d.cpp
int b;
Run Code Online (Sandbox Code Playgroud)

链接到静态库

$ 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
Run Code Online (Sandbox Code Playgroud)

链接器从左向右搜索,并记录未解析的符号.如果库解析符号,它将获取该库的目标文件来解析符号(在本例中为libb.a).

静态库相互依赖的工作方式相同 - 需要符号的库必须是第一个,然后是解析符号的库.

如果静态库依赖于另一个库,但另一个库再次依赖于前一个库,则存在一个循环.你可以通过用-(和封闭循环相关的库来解决这个问题-),例如-( -la -lb -)(你可能需要逃避parens,例如-\(-\)).然后,链接器会多次搜索这些封闭的lib,以确保解析循环依赖关系.或者,您可以多次指定库,因此每个库都在彼此之前:-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
Run Code Online (Sandbox Code Playgroud)

它在这里是一样的 - 库必须遵循程序的目标文件.这里与静态库的不同之处在于,您不必关心库之间的依赖关系,因为动态库本身会对其依赖关系进行排序.

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

在创建前者时省略b.so反对的依赖是不正确d.so的.当链接时a,您将需要指定库,但a实际上并不需要整数b本身,因此不应该关心b自己的依赖关系.

以下是您错过指定依赖项的含义示例 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"
Run Code Online (Sandbox Code Playgroud)

如果您现在查看二进制文件具有哪些依赖关系,您会注意到二进制文件本身也依赖于它libd,而不仅仅是libb它应该如此.如果libb以后依赖于另一个库,则需要重新链接二进制文件,如果这样做的话.如果其他人在运行时加载libb使用dlopen(想想动态加载插件),调用也会失败.所以"right"真的应该是一个wrong.

  • 重复,直到所有符号都解决了,呃 - 你认为他们可以管理拓扑排序.LLVM有自己的78个静态库,谁知道什么是依赖项.是的,它还有一个脚本来确定编译/链接选项 - 但你不能在所有情况下使用它. (10认同)
  • @Johannes - 确定最大强连通分量(例如Tarjans算法),然后拓扑排序组件的(固有非循环)图.可以将每个组件视为一个库 - 如果需要组件中的任何一个库,则依赖性循环将导致需要该组件中的所有库.所以不,没有必要循环遍历所有库以解决所有问题,并且不需要笨拙的命令行选项 - 使用两个众所周知的算法的一种方法可以正确处理所有情况. (10认同)
  • @Steve这就是程序`lorder` +`tsort`的作用.但是,如果你有循环引用,有时候没有顺序.然后你只需循环遍历库列表,直到一切都解决了. (6认同)
  • 我想在这个优秀的答案中添加一个重要细节:使用" - (archives - )"或"--start-group archives --end-group"**是解决循环依赖关系的唯一可靠方法**因为每次链接器访问一个存档时,它就会拉入(并注册未解析的符号)_only目标文件,解析当前未解析的符号_.因此,CMake在依赖图中重复连接组件的算法可能偶尔会失败.(有关详细信息,请参阅[Ian Lance Taylor的优秀博客文章](http://www.airs.com/blog/archives/48)关于链接器.) (4认同)
  • 您的回答帮助我解决了我的链接错误,并且您已经非常清楚地解释了如何避免陷入困境,但您是否知道为什么它设计为以这种方式工作? (3认同)
  • 神奇的写作:它得到了我的投票.我将添加到关于_resolving cyclic dependencies_的部分,如果他们想要修改,我将把它留给原始的海报,当使用gcc传递`--start-group`和`--end-group`选项时,你必须在`-Wl,<option>`之前,如`-Wl, - start-group <archives> -Wl, - end-group`.在参考的手册页中提到了它,但是这里的快速提及也是有帮助的. (3认同)
  • 约翰内斯很棒的帖子,谢谢.我有静态库订单的问题 - 订购的要求是我所期望的_opposite_!从ld联机帮助页:"链接器将仅在命令行上指定的位置搜索一次存档.如果存档定义了在命令行中存档之前出现的某个对象中未定义的符号,则链接器将包含存档中的相应文件.但是,稍后在命令行中出现的对象中的未定义符号将不会导致链接器再次搜索存档." (2认同)
  • @Steve不幸的是我没有对这些算法做过任何事情.但我希望有一个易于使用的LD命令行选项与你提到的那些方法.也许您可以将您的想法发送到相应的GNU邮件列表? (2认同)
  • @Johannes - 不是我的主意.我首先从Dobbs博士的文章中读到有关其他类型的编程工具的文章(IIRC,拓扑排序是性能优化,但依赖图不能保证非循环),它似乎并不特别然后.我认为GNU选择拒绝这种方法,可能是出于宗教原因.毕竟,还有其他连接器必须做到这一点或类似的东西 - 我没有在这里发明任何聪明或原创的东西.尽管如此,写一封电子邮件并不会有什么坏处 - 我很快就会解决这个问题. (2认同)
  • @jin我怀疑使用了文本编辑器.两个(一个有机和一个软件).除此之外,请查看http://search.cpan.org/~tels/Graph-Easy/bin/graph-easy#ASCII_output (2认同)
  • "动态图书馆"错字.我认为如果使用像`--as-needed`这样的选项(ubuntu IIRC中的默认值)并且库没有链接到它们依赖的所有内容(`--allow-shlib-undefined`),顺序对动态库很重要. ),但这对于一般性介绍来说可能太罕见了. (2认同)
  • 动态共享库和静态库之间的差异是因为动态库*已经链接*并且已经整理了它们的依赖关系。 (2认同)

cas*_*der 98

GNU ld链接器是所谓的智能链接器.它将跟踪前面的静态库使用的函数,永久地抛弃那些未在其查找表中使用的函数.结果是,如果过早链接静态库,那么稍后链接行上的静态库将不再提供该库中的函数.

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

  • MSVC链接器对此问题不太敏感,因为它将在所有库中搜索未引用的符号.如果多个库具有该符号,则库顺序仍然可以影响*哪个*符号得到解析.从MSDN:"也按命令行顺序搜索库,但有以下警告:首先在该库中搜索从库中引入目标文件时未解析的符号,然后从命令行搜索以下库,/DEFAULTLIB(指定默认库)指令,然后到命令行开头的任何库" (16认同)
  • MS开发工具并不倾向于显示这些问题,因为如果您使用全MS工具链,它最终会正确设置链接器顺序,您永远不会注意到这个问题. (4认同)
  • 这只是gnu ld/gcc吗?或者这与链接器有什么共同之处? (2认同)
  • 显然,更多的Unix编译器也有类似的问题.MSVC并非完全没有这些问题,但它们看起来并不那么糟糕. (2认同)
  • *“ ...智能链接器...” *-我相信它被归类为“单通”链接器,而不是“智能链接器”。 (2认同)
  • 关于“单遍非智能链接器不会以相同的方式执行”:算法很简单:对于链接命令行上库中的每个模块,如果该模块满足当前挂起的引用,则将该模块包含在输出文件中正在建设中。否则,不要。未选择的模块中的符号可能根本不会输入到链接器的查找表中,因此它们不会从其中被丢弃。 (2认同)

Lum*_*umi 52

这是一个示例,用于说明在涉及静态库时GCC的工作原理.所以我们假设我们有以下场景:

  • myprog.o- 包含main()功能,依赖于libmysqlclient
  • libmysqlclient- 静态,为了这个例子(你更喜欢共享库,当然,因为它libmysqlclient是巨大的); in /usr/local/lib; 并依赖于来自的东西libz
  • 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
Run Code Online (Sandbox Code Playgroud)


Sva*_*Lop 28

如果添加-Wl,--start-group到链接器标志,它不关心它们在哪个顺序或者是否存在循环依赖关系.

在Qt上这意味着添加:

QMAKE_LFLAGS += -Wl,--start-group
Run Code Online (Sandbox Code Playgroud)

节省了大量的时间,它似乎没有减慢连接速度(这比编译所花费的时间少得多).

  • 它之所以有效,是因为 ```/usr/bin/ld: 缺少 --end-group; 添加为最后一个命令行选项``` (5认同)

eck*_*kes 8

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

gcc prog.o libA.a libB.a libA.a libB.a -o prog.x
Run Code Online (Sandbox Code Playgroud)

这样做,您不必打扰正确的序列,因为参考将在第二个块中解析.


M.M*_*M.M 7

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

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


小智 5

您可以使用-Xlinker选项.

g++ -o foobar  -Xlinker -start-group  -Xlinker libA.a -Xlinker libB.a -Xlinker libC.a  -Xlinker -end-group 
Run Code Online (Sandbox Code Playgroud)

几乎等于

g++ -o foobar  -Xlinker -start-group  -Xlinker libC.a -Xlinker libB.a -Xlinker libA.a  -Xlinker -end-group 
Run Code Online (Sandbox Code Playgroud)

小心!

  1. 组内的顺序很重要!下面是一个示例:调试库具有调试例程,但非调试库具有相同的弱版本.您必须将调试库FIRST置于组中,否则您将解析为非调试版本.
  2. 您需要在组列表中的每个库之前使用-Xlinker


tit*_*nae 4

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

\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n