我可以让共享库构造函数在重定位之前执行吗?

Posted

技术标签:

【中文标题】我可以让共享库构造函数在重定位之前执行吗?【英文标题】:Can I make shared library constructors execute before relocations? 【发布时间】:2015-12-01 18:28:07 【问题描述】:

背景:我正在尝试实现一个系统like that described in this previous answer。简而言之,我有一个链接到共享库的应用程序(目前在 Linux 上)。我希望该共享库在运行时在多个实现之间切换(例如,基于主机 CPU 是否支持某个指令集)。

在最简单的情况下,我有三个不同的共享库文件:

libtest.so:这是库的“香草”版本,将用作后备案例。 libtest_variant.so:这是库的“优化”变体,如果 CPU 支持,我想在运行时选择它。它与libtest.so 兼容。 libtest_dispatch.so:这个库负责选择在运行时使用库的哪个变体。

按照上面链接答案中建议的方法,我正在执行以下操作:

最终申请链接到libtest.so。 我将libtest.soDT_SONAME 字段设置为libtest_dispatch.so。因此,当我运行应用程序时,它将加载 libtest_dispatch.so 而不是实际的依赖关系 libtest.so

libtest_dispatch.so 被配置为具有如下所示的构造函数(伪代码):

__attribute__((constructor)) void init()

    if (can_use_variant) dlopen("libtest_variant" SHLIB_EXT, RTLD_NOW | RTLD_GLOBAL);
    else dlopen("libtest" SHLIB_EXT, RTLD_NOW | RTLD_GLOBAL);

dlopen() 的调用将加载提供适当实现的共享库,然后应用程序继续运行。

结果:这行得通!如果我在每个共享库中放置一个同名函数,我可以在运行时验证是否根据调度库使用的条件执行了适当的版本。

问题:以上适用于我在链接问题中演示的玩具示例。具体来说,如果库只导出函数,它似乎工作正常。但是,一旦有变量在起作用(无论它们是具有 C 链接的全局变量还是像 typeinfo 这样的 C++ 结构),我会在运行时遇到未解决的符号错误。

下面的代码演示了这个问题:

libtest.h

extern int bar;

int foo();

libtest.cc

#include <iostream>

int bar = 2;

int foo()

    std::cout << "function call came from libtest" << std::endl;
    return 0;

libtest_variant.cc

#include <iostream>

int bar = 1;

int foo()

    std::cout << "function call came from libtest_variant" << std::endl;
    return 0;

libtest_dispatch.cc

#include <dlfcn.h>
#include <iostream>
#include <stdlib.h>

__attribute__((constructor)) void init()

    if (getenv("USE_VARIANT")) dlopen("libtest_variant" SHLIB_EXT, RTLD_NOW | RTLD_GLOBAL);
    else dlopen("libtest" SHLIB_EXT, RTLD_NOW | RTLD_GLOBAL);

test.cc

#include "lib.h"
#include <iostream>

int main()

    std::cout << "bar: " << bar << std::endl;
    foo();

我使用以下代码构建库和测试应用程序:

g++ -fPIC -shared -o libtest.so libtest.cc -Wl,-soname,libtest_dispatch.so
g++ -fPIC -shared -o libtest_variant.so libtest_variant
g++ -fPIC -shared -o libtest_dispatch.so libtest_dispatch.cc -ldl
g++ test.cc -o test -L. -ltest -Wl,-rpath,.

然后,我尝试使用以下命令行运行测试:

> ./test
./test: symbol lookup error: ./test: undefined symbol: bar
> USE_VARIANT=1 ./test
./test: symbol lookup error: ./test: undefined symbol: bar

失败。如果我删除全局变量 bar 的所有实例并尝试仅调度 foo() 函数,那么一切正常。我正试图弄清楚为什么以及是否可以在存在全局变量的情况下获得我想要的效果。

调试:在尝试诊断问题时,我在运行测试程序时使用了LD_DEBUG 环境变量。看来问题归结为:

动态链接器在加载过程的早期,在调用来自共享库的构造函数之前,从共享库中重新定位全局变量。因此,它会在我的调度库有机会运行其构造函数并加载实际提供这些符号的库之前尝试定位一些全局变量符号。

这似乎是一个很大的障碍。有什么方法可以改变这个过程,以便我的调度程序可以首先运行?

我知道我可以使用LD_PRELOAD 预加载库。但是,这对我的软件最终运行的环境来说是一个繁琐的要求。如果可能的话,我想找到一个不同的解决方案。

经过进一步审查,似乎即使我LD_PRELOAD 图书馆,我也有同样的问题。在全局变量符号解析发生之前,构造函数仍然没有被执行。使用预加载功能只是将所需的库推到库列表的顶部。

【问题讨论】:

使用 fPIC 编译的代码根本不受任何重定位的影响。相反,它使用全局偏移表和过程链接表来访问符号。您的分析不正确。 @SergeyA:我对我错了并不感到惊讶。我的猜测来自LD_DEBUG 输出打印像relocation processing: /lib/x86_64-linux-gnu/libc.so.6 (lazy) 这样的行,之后发生符号绑定错误(它遇到绑定全局变量符号的问题)。此重定位处理发生在任何calilng init 行出现之前;它甚至没有达到调用构造函数的地步。 您可能需要搜索 STT_GNU_IFUNC 扩展,以便在加载时在符号(而不是整个库)之间进行选择。 你能把全局变量放入libtest_dispatch.so吗?任何外部可见的全局变量必须在库的两个版本之间具有相同的 ABI,因此可以将它们分解到正常链接的调度库中,而不是使用 dlopen。我认为这意味着 libtestlibtest_variant.so 应该链接到 libtest_dispatch.so 以查看全局变量的定义(并且只将它们声明为 extern 自己)。 【参考方案1】:

失败。如果我删除全局变量 bar 的所有实例并尝试仅调度 foo() 函数,那么一切正常。

这在没有全局变量的情况下工作的原因是函数(默认情况下)使用惰性绑定,但变量不能(原因很明显)。

如果您的测试程序与-Wl,-z,now 链接(这将禁用函数的延迟绑定),那么您将在没有任何全局变量的情况下得到完全相同的失败。

您可以通过将主程序引用的每个全局变量的实例引入调度库来解决此问题。

与您的其他答案所暗示的相反,这不是执行特定于 CPU 的调度的标准方法。

有两种标准方式。

较旧的:使用$PLATFORM 作为DT_RPATHDT_RUNPATH 的一部分。内核将传入一个字符串,例如x86_64,或i386,或i686,作为aux 向量的一部分,ld.so 将用该字符串替换$PLATFORM

这允许发行版同时发布 i386i686 优化的库,并让程序根据运行的 CPU 选择合适的版本。

不用说,这不是很灵活,并且(据我所知)不允许您区分各种 x86_64 变体。

新的热点是IFUNC 调度,记录在here。这是 GLIBC 目前用来提供不同版本的例如memcpy 取决于它在哪个 CPU 上运行。还有targettarget_clones 属性(记录在同一页上)允许您编译例程的多个变体,针对不同的处理器进行优化(以防您不想在汇编中编写它们)。

我正在尝试将此功能应用于现有的非常大的库,因此只需重新编译是实现它的最直接方式。

在这种情况下,您可能必须将二进制文件包装在 shell 脚本中,并根据 CPU 将LD_LIBRARY_PATH 设置为不同的目录。或者在运行程序之前让用户source 你的脚本。

target_clones 看起来确实很有趣;这是最近添加到 gcc 的吗?

我相信IFUNC 支持大约有 4-5 年的历史,GCC 中的自动克隆大约有 2 年的历史。所以是的,最近。

【讨论】:

再次感谢您的洞察力。我可能在说明我的目标时不够清楚,所以选项 #1 ($PLATFORM) 不起作用。例如,我希望根据 CPU 是否仅支持 SSE2、SSE4.2、AVX 等进行调度。我希望我可以为每个配置重新编译我的库,然后引入一个可以加载的存根运行时的正确版本。我开始意识到这可能是不可能的。到目前为止,我发现的最佳解决方案是使用 LD_AUDIT 将调度程序注册为审计库,但这对用户来说不是很透明。 您不需要(通常)重新编译整个库,只需要重新编译其中的热门函数。 target_clones 会以最小的努力为您做到这一点。 确实,这看起来确实可以完成我想要的。我正在尝试将此功能应用于现有的非常大的库,因此重新编译是实现它的最直接方式。 target_clones 看起来很有趣;这是gcc 的最新成员吗? 据我了解,target_clones 属性不允许您提供代码,即您相信编译器会自动矢量化。我想要的是一种提供与多个编译器一起使用的函数的多种实现的方法(针对 SSE/AVX/AVX2/AVX512 仔细调整)。这就是动态 dlopen 技巧吸引人的原因。 @fsaintjacques "target_clones 不允许 ..." -- 正确。但是IFUNC 确实允许您在手动编码的实现之间进行选择。【参考方案2】:

它本身可能不是重定位(-fPIC 抑制重定位),而是通过 GOT(全局偏移表)进行的惰性绑定,具有相同的效果。这是不可避免的,因为链接器必须在调用 init 之前绑定变量 - 仅仅是因为 init 也可能引用这些符号。

解决方案广告...好吧,一旦解决方案可能是使用(甚至公开)全局变量给可执行代码。相反,提供一组函数来访问它们。无论如何都不欢迎全局变量:)

【讨论】:

感谢您的洞察力。我同意全局变量不是一件好事。但是,除了我的玩具示例之外,我还看到与 typeinfo 之类的 C++ 类相同的符号解析错误,据我所知,这是不可避免的。正如我所怀疑的,这里可能没有好的解决方案。 相同的区别。为您的库提供一个纯 C 接口,并在您的代码中隐藏所有 C++ 内容。无论如何,这是一件明智的事情 - 否则您将不得不为您的客户可能使用的每个编译器/标准库实现重新编译您的 .so。 感谢您的建议。有问题的库本质上是一个 C++ 库,所以用 C 包装它是行不通的。

以上是关于我可以让共享库构造函数在重定位之前执行吗?的主要内容,如果未能解决你的问题,请参考以下文章

我可以从 Main 构造函数中关闭程序吗?

是否可以定义指向构造函数的函数指针?

GCC __attribute__((constructor)) 在对象构造函数之前调用

基类构造函数真的在派生类构造函数之前调用吗

我可以从 C# 中同一类的另一个构造函数调用重载构造函数吗?

java构造代码块和构造函数内的代码块有啥区别,谁先执行