【问题标题】:Shared library symbol conflicts and static linking (on Linux)共享库符号冲突和静态链接(在 Linux 上)
【发布时间】:2020-11-03 04:27:07
【问题描述】:

我遇到了一篇好文章 Shared Library Symbol Conflicts (on Linux) 中详细阐述的问题。问题是,当执行和.so定义了同名函数时,如果.so调用这个函数名,它会在执行中调用那个函数而不是.so本身中的这个。

让我们来谈谈这篇文章中的案例。我了解layer.o 中的DoLayer() 函数在编译layer.o 时具有DoThing() 的外部函数依赖关系。

但是在编译libconflict.so的时候,不应该就地解析外部函数依赖,直接用conflict.o/DoThing()的地址静态替换吗?

为什么layer.o/DoLayer() 仍然使用动态链接来查找DoThing()?这是一种设计行为吗?

【问题讨论】:

    标签: linux gcc ld elf dynamic-linking


    【解决方案1】:

    这是一种设计行为吗?

    是的。

    在 UNIX 上引入共享库时,目标是假装它们就像代码在常规(归档)库中一样工作。

    假设您在libfoo 和libbar 中都定义了foo(),并且libbar 中的bar() 调用foo()。

    设计目标是无论libfoo 和libbar 是存档库还是共享库,cc main.c -lfoo -lbar 的工作方式都相同。实现此目的的唯一方法是让libbar.so 使用动态链接 来解析从bar() 到foo() 的调用,尽管有foo() 的本地版本。

    这种设计使得创建一个自包含的libbar.so 成为不可能——它的行为(它最终调用哪些函数)取决于链接到进程中的其他函数。这也与 Windows DLL 的工作方式相反。

    当时并没有考虑创建独立的 DSO,因为 UNIX 实际上是开源的。

    您可以使用特殊的链接器标志更改规则,例如-Bsymbolic。但是规则很快就会变得复杂,并且(因为这不是默认设置)您可能会在链接器或运行时加载器中遇到错误。

    【讨论】:

    • 谢谢。虽然我有几个问题......但第一个问题,我不完全理解你的情况。根据Why does the order in which libraries are linked sometimes cause errors in GCC?,对于链接顺序-lfoo -lbar,如果libfoo 和libbar 是静态库,我认为libbar 会在libbar 本身中调用foo(),因为链接器从左到右搜索对,并在此过程中记下未解析的符号。 在此链接中说?
    • @xnervwang 你是对的:最终调用哪个foo() 取决于(在归档库的情况下)main.cc 是否引用foo(),以及所使用的链接器( LLD 不实现传统的 UNIX 链接规则)。
    • 好的,我现在明白你的假设了。在你的情况下,假设mani.cc也调用foo(),所以当cc main.c -lfoo -lbar(假设libfoo.a和libbar.a),因为-lfoo首先出现,所以-lfoo中的foo()被@使用987654354@ 和libbar.a 将在-lfoo 中使用foo()。然后考虑另一种情况,如果它们是libfoo.so 和libbar.so,如果我们需要保持保存行为,那么libbar.so 也应该在libfoo.so 中调用foo() 而不是它本身。所以这就是它被设计成这样的原因。
    • 嗨@EmployedRussian,正如您提到的This is also the opposite of how Windows DLLs work,您是否知道如何使Windows 表现相同?我正在将一个 Linux 项目移植到 Windows,这种行为让我抓狂。
    【解决方案2】:

    是的,这是一种设计行为。当您将程序链接到二进制文件时,所有对命名外部(非静态)函数的引用都被解析为指向二进制文件的符号表。任何链接的共享库都被指定为DT_NEEDED 条目。

    然后,当您运行二进制文件时,动态链接器会将每个所需的共享库加载到合适的地址,并将每个符号解析为一个地址。有时这是懒惰地完成,有时是在第一次启动时完成一次。如果有多个同名符号,链接器将选择其中一个符号,并且您的程序可能会崩溃,因为您最终可能没有得到正确的符号。

    请注意,这是 Linux 上的行为,它将所有符号都作为平面命名空间。 Windows 以不同的方式解析符号,使用树形拓扑,它既有优点(冲突较少)也有缺点(无法在一个库中分配内存并在另一个库中释放它)。

    如果您想让LD_PRELOAD 之类的东西正常工作,Linux 行为非常重要。这允许您使用诸如 Electric Fence 之类的调试工具和诸如 Google 性能工具之类的 CPU 分析工具,或者在运行时替换内存分配器。如果符号被优先解析为它们的二进制或共享库,那么这些都不会起作用。

    但是,GNU 动态链接器确实支持符号版本,因此可以将共享库的多个版本加载到同一个程序中。通常,像 Debian 这样的发行版会使用他们希望经常更改的库来执行此操作,例如 OpenSSL。如果程序使用使用 OpenSSL 1.0 的 liba 和使用 OpenSSL 1.1 的 libb,那么程序在这种情况下应该仍然可以运行,因为 OpenSSL 具有版本化符号,并且每个库都将使用相关符号的适当版本。

    【讨论】:

    • 感谢您的回答。但我想在这里了解更多细节。就我粘贴的文章而言,libconflict.so 在 layer.o 中实现了 DoLayer(),在 conflict.o 中实现了 DoThing()。然后 DoLayer() 调用 DoThing()。编译完libconflict.so后,我认为DoLayer()可以调用libconflict.so本身定义的DoThing()。为什么它使用另一个在执行中实现的 DoThing() 而不是在 libconflict.so 本身中实现的这个 DoThing()?看起来像在 .so 动态库中,如果它是从多个 .o 文件编译的,那么从一个 .o 到另一个的每次调用都将依赖于动态链接。
    • 那是允许符号插入,这就是 bk2204 与LD_PRELOAD 所指的内容(预加载是插入的多种用途之一),它允许根据加载的模块替换函数。您可以强制链接编辑器在构建时解析所有定义,使用-Wl,-Bsymbolic — 请参阅flameeyes.blog/2012/10/07/… 了解更多详细信息。
    • 你大错特错了:将 Open SSL 1.0 和 1.1 加载到同一个进程中不起作用,并且预计不会起作用。
    • 如果您设置了符号版本控制,它确实可以工作,Debian 就是这样做的。如果两个版本都没有符号版本控制,它将完全被破坏。仅仅因为您的发行版没有,并不意味着它不起作用。
    • @DiegoElioPettenò 谢谢!这是一个伟大的博客。它几乎回答了我所有的问题。现在我明白我的问题是因为 .so 动态库是 PIC,所以它在我的情况下不在 GOT 中时使用 PLT 来搜索功能。而且我也知道我们可以使用 GCC 可见性或其他 GCC/链接器选项来解决这个问题,比如 visibility=hidden 可以强制一个函数在 GOT 中,然后当 .so 需要找到这个函数时,它可以在 GOT 中找到它首先而不是从动态链接器中询问函数地址。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-08
    相关资源
    最近更新 更多