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

背景 :我正在试图实现一个像这个在前面的答案中描述的系统。 总之,我有一个应用程序链接到共享库(目前在Linux上)。 我希望共享库在运行时在多个实现之间切换(例如,根据主机CPU是否支持某个指令集)。

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

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

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

结果:这工作! 如果我在每个共享库中放置一个名称相同的函数,我可以在运行时validation是否根据调度库使用的条件执行相应的版本。

问题:上面的作品是我在链接问题中用它展示的玩具的例子。 具体来说,如果库仅导出函数,似乎工作正常。 但是,一旦有variables(无论它们是C链接的全局variables还是像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(); } 

我使用以下方法构build库和testing应用程序:

 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,. 

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

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

失败。 如果我删除全局variablesbar所有实例,并尝试仅派发foo()函数,那么它都可以工作。 我试图弄清楚为什么以及是否可以在全局variables的存在下得到我想要的效果。

debugging:在试图诊断问题时,我已经在运行testing程序的同时玩了LD_DEBUG环境variables。 这似乎是问题归结为:

在调用共享库的构造函数之前,dynamic链接程序在加载过程的早期就会从共享库执行全局variables的重定位。 因此,它试图在我的调度库有机会运行其构造函数并加载实际提供这些符号的库之前find一些全局variables符号。

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

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

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

失败。 如果我删除全局变量栏的所有实例,并尝试仅派发foo()函数,那么它都可以工作。

这个工作没有全局变量的原因是函数(默认)使用惰性绑定,但变量不能(由于显而易见的原因)。

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

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

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

有两种标准的方法。

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

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

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

新的IFUNCIFUNC派遣,记录在这里 。 这就是GLIBC目前用来提供不同版本的memcpy取决于它运行在哪个CPU上。 还有targettarget_clones属性(记录在同一页面上),允许您编译例程的多个变体,针对不同的处理器进行优化(如果您不想在汇编中对其进行编码)。

我试图将这个功能应用到现有的,非常大的库,所以只需重新编译是最直接的实现方法。

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

target_clones看起来很有趣; 是最近添加到海湾合作委员会

我相信IFUNC支持大概是4-5岁,GCC的自动克隆大概是2岁左右。 所以是的,很近。

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

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