Xcode LD 检测到静态库中重复的符号问题

3
这个问题之前在gcc中被提出过,但是Darwin的ld (clang?)似乎处理方式不同。假设我在main1.cc和main2.cc两个文件中都定义了一个main()函数,如果我试图把它们一起编译,会得到(预期的)重复符号错误提示:
$ g++ -o main1.o -c main1.cc
$ g++ -o main2.o -c main2.cc
$ g++ -o main main1.o main2.o
duplicate symbol _main in:
    main1.o
    main2.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

但是,如果我将其中一个放入静态库中,当我链接应用程序时,就不会出错:

$ ar rcs libmain1.a main1.o
$ g++ -o main libmain1.a main2.o
(no error)

使用gcc,您可以使用--whole-archive选项包装库,然后gcc的ld将产生错误。但是,xcode附带的ld不支持此选项。
是否有可能让ld打印出错误信息?

main()是一个全局函数,它是程序的入口点。每个进程都是程序的一个实例。如果你只链接两个目标文件然后生成最终的可执行程序,它必须只有一个main()函数。如果你链接一个静态库,入口点是main2.o中的main()函数。链接器会复制粘贴main2.o需要但实际上存在于libmain1.a中的任何外部代码。libmain1.a中的main()函数将不会存在于最终的程序main中。所以这是可以的。 - merito
1个回答

4
我相信您知道不应将包含main函数的目标文件放入静态库中。以防万一我们读者中有人不清楚:库用于包含许多程序可以重复使用的函数。程序只能包含一个main函数,并且程序的main函数可重用的可能性微乎其微,作为另一个程序的main函数。因此,main函数不适合放在库中(有一些奇怪的例外情况)。
接下来是您担心的问题。为简单起见,在本文的其余部分中,我将排除共享/动态库的链接考虑。
当竞争定义位于不同的输入目标文件中时,链接器检测到重复符号错误(也称为多重定义错误),但当一个定义位于输入静态库中而另一个定义位于输入目标文件中时,它无法检测到它。在这种情况下,GNU链接器可以在静态库之前传递--whole-archive选项来检测重复定义的符号。但是,您的链接器(Darwin Mach-O链接器)没有该选项。
请注意,尽管您的链接器不支持--whole-archive,但它具有等效的选项-all_load。但是,请不要过度使用它,因为这种担忧是没有根据的。对于两个链接器:
  • 在[foo.o ... bar.o]情况下,确实存在多重定义错误。
  • 在[foo.o ... libbar.a]情况下,确实不存在多重定义错误。
此外,对于GNU链接器:
  • 在[foo.o ... --whole-archive libbar.a]情况下,确实存在多重定义错误。
在任何情况下,任何一个链接器都不允许多个符号定义未被检测到并且任意使用其中一个。
链接foo.o和链接libfoo.o之间有什么区别?
链接器只会将目标文件添加到程序中。更准确地说,当它遇到输入文件foo.o时,它将从foo.o中添加到程序中所有符号引用和符号定义。 (至少是起点:如果您已请求,并且如果可以在不附带丢弃任何使用的定义的情况下执行此操作,则最终可能会丢弃未使用的定义)。
静态库就是一个对象文件的集合。当链接器遇到一个输入文件libfoo.a时,默认情况下它不会将袋子中的任何对象文件添加到程序中。
只有在链接过程中,如果已经向程序添加了一些没有定义的符号引用,则会检查袋子的内容。这些未解析的符号可能在袋子中的某些对象文件中定义。
如果需要查看袋子中的内容,则会检查对象文件以查看其中是否包含程序中已有的未解析符号的定义。如果有这样的对象文件,那么将它们添加到程序中并重新考虑是否需要继续查看袋子。当找不到程序所需或已找到程序引用的所有符号的定义的对象文件时,它停止查找袋子,无论哪种情况先出现。
如果需要袋子中的任何对象文件,则至少会向程序添加一个符号定义,并可能添加更多未解析符号。然后链接器继续执行。一旦它遇到libfoo.a并考虑了该包中是否需要为您的程序添加其中任何对象文件,它就不会再次考虑该包,除非它在链接序列中稍后再遇到它。
因此......
情况1.输入文件包含[foo.o ... bar.o]。均定义符号A。必须链接两个对象文件,因此必须将A的两个定义添加到程序中,这是一个多重定义错误。两个链接器都会检测到它。
情况2。输入文件包含[foo.o ... libbar.a]。
  • libbar.a包含对象文件a.ob.o
  • foo.o定义符号A并引用但未定义符号B
  • a.o也定义了A,但未定义B,并且未定义foo.o引用的其他符号。
  • b.o定义符号B
然后:
  • foo.o处,必须链接目标文件。链接器将A的定义和对B的未解决引用添加到程序中。
  • libbar.a处,链接器需要一个未解决引用B的定义,因此它查找包中的内容。
  • a.o不定义B或任何其他未解决符号。它没有被链接。第二个A的定义也没有被添加。
  • b.o定义了B,因此它被链接。B的定义被添加到程序中。
  • 链接器继续执行。

在程序中不需要两个都定义A的目标文件。不会出现多重定义错误。

情况3 输入文件包含[foo.o ... libbar.a]。

  • libbar.a包含目标文件a.ob.o
  • foo.o定义符号A。它引用但未定义符号BC
  • a.o也定义了A,它定义了B,并且未定义foo.o引用的其他符号。
  • b.o定义符号C

然后:

  • foo.o处,链接目标文件。链接器将A的定义和对BC的未解决引用添加到程序中。
  • libbar.a处,链接器需要未解决引用BC的定义,因此它查找包中的内容。
  • a.o不定义C。但是它确实定义了B。因此a.o被链接。这会添加所需的B定义,以及不需要的多余A的定义。

这是一个多重定义错误。两个链接器都检测到它。链接结束。

仅当程序中链接的目标文件包含某个符号的两个定义时,才会出现多重定义错误。静态库中的目标文件仅链接以提供程序引用的符号的定义。如果存在多重定义错误,则两个链接器都会检测到它。

那么为什么GNU链接器选项--whole-archive会产生不同的结果?

假设 libbar.a 包含 a.ob.o。那么:
foo.o --whole-archive -lbar

告诉连接器链接libbar.a中的所有目标文件,无论它们是否被需要。因此,这个链接命令片段等价于:
foo.o a.o b.o

因此,在上述情况2中,添加--whole-archive是一种创建多重定义错误的方法,如果没有它,则不会有任何错误。而不是一种检测未被检测到的多重定义错误的方法。
如果错误地使用--whole-archive作为一种“检测”虚假多重定义错误的方法,并且在链接仍然成功的那些情况下,这也是一种向程序中添加大量冗余代码的方法。Mach-O链接器的-all_load选项也是如此。
还不满意吗?
即使所有这些都清楚了,也许您仍然渴望某种方式,使得在链接中定义符号的输入对象文件与另一个不需要链接但恰好包含在某个输入静态库中的对象文件中也定义该符号成为错误。
嗯,那可能是您想知道的情况,但它根本不是任何类型的链接错误,无论是多重定义还是其他类型。静态库在链接中的目的是提供默认符号定义,您可以在输入对象文件中未定义这些符号。在对象文件中提供自己的定义,将忽略库的默认值。
如果您不希望链接工作方式像这样-它的预期工作方式,但是:-
  • 您仍然想使用静态库
  • 您不希望来自输入对象文件的任何定义都优于静态库成员中的定义
  • 您不想链接任何冗余的对象文件。
那么最简单的解决方案(虽然在构建时可能不是最省时的)是:
在项目构建中提取所有静态库成员作为链接步骤的先决条件,以一种还可以获得它们名称列表的方式,例如:
$ LIBFOOBAR_OBJS=`ar xv libfoobar.a | sed 's/x - //'g`
$ echo $LIBFOOBAR_OBJS
foo.o bar.o

(但要将它们提取到不能破坏您构建的任何目标文件的地方)。然后,在链接步骤之前再次运行一个初步的丢弃链接,其中$LIBFOOBAR_OBJS替换了libfoobar.a。例如,而不是:

cc -o prog x.o y.o z.o ... -lfoobar ...

运行

cc -o deleteme x.o y.o z.o ... $LIBFOOBAR_OBJS ...

如果初步链接失败 - 出现多重定义错误或其他任何错误 - 则应在那里停止。否则,请继续进行实际的链接。你不会在 prog 中链接任何冗余的目标文件。代价是执行一个冗余的链接 deleteme,除非它因多重定义错误而失败1 在专业实践中,没有人会这样运行构建,以预防程序员在 x.o y.o z.o 中定义了一个函数,而无意中删除了 libfoobar.a 中定义的一个成员中的函数。职业能力和代码审查都被认为可以避免这种情况,就像它们被认为可以避免程序员在 x.o y.o z.o 中定义一个函数来完成应当使用库资源完成的任何操作一样。
[1] 与其从静态库中提取所有对象文件以用于临时链接,不妨考虑使用 GNU 链接器的 --whole-archive 或 Mach-O 链接器的 -all_load 进行可抛弃的链接。但是这种方法存在潜在的陷阱,在此不进行深入讨论。

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