其他分享
首页 > 其他分享> > c-我可以让共享库构造函数在重定位之前执行吗?

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

作者:互联网

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

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

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

与上面链接的答案中建议的方法保持一致,我正在执行以下操作:

>最终应用程序链接到libtest.so.
>我将libtest.so的DT_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环境变量的操作.看来问题归结为:

The dynamic linker performs relocations of global variables from shared libraries very early in the loading process, before constructors from shared libraries are called. Therefore, it tries to locate some global variable symbols before my dispatch library has had a chance to run its constructor and load the library that will actually provide those symbols.

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

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

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

解决方法:

Failure. If I remove all instances of the global variable bar and try to dispatch the foo() function only, then it all works.

这种方法没有全局变量的原因是函数(默认情况下)使用惰性绑定,而变量却不能(出于明显的原因).

如果您的测试程序与-Wl,-z,now链接(这将禁用功能的延迟绑定),那么您将获得完全相同的失败而没有任何全局变量.

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

与您的其他答案相反,这不是进行特定于CPU的调度的标准方法.

有两种标准方法.

较老的版本:将$PLATFORM用作DT_RPATH或DT_RUNPATH的一部分.内核将传入一个字符串,例如x86_64或i386或i686作为aux向量的一部分,而ld.so将用该字符串替换$PLATFORM.

这样,发行版就可以发布经过i386和i686优化的库,并且可以根据程序在哪个CPU上运行来选择合适的版本.

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

新的热点是IFUNC调度,记录为here.这是GLIBC当前用于提供不同版本的IFUNC的功能. memcpy取决于运行的CPU.还有target和target_clones属性(记录在同一页上),该属性使您可以编译例程的多个变体,这些变体针对不同的处理器进行了优化(以防您不想在汇编中对它们进行编码).

I’m trying to apply this functionality to an existing, very large library, so just a recompile is the most straightforward way of implementing it.

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

target_clones does look interesting; is that a recent addition to gcc

我相信IFUNC支持大约4-5年,在GCC中自动克隆大约2年.是的,最近.

标签:x86-64,shared-libraries,linux,c-4
来源: https://codeday.me/bug/20191027/1945935.html