【发布时间】: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.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 环境变量。看来问题归结为:
动态链接器在加载过程的早期,在调用来自共享库的构造函数之前,从共享库中重新定位全局变量。因此,它会在我的调度库有机会运行其构造函数并加载实际提供这些符号的库之前尝试定位一些全局变量符号。
这似乎是一个很大的障碍。有什么方法可以改变这个过程,以便我的调度程序可以首先运行?
我知道我可以使用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。我认为这意味着libtest和libtest_variant.so应该链接到libtest_dispatch.so以查看全局变量的定义(并且只将它们声明为extern自己)。
标签: c++ linux shared-libraries x86-64