共享库符号冲突和静态链接(Linux)
Shared library symbol conflicts and static linking (on Linux)
我遇到了一篇很好的文章 Shared Library Symbol Conflicts (on Linux) 中详细阐述的问题。问题是当执行和.so定义了同名函数时,如果.so调用这个函数名,它会调用执行中的那个函数而不是.so本身中的这个。
本文来说说案例。我了解 layer.o
中的 DoLayer()
函数在编译 layer.o
.
时具有 DoThing()
的外部函数依赖性
但是在编译libconflict.so
时,不应该就地解析外部函数依赖并静态替换为conflict.o/DoThing()
的地址吗?
为什么 layer.o/DoLayer()
仍然使用动态链接来查找 DoThing()
?这是设计好的行为吗?
是的,这是设计好的行为。当您将程序 link 转换为二进制文件时,所有对命名外部(非静态)函数的引用都将解析为指向二进制文件的符号 table。 link 编辑的任何共享库都指定为 DT_NEEDED
个条目。
然后,当您 运行 二进制文件时,动态 linker 将每个所需的共享库加载到一个 suitable 地址并将每个符号解析为一个地址。有时这是懒惰地完成的,有时是在第一次启动时完成一次。如果有多个名称相同的符号,linker 将选择其中一个,您的程序可能会崩溃,因为您最终可能不会找到正确的符号。
请注意,这是 Linux 上的行为,它将所有符号作为平面命名空间。 Windows 使用树形拓扑以不同方式解析符号,它既有优点(冲突更少)也有缺点(无法在一个库中分配内存并在另一个库中释放它)。
如果您希望 LD_PRELOAD
之类的东西起作用,Linux 行为非常重要。这允许您使用像 Electric Fence 这样的调试工具和像 Google 性能工具这样的 CPU 分析工具,或者在 运行 时替换内存分配器。 None 如果符号优先解析为它们的二进制或共享库,这些东西将起作用。
GNU dynamic linker 确实支持符号版本,因此可以将多个版本的共享库加载到同一个程序中。通常,像 Debian 这样的发行版会使用他们希望经常更改的库来执行此操作,例如 OpenSSL。如果程序使用使用 OpenSSL 1.0 的 liba 和使用 OpenSSL 1.1 的 libb,那么程序在这种情况下应该仍然可以运行,因为 OpenSSL 具有版本化符号,并且每个库将使用相关符号的适当版本。
Is this a designed behavior?
是的。
在 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
。但是规则很快就会变得复杂,并且(因为这不是默认设置)您可能会在链接器或运行时加载器中遇到错误。
我遇到了一篇很好的文章 Shared Library Symbol Conflicts (on Linux) 中详细阐述的问题。问题是当执行和.so定义了同名函数时,如果.so调用这个函数名,它会调用执行中的那个函数而不是.so本身中的这个。
本文来说说案例。我了解 layer.o
中的 DoLayer()
函数在编译 layer.o
.
DoThing()
的外部函数依赖性
但是在编译libconflict.so
时,不应该就地解析外部函数依赖并静态替换为conflict.o/DoThing()
的地址吗?
为什么 layer.o/DoLayer()
仍然使用动态链接来查找 DoThing()
?这是设计好的行为吗?
是的,这是设计好的行为。当您将程序 link 转换为二进制文件时,所有对命名外部(非静态)函数的引用都将解析为指向二进制文件的符号 table。 link 编辑的任何共享库都指定为 DT_NEEDED
个条目。
然后,当您 运行 二进制文件时,动态 linker 将每个所需的共享库加载到一个 suitable 地址并将每个符号解析为一个地址。有时这是懒惰地完成的,有时是在第一次启动时完成一次。如果有多个名称相同的符号,linker 将选择其中一个,您的程序可能会崩溃,因为您最终可能不会找到正确的符号。
请注意,这是 Linux 上的行为,它将所有符号作为平面命名空间。 Windows 使用树形拓扑以不同方式解析符号,它既有优点(冲突更少)也有缺点(无法在一个库中分配内存并在另一个库中释放它)。
如果您希望 LD_PRELOAD
之类的东西起作用,Linux 行为非常重要。这允许您使用像 Electric Fence 这样的调试工具和像 Google 性能工具这样的 CPU 分析工具,或者在 运行 时替换内存分配器。 None 如果符号优先解析为它们的二进制或共享库,这些东西将起作用。
GNU dynamic linker 确实支持符号版本,因此可以将多个版本的共享库加载到同一个程序中。通常,像 Debian 这样的发行版会使用他们希望经常更改的库来执行此操作,例如 OpenSSL。如果程序使用使用 OpenSSL 1.0 的 liba 和使用 OpenSSL 1.1 的 libb,那么程序在这种情况下应该仍然可以运行,因为 OpenSSL 具有版本化符号,并且每个库将使用相关符号的适当版本。
Is this a designed behavior?
是的。
在 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
。但是规则很快就会变得复杂,并且(因为这不是默认设置)您可能会在链接器或运行时加载器中遇到错误。