通过两次链接同一库解决循环依赖?

39
我们有一个被分解成静态库的代码库。不幸的是,这些库存在循环依赖关系;例如,libfoo.a 依赖于 libbar.a,反之亦然。
我知道处理这种情况的“正确”方法是使用链接器的 --start-group--end-group 选项,像这样:
g++ -o myApp -Wl,--start-group -lfoo -lbar -Wl,--end-group

但是在我们现有的Makefile中,通常会像这样处理问题:

g++ -o myApp -lfoo -lbar -lfoo

(想象一下这种情况扩展到约20个存在复杂相互依赖的库。)

我一直在查看我们的Makefile,将第二种形式更改为第一种形式,但现在我的同事们正在问我为什么...除了“因为更简洁”和对另一种形式有风险的模糊感觉之外,我没有一个好的答案。

那么,多次链接同一个库会有问题吗?例如,如果同一.o被载入两次,链接是否会失败并导致多次定义符号?或者我们是否有风险得到两个相同的静态对象的副本,从而创建微妙的错误?

基本上,我想知道多次链接同一个库是否可能导致链接时或运行时故障;如果是,如何触发它们。谢谢。


我能想到的唯一问题是当你成功链接到同一库的两个不同版本时。这很难做到,而且(在我看来)不太可能发生在Linux上。此外,仅有20个库看起来不算多。值得浏览makefile吗?你可以把那些时间用于做其他事情。 - SigTerm
3
如果你修复你的库,让它们不再有循环依赖关系,这个问题就会消失。 - Mark B
4
我理解您的意思是通过检查和拆分库来消除循环依赖关系并不可行,因为那将是最清晰的方式。 - Tom Tanner
1
@Mark - 不容易做到,因为这是一个非平凡的传统代码库,一些有用的OOP模式自然地创建了循环依赖。 - Nemo
@SigTerm - 我也想不出具体的问题,这就是为什么我在问这个问题。 - Nemo
第一种形式的缺点是它强制链接器将所有库中的对象都包含在要添加到二进制文件中。这是否对您造成问题取决于您对二进制文件大小的敏感程度,但如果您的应用程序实际上并未使用库中的大部分代码,则使用第一种形式进行链接可能会使您的二进制文件变得更大。第二种形式看起来很混乱,是库设计不良的标志,但从二进制文件大小的角度来看,这可能是处理糟糕情况最有效的方式。 - Al Riddoch
3个回答

10

关于这个问题,

g++ -o myApp -lfoo -lbar -lfoo

这里并不能保证两次使用libfoo和一次使用libbar就足够了。

使用Wl,--start-group ... -Wl,--end-group的方法更好,因为更加健壮。

考虑以下场景(所有符号都在不同的目标文件中):

  • myApp需要在libfoo中定义的符号fooA
  • 符号fooA需要在libbar中定义的符号barB
  • 符号barB需要在libfoo中定义的符号fooC。这是循环依赖,可以通过-lfoo -lbar -lfoo来处理。
  • 符号fooC需要在libbar中定义的符号barD

要能够构建上述情况,我们需要将-lfoo -lbar -lfoo -lbar传递给链接器。为什么?

  1. 链接器第一次看到libfoo,使用符号fooA的定义,但没有使用fooC的定义,因为目前还没有必要将fooC包含在二进制文件中。但是,链接器开始寻找符号barB的定义,因为需要其定义才能让fooA正常工作。
  2. 链接器看到了-libbar,包含了barB的定义(但不包括barD),并开始寻找fooC的定义。
  3. 当第二次处理libfoo时,找到了fooC的定义。现在明显需要barD的定义,但太晚了,命令行上已经没有libbar了!

上述示例可以扩展到任意依赖深度(但在实际情况下很少发生)。

因此,使用

g++ -o myApp -Wl,--start-group -lfoo -lbar -Wl,--end-group

这是一种更加强大的方法,因为链接器会尽可能多地通过库组进行传递 - 只有当一次传递没有改变符号表时,链接器才会继续执行命令行中的下一个库。

然而,这种方法会带来一些性能损失:在第一个示例中,与手动命令行-lfoo -lbar -lfoo相比,-lbar将被扫描多一次。不确定是否值得考虑或提及。


--start-group ... --end-group 是一个方便的开关,还是通过缓存整个组的.o文件或符号表来更快地处理循环依赖关系?我的意思是,如果我将 g++ -o myApp -lfoo -lbar -lfoo -lbar -lfoo 转换为 --start-group ... --end-group,那么我是否期望链接器只扫描一次 lfoolbar 存档? - Grim Fandango
1
这不是一个方便的开关,因为即使在代码中添加了更多的周期,它仍将继续工作。手写的命令行会中断,如果barD突然需要fooE,尽管依赖关系上,“没有什么”改变了,因为仍然是foo和bar相互依赖。为了澄清,仅从已链接的库中引入其他符号可能会导致构建失败。 - FallenWarrior

9
我能提供的是缺乏反例。我从未见过第一种形式(尽管它显然更好),总是使用第二种形式解决问题,但没有观察到任何问题。
即便如此,我仍建议改用第一种形式,因为它清晰地展示了库之间的关系,而不是依赖于链接器以特定方式运行。
话虽如此,我建议至少考虑将代码重构为将公共部分拆分成附加库。

5
谢谢,马克。虽然我觉得很有趣,我的问题一半的评论说“修复你的代码库!”另一半说“为什么要修改一个正常工作的代码库?” :-) - Nemo
第一个形式会引入性能成本,因为链接器尝试查找在所有列出的库中重复的符号。请参见:https://dev59.com/Z3VD5IYBdhLWcg3wO5AD#409470 - Baiyan Huang
1
第一个形式仅适用于GNU ld,因此不是一种可移植的解决方案。 - user1225999

2
由于这是一个传统应用程序,我敢打赌库的结构是继承自某个安排,可能已经不再重要,比如用于构建另一个您不再使用的产品。
即使仍然存在继承库结构的结构原因,几乎可以肯定,从遗留安排构建一个以上的库仍然是可以接受的。只需将20个库中的所有模块放入新库“liballofthem.a”中,然后每个应用程序都是简单的“g++ -o myApp -lallofthem ...”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接