静态链接libstdc++:有什么需要注意的地方吗?

116

我需要部署一个在Ubuntu 12.10上使用GCC 4.7的libstdc ++构建的C ++应用程序到运行Ubuntu 10.04的系统中,该系统带有一个相当旧的版本的libstdc++。

目前,我正在使用-static-libstdc++ -static-libgcc编译,正如这篇博客文章所建议的:静态链接libstdc ++。作者警告说,在编译libstdc ++静态库时不要使用任何动态加载的C++代码,这是我尚未检查过的问题。不过,到目前为止,一切似乎都很顺利:我可以在Ubuntu 10.04上使用C ++11特性,这正是我想要的。

我注意到这篇文章是从2005年开始的,或许从那以后已经发生了很多变化。它的建议仍然适用吗?是否有任何潜在问题需要我注意?


不,静态链接到libstdc++并不意味着那样。如果它确实意味着那样,那么-static-libstdc++选项就没有意义了,你只需使用-static即可。 - Jonathan Wakely
@JonathanWakely -static 在某些 Ubuntu 1404 系统中会出现“内核过旧”的错误。glibc.so 就像 Windows 中的 kernel32.dll,它是操作系统接口的一部分,我们不应该将其嵌入到我们的二进制文件中。您可以使用 objdump -T [binary path] 命令查看是否动态加载了 libstdc++.so。对于 Golang 程序员,您可以在导入 "C" 之前添加 #cgo linux LDFLAGS: -static-libstdc++ -static-libgcc - bronze man
@bronzeman,但我们讨论的是-static-libstdc++而不是-static,因此libc.so将不会被静态链接。 - Jonathan Wakely
1
@NickHutchinson,链接的博客文章已经不存在了。这个SO问题是相关术语的热门搜索结果。你能否在你的问题中重现那篇博客文章的关键信息,或者如果你知道它被移动到哪里,提供一个新的链接? - Brian Cain
2
@BrianCain 互联网档案馆有它的备份:https://web.archive.org/web/20160313071116/http://www.trilithium.com/johan/2005/06/static-libstdc/ - Rob Keniger
@bronzeman glibc 不是 Linux 发行版上的唯一操作系统接口,而只是其中之一。还有 dietlibc、musl-libc 等等……即使没有 glibc,也完全可以使用系统调用。选择是否使用 glibc 取决于发行版,如果需要的话,用户可以有很大的灵活性来解决这个问题。 - 0xC0000022L
5个回答

165

那篇博客文章相当不准确。

据我所知,每个GCC的主要版本发布(即第一或第二个版本号组件有所不同)都会引入C++ ABI变更。

这并不正确。自GCC 3.4以来,引入的唯一C++ ABI变化是向后兼容的,这意味着近九年来C++ ABI一直保持稳定。

更糟糕的是,大多数主流Linux发行版使用GCC快照和/或补丁其GCC版本,使得在分发二进制文件时几乎不可能知道你正在处理哪些GCC版本。

发行版的GCC修补程序版本之间的差异很小,并且没有ABI更改,例如Fedora的4.6.3 20120306 (Red Hat 4.6.3-2)与上游FSF 4.6.x发布版本和几乎肯定与任何其他发行版的4.6.x是ABI兼容的。

在GNU/Linux上,GCC的运行时库使用ELF符号版本控制,因此很容易检查对象和库所需的符号版本,如果你有一个提供这些符号的libstdc++.so,它将工作正常,无论它是否是另一个版本的您的发行版。

但是,如果要使其工作,则不得将任何使用C++运行时支持的代码(或任何C++代码)链接动态。

这也不正确。

话虽如此,静态链接到libstdc++.a是一种选择。

如果你使用dlopen动态加载库,可能无法正常工作的原因是,在你静态链接时,应用程序未使用的依赖于libstdc++符号,因此这些符号不会出现在可执行文件中。解决方法是将共享库动态链接到libstdc++.so(如果它依赖于它,这是正确的做法)。ELF符号交叉引用意味着共享库将使用可执行文件中存在的符号,但其他未在可执行文件中出现的符号将在它连接到的任何libstdc++.so中找到。如果您的应用程序不使用dlopen,则无需担心这些问题。另一种选择(也是我更喜欢的)是在您的应用程序旁部署更新的libstdc++.so文件,并确保它在默认系统libstdc++.so之前被找到,可以通过强制动态链接器查找正确的位置来实现,可以使用$LD_LIBRARY_PATH环境变量在运行时或在链接时通过设置可执行文件中的RPATH来完成。 我更喜欢使用RPATH,因为它不依赖于环境是否正确设置以使应用程序正常工作。如果您使用'-Wl,-rpath,$ORIGIN'(请注意单引号以防止shell尝试展开$ORIGIN)链接您的应用程序,则可执行文件将具有$ORIGINRPATH,这告诉动态链接器在与可执行文件本身相同的目录中查找共享库。如果您将更新的libstdc++.so放在可执行文件相同的目录中,则可以在运行时找到它,问题解决了。(另一种选择是将可执行文件放在/some/path/bin/中,将更新的libstdc++.so放在/some/path/lib/中,并使用'-Wl,-rpath,$ORIGIN/../lib'或与可执行文件相关的任何其他固定位置链接,并相对于$ORIGIN设置RPATH。)

16
这个解释,特别是关于RPATH的部分,非常出色。 - nilweed
7
在Linux上将libstdc++与应用程序一起发布是错误的建议。可以通过搜索“steam libstdc++”来了解相关问题。简而言之,如果您的可执行文件加载了需要再次打开libstdc++(例如radeon驱动程序)的外部库(如OpenGL),那么这些库将使用已经加载的_您的_libstdc++,而不是它们自己所需和期望的库。因此,您又回到了原点。 - user519179
8
@cap,OP特别询问如何部署到一个系统libstdc++较旧的发行版上。Steam的问题在于他们捆绑了一个比系统版本更旧的libstdc++.so文件(可能在捆绑时是更新的,但发行版已经升级到更新的版本)。可以通过使RPATH指向一个目录来解决这个问题,该目录包含在安装时设置为指向捆绑库或系统库中较新一个的libstdc++.so.6符号链接。还有一些更复杂的混合链接模型,如Red Hat DTS所使用的,但自己实现这些模型很困难。 - Jonathan Wakely
6
嘿,朋友,如果我不想让我的模型用于运送向后兼容的二进制文件时包括“相信其他人保持libstdc++ ABI兼容性”或“在运行时有条件地链接libstdc++”,我很抱歉...如果这引起了一些不满,我能做什么呢?我的意思并不是不尊重。如果你还记得memcpy@GLIBC_2.14事件,你就不能怪我对此持有不信任感 :) - user519179
7
我必须使用“-Wl,-rpath,$ORIGIN”(请注意rpath前面的“-”符号)。我无法编辑答案,因为编辑必须至少包含6个字符.... - user368507
显示剩余28条评论

14

Jonathan Wakely的回答很好,我要补充一点关于为什么dlopen()存在问题的事情:

由于GCC 5中引入了新的异常处理池(参见PR 64535PR 65434),如果你dlopen并dlclose一个静态链接到libstdc++的库,每次都会造成内存泄漏(池对象)。因此,如果有任何可能使用dlopen,将libstdc++静态链接似乎是一个非常糟糕的想法。请注意,这是一个真正的泄漏,而不是在PR 65434中提到的良性泄漏。


2
函数__gnu_cxx::__freeres()似乎至少在某种程度上有助于解决这个问题,因为它释放了池对象的内部缓冲区。但对我来说,调用此函数对随后意外抛出的异常有什么影响还不太清楚。 - phlipsy

4

关于 Jonathan Wakely 的答案,我想补充一点有关 RPATH 的内容:

只有当需要的 RPATH 是运行应用程序的 RPATH 时,RPATH 才能起作用。如果您有一个库通过自己的 RPATH 动态链接到任何库,那么该库的 RPATH 将被加载它的应用程序的 RPATH 覆盖。当您无法保证应用程序的 RPATH 与您的库的 RPATH 相同时,这会是一个问题。例如,如果您期望依赖项在特定目录中,但该目录不是应用程序的 RPATH 的一部分。

举个例子,假设您有一个名为 App.exe 的应用程序,它动态链接到 GCC 4.9 的 libstdc++.so.x。App.exe 通过 RPATH 解决了此依赖关系,即

App.exe (RPATH=.:./gcc4_9/libstdc++.so.x)

现在假设有另一个库Dependency.so,它在GCC 5.5上具有对libstdc++.so.y的动态链接依赖关系。此处的依赖关系通过库的RPATH解决。

Dependency.so (RPATH=.:./gcc5_5/libstdc++.so.y)

当App.exe加载Dependency.so时,它既不会附加也不会预置库的RPATH。它根本不会查看它。唯一被考虑的RPATH将是正在运行的应用程序或本例中的App.exe的RPATH。这意味着,如果库依赖于gcc5_5/libstdc++.so.y中存在但在gcc4_9/libstdc++.so.x中不存在的符号,则该库将无法加载。
这只是一个警告,因为我过去也遇到了这些问题。RPATH是一个非常有用的工具,但它的实现仍然有一些需要注意的地方。

因此,对于共享库来说,RPATH有点毫无意义!我曾经希望,在过去的20年中,他们在这方面改进了Linux... - user12411795

3
你可能还需要确保不依赖于动态glibc。在你的可执行文件上运行ldd,注意任何动态依赖关系(libc/libm/libpthread是常见的嫌疑人)。
额外的练习是使用这种方法构建一堆涉及C++11的示例,并在真正的10.04系统上尝试生成的二进制文件。大多数情况下,除非你在动态加载方面做了奇怪的事情,否则你会立即知道程序是工作还是崩溃。

1
依赖于动态glibc有什么问题? - Nick Hutchinson
我相信至少在一段时间内,libstdc++ 暗示了对 glibc 的依赖。不确定今天的情况如何。 - Alexander L. Belikoff
11
libstdc++ 依赖于 glibc(例如,iostreams 是基于 printf 实现的),但只要 Ubuntu 10.04 上的 glibc 提供了新版 libstdc++ 所需的所有功能,就没有使用动态 glibc 的问题。事实上,强烈建议不要静态链接到 glibc。 - Jonathan Wakely

1
我想对Jonathan Wakely的答案做出以下补充。
在Linux上使用 "-static-libstdc++" 进行实验时,我遇到了与 "dlclose()" 相关的问题。假设我们有一个应用程序 'A' 静态链接到 "libstdc++",并在运行时动态链接到 "libstdc++" 插件 'P',这很好。 但是当 'A' 卸载 'P' 时,会发生分段错误。我的假设是,在卸载 "libstdc++.so" 后,'A' 不再能够使用与 "libstdc++" 相关的符号。请注意,如果 'A' 和 'P' 都静态链接到 "libstdc++",或者如果 'A' 动态链接且 'P' 静态链接,则不会出现此问题。
总结:如果您的应用程序加载/卸载可能动态链接到 "libstdc++" 的插件,则该应用程序也必须以动态方式链接到它。这只是我的观察,我希望听到您的评论。

1
这可能类似于混合使用libc实现(例如动态链接到一个再动态链接glibc的插件,而应用程序本身静态链接到musl-libc)。musl-libc的作者Rich Felker声称,在这种情况下的问题在于glibc内存管理(使用sbrk)做出了某些假设,并且几乎期望在一个进程中独自存在...不确定是否仅限于特定的glibc版本或其他。 - 0xC0000022L
而且人们仍然没有看到Windows堆接口的优势,该接口能够处理单个进程中多个独立的libc++/libc副本。这样的人不应该设计软件。 - user12411795
@FrankPuck,作为一名拥有丰富的Windows和Linux经验的专业人士,我可以告诉你,“Windows”的做法在MSVC决定使用哪个分配器以及如何使用时并没有帮助。我认为Windows堆的主要优点是可以分发一些碎片,然后一次性释放它们。但是,在使用MSVC时,您仍然会遇到几乎与上述问题相同的问题,例如传递由另一个VC运行时分配的指针(发布版与调试版或静态链接与动态链接)。因此,“Windows”并不免疫。两个系统都需要注意。 - 0xC0000022L

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