为啥链接库的顺序有时会导致 GCC 出错?
Posted
技术标签:
【中文标题】为啥链接库的顺序有时会导致 GCC 出错?【英文标题】:Why does the order in which libraries are linked sometimes cause errors in GCC?为什么链接库的顺序有时会导致 GCC 出错? 【发布时间】:2008-09-05 02:24:19 【问题描述】:为什么链接库的顺序有时会导致 GCC 出错?
【问题讨论】:
现在参见 ***.com/questions/7826448/… -- TLDRgcc
最近更改为(相对)更严格的行为。
【参考方案1】:
(查看这个答案的历史以获得更详细的文本,但我现在认为读者更容易看到真正的命令行)。
以下所有命令共享的公共文件
$ 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 来自 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
这里也一样——库必须遵循程序的目标文件。与静态库相比,这里的不同之处在于您不需要关心库之间的依赖关系,因为动态库自己会整理它们的依赖关系。
一些最近的发行版显然默认使用--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"
如果您现在查看二进制文件有哪些依赖项,您会注意到二进制文件本身也依赖于libd
,而不仅仅是libb
。如果libb
稍后依赖于另一个库,则需要重新链接二进制文件,如果你这样做的话。如果其他人在运行时使用dlopen
加载libb
(考虑动态加载插件),调用也会失败。所以"right"
也应该是wrong
。
【讨论】:
重复直到所有符号都解决了,嗯 - 你会认为他们可以管理拓扑排序。 LLVM 拥有自己的 78 个静态库,具有谁知道什么依赖项。确实,它还有一个脚本来计算编译/链接选项 - 但你不能在所有情况下都使用它。 @Steve 这就是程序lorder
+ tsort
所做的。但有时没有顺序,如果你有循环引用。然后你只需要循环浏览库列表,直到一切都解决。
@Johannes - 确定最大的强连接组件(例如 Tarjans 算法),然后对组件的(固有非循环的)有向图进行拓扑排序。每个组件都可以被视为一个库 - 如果需要组件中的任何一个库,则依赖循环将导致需要该组件中的所有库。所以不,确实不需要循环遍历所有库来解决所有问题,也不需要笨拙的命令行选项 - 使用两种众所周知的算法的一种方法可以正确处理所有情况。
我想为这个出色的答案添加一个重要的细节:使用“-(archives -)”或“--start-group archives --end-group”是唯一确定的-fire 解决循环依赖关系的方法,因为每次链接器访问档案时,它都会拉入(并注册未解析的符号)仅解析当前未解析符号的目标文件。因此,CMake 在依赖图中重复连接组件的算法可能偶尔会失败。 (有关更多详细信息,另请参阅链接器上的 Ian Lance Taylor's excellent blog post。)
您的回答帮助我解决了链接错误,并且您已经非常清楚地解释了如何避免陷入困境,但是您知道为什么它会以这种方式工作吗?【参考方案2】:
GNU ld 链接器是所谓的智能链接器。它将跟踪前面的静态库使用的函数,永久地从其查找表中丢弃那些未使用的函数。结果是,如果您过早地链接静态库,则该库中的函数在链接行后面的静态库中不再可用。
典型的 UNIX 链接器从左到右工作,因此将所有依赖库放在左侧,将满足这些依赖关系的库放在链接行的右侧。您可能会发现一些库依赖于其他库,而同时其他库也依赖于它们。这就是复杂的地方。当涉及到循环引用时,请修复您的代码!
【讨论】:
这是只有 gnu ld/gcc 的东西吗?或者这是链接器常见的东西? 显然更多的 Unix 编译器有类似的问题。 MSVC 也不是完全没有这些问题,但它们似乎并没有那么糟糕。 MS 开发工具不会过多地显示这些问题,因为如果您使用全 MS 工具链,它最终会正确设置链接器顺序,而您永远不会注意到问题。 MSVC 链接器对这个问题不太敏感,因为它会在所有库中搜索未引用的符号。如果多个库具有符号,则库顺序仍会影响 哪个 符号被解析。来自 MSDN :“库也按命令行顺序搜索,但有以下警告:首先在该库中搜索从库中引入目标文件时未解析的符号,然后从命令行中搜索以下库和/DEFAULTLIB(指定默认库)指令,然后到命令行开头的任何库" "... 智能链接器 ..." - 我相信它被归类为“单通道”链接器,而不是“智能链接器”。【参考方案3】:这里有一个例子来说明当涉及到 static 库时,GCC 是如何工作的。所以让我们假设我们有以下场景:
myprog.o
- 包含 main()
函数,依赖于 libmysqlclient
libmysqlclient
- 静态,为了示例(当然,您更喜欢共享库,因为 libmysqlclient
很大);在/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
【讨论】:
【参考方案4】:如果您将-Wl,--start-group
添加到链接器标志,则它不关心它们的顺序或是否存在循环依赖关系。
在 Qt 上,这意味着添加:
QMAKE_LFLAGS += -Wl,--start-group
节省了大量的时间,而且它似乎并没有减慢链接速度(无论如何这比编译花费的时间要少得多)。
【讨论】:
它之所以有效是因为/usr/bin/ld: missing --end-group; added as last command line option
【参考方案5】:
另一种选择是两次指定库列表:
gcc prog.o libA.a libB.a libA.a libB.a -o prog.x
这样做,您不必担心正确的顺序,因为引用将在第二个块中解析。
【讨论】:
【参考方案6】:一个让我大吃一惊的小技巧:如果您将链接器调用为“gcc”或“g++”,那么使用“--start-group”和“--end-group”将不会传递这些选项通过链接器——它也不会标记错误。如果您的库顺序错误,它将使用未定义的符号链接失败。
您需要将它们写为“-Wl,--start-group”等,以告诉 GCC 将参数传递给链接器。
【讨论】:
【参考方案7】:您可以使用 -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
小心!
-
组内的顺序很重要!
这是一个例子:调试库有一个调试例程,但非调试
库具有相同的弱版本。你必须把调试库
组中的第一个,否则您将解析为非调试版本。
您需要在组列表中的每个库前面加上 -Xlinker
【讨论】:
【参考方案8】:我已经看到了很多,我们的一些模块链接了超过 100 个我们的代码库以及系统和第 3 方库。
根据不同的链接器 HP/Intel/GCC/SUN/SGI/IBM/etc,您可以获得未解析的函数/变量等,在某些平台上您必须列出两次库。
在大多数情况下,我们使用库、核心、平台、不同抽象层的结构化层次结构,但对于某些系统,您仍然需要使用链接命令中的顺序。
一旦你找到了解决方案,就将它记录下来,这样下一个开发人员就不必再次解决它。
我的老讲师曾经说过,“高内聚低耦合”,今天仍然如此。
【讨论】:
【参考方案9】:至少在某些平台上,链接顺序确实很重要。我见过以错误顺序链接到库的应用程序崩溃(错误意味着 A 在 B 之前链接,但 B 取决于 A)。
【讨论】:
以上是关于为啥链接库的顺序有时会导致 GCC 出错?的主要内容,如果未能解决你的问题,请参考以下文章
GCC 编译使用动态链接库和静态链接库--及先后顺序----及环境变量设置总结