根据用户配置在运行时链接共享对象

Posted

技术标签:

【中文标题】根据用户配置在运行时链接共享对象【英文标题】:Linking shared objects at runtime depending on user configuration 【发布时间】:2017-11-27 09:57:37 【问题描述】:

TL;DR

我有一个我想在我的程序中使用的库,它有两个不同的版本。两个版本都提供相同的接口,但用于编译它们的选项不同。

我现在想使用特定版本的库,但是,因为两个版本都适用于不同的任务,并且用户应该定义要执行的任务,所以我需要决定在运行时使用哪个库。

我知道我可以根据用户的选择使用dlopendlsym 在运行时加载正确的库,但是接口非常大,将我需要的所有内容加载到不同的函数指针中会非常乏味.. .

问题

我有一个库,它有两个不同的版本。两个版本都提供相同的界面,但它们适用的任务不同。 这是文件树的样子:

lib
 \ - lib_task1
      \ - libsharedobj.so
 \ - lib_task2
      \ - libsharedobj.so

我想为用户提供选择在运行时执行哪个任务的可能性。因此,我还需要决定在运行时选择哪个库。 我的想法是编写一个包装器,它提供与库相同的接口,并在其中将 dlopen 所需的库和 dlsymthe 相应的符号放入函数指针中。但是,库接口非常大,按照描述包装它会很乏味,而且它是一个 C 接口,所以它还包含很多我不想在包装器之外看到的原始指针。

这是一个小例子

// library interface
typedef struct 
  // ...
 a_type;
void do_something(a_type* param);

// wrapper 
class LibWrapper 
private:
    void (*do_something)(a_type*);

    void* lib;
public:
    LibWrapper(const bool task_one)  // specified by the user
        if (task_one) 
            lib = dlopen("/usr/lib/lib_task1/libsharedobj.so", RTLD_NOW);
         else 
            lib = dlopen("/usr/lib/lib_task2/libsharedobj.so", RTLD_NOW);
        
        do_something = dlsym(lib, "do_something");
    

    ~LibWrapper() 
        if (lib) 
            dlclose(lib);
        
    

    void do_something(std::unique_ptr<a_type> param) 
        do_something(param.get());
    
;

问题

有没有更好的方法来做到这一点,还是我真的需要一个一个地加载每个符号?

操作系统:ubuntu 14.04。 兼容性:C++11

【问题讨论】:

主程序是否需要多次决定,和/或在启动之后,还是在启动期间知道需要哪个库实现,并且可以坚持到退出? 每次运行只加载一次,直到程序重新启动后才能更改决定。 您可以按照一些答案的建议更改库标题吗? 【参考方案1】:

混淆 LD_PRELOAD 设置的一种方法可能是,当您的程序启动时,如果 LD_PRELOAD 设置不合适,请确定您想要的设置,添加它,然后立即使用您自己的参数执行 execv 以重新启动! 我认为 LD_PRELOAD 在这里更有意义,如果它满足你的需要,因为它更容易检测它是否设置为你需要的方式。

【讨论】:

这是一个流行的解决方案,但请注意,当您可以重新启动应用程序时,它可能已经完成了一些工作或从其他构造函数调用了有问题的函数(无法确保您的构造函数首先运行)。对于 setuid,LD_PRELOAD 也会被忽略。【参考方案2】:

您可以通过在启动程序之前设置环境变量LD_LIBRARY_PATH 来解决这个问题。如有必要,编写一个启动程序来执行此操作:

LD_LIBRARY_PATH=lib/lib_task1 ./myprog # or lib_task2

然后ld.so 将首先在指定目录中查找libsharedobj.so,而不管链接的是哪个(即ldd myprog 显示的是哪个)。

【讨论】:

两个问题:你说它首先使用这个目录,所以它会在标准路径中搜索,对吗?第二:有没有办法在不编写外部包装器(即启动程序)的情况下做到这一点,我不能在调用任何库函数之前从我的程序内部更改变量还是太晚了? 一个类似的技巧是使用 LD_PRELOAD 来指定要加载“first”的库 @muXXmit2X:是的,它仍然会在LD_LIBRARY_PATH 之后的标准路径中查找。不,你不能在你的程序中设置变量——那就太晚了。 我不确定这是否回答了 OP 的问题 - 他想在运行时选择库版本,而不是在程序启动之前。【参考方案3】:

您可以考虑使用 objcopy 重命名库中的公共符号吗?

[根据要求提供更多详细信息]

如果将两个库中的所有竞争函数重命名为非竞争命名空间,则可以同时加载它们,并在运行时选择所需的符号。

如果您可以使唯一名称看起来像真正的 c++ 命名空间,那么您应该能够通过在命名空间定义中#include 来重用现有的头文件。

这里有一些全局和命名空间重命名:

             U _Z9MyTestFn1Pv
             U _Z9MyTestFn2Pv
             U _ZN2N19MyTestFn1EPv
             U _ZN2N19MyTestFn2EPv
             U _ZN2N29MyTestFn1EPv
             U _ZN2N29MyTestFn2EPv

你可以在这里解开它们:https://demangler.com/

使用objcopy --redefine-sym old=newobjcopy --redefine-syms=filename 进行重命名。可以使用 nm 和 sed 生成重命名。

通过一些额外的巧妙宏工作,您甚至可以让它编写一个 c 风格的函数表原型。不过,您仍然需要填充该表,但是通过拥有真正的原型,您将不太可能遭受 dlsym 很容易出现的胖手指错误。

在您的标题中读取类似以下内容以允许指针声明:

int (PTR_MAYBE FirstExternalFn) ( int firstArg,  ...  ) ;
std::stringint (PTR_MAYBE SecondExternalFn) ( bool firstArg,  ...  ) ;

或者这可能太多了,但也允许自动填充跳转表:

RETURNS(int) (PTR_MAYBE FirstExternalFn) ARGUMENTS ( int firstArg,  ...  ) ENDLINE
RETURNS(std::stringint) (PTR_MAYBE SecondExternalFn) ARGUMENTS ( bool firstArg,  ...  ) ENDLINE

第三种方法,旨在帮助填充 shim 虚拟 c++ 类,要求您将每个参数装饰为 OneWordType (name) 形式,这对于模板来说相对容易,即 const char* name 变为 DecorateArg&lt;char&gt;::const_ptr (name)。然后,这允许您调用声明!当然,使用虚拟类的代价是代码携带了一个您永远不会使用的 this 指针,并且您正在进行额外的 shim 调用。

请注意,此文件被多次包含,并且没有包含保护,但它不再真正用于直接使用。如果未定义父包含,您可能会出现#error。

您可以通过多种方式使用它:

//Declare the external methods
#define PTR_MAYBE 
#define ARGUMENTS 
#define ENDLINE ;
namespace FirstLib 
    #include Header.hpp

// And the second library
namespace SecondLib 
    #include Header.hpp


//define the indirection table declaration
#define PTR_MAYBE *
struct Indirection

    #include Header.hpp
    void* dummy;
;
namespace FirstLib 

    extern Indirection indirection;

namespace SecondLib 

    extern Indirection indirection;


// populate the table
#define ARGUMENTS(...) 
#define ENDLINE ,
namespace FirstLib 

    Indirection indirection =
      
        #include Header.hpp
        nullptr
    ;

/* looks like:
         FirstExternalFn,
         SecondExternalFn,
         nullptr
*/
namespace SecondLib 

    Indirection indirection =
      
        #include Header.hpp
        nullptr
    ;

这有点令人讨厌的宏骇客,但如果您愿意自己构建表,则不需要 ARGUMENTS 或 ENDLINE,这样会更理智。

我们现在可以访问 2 个间接表,FirstLib::indirection,SecondLib::indirection,您可以在其中或多或少地直接从主代码调用库方法,方法是分配指向您最喜欢的指针:

std::cout << currentIndirection->FirstExternalFn(nullptr);

【讨论】:

但是你需要运行时调度每个符号。 嗯,是的,您仍然需要进行某种运行时重定向,但它没有 dlsym() 可怕。如果你偷偷摸摸,你可以让你重命名的符号看起来像是在一个命名空间中,那么你可以#include 现有的标题到同一个命名空间中。 您能否澄清一下这如何允许 OP 在 运行时 选择函数实现?【参考方案4】:

这在精神上与Is there an elegant way to avoid dlsym when using dlopen in C? 非常相似。基本上,您正在寻找一个等效的 Windows 导入库,它会提供存根符号以满足静态链接器,然后是 dlopen 动态库,在运行时具有实际实现。

Linux 开箱即用不支持导入库,因此人们通常手动或通过为特定项目定制的脚本(例如GLEW)来实现存根。我最近开发了Implib.so 来自动生成与 POSIX 兼容的导入库。模数错误,你应该可以用它作为

$ implib-gen.py --dlopen-callback=mycallback

mycallback 选择适当版本的库并dlopens)。

【讨论】:

以上是关于根据用户配置在运行时链接共享对象的主要内容,如果未能解决你的问题,请参考以下文章

Samba文件共享

Linux访问Window共享文件夹的配置步骤

CDH中yarn的动态资源池的相关配置

Linux环境变量及其配置实战

虚拟用户启用vsftp的文件共享

配置数据库时,出现对象关闭,无法操作。