如何在Linux上实现共享库的延迟加载

13

我一直在寻找一种方法使一个共享库(我们将库命名为 libbar.so )在Linux上被延迟加载,并希望只有链接器的帮助就可以实现,而不需要修改用C++编写的源代码; 我的意思是,我不想在父库(命名为libfoo.so)的源代码中调用dlopen()dlsym()以调用libbar.so的函数,因为它们会使源代码混乱且维护过程困难。 (简而言之,我希望在Linux上采用类似于Visual Studio的/DELAYLOAD选项的方式)

无论如何,到目前为止,我在互联网上找到了一些与我的问题相关的不确定信息,因此很高兴能从以下问题的答案中获得清晰的信息。

  1. GNU ld是否支持Linux上的任何延迟加载机制?
  2. 如果不支持,那么Clang呢?
  3. dlopen()系列是在Linux上使共享库延迟加载的唯一方法吗?

我尝试使用路径将-zlazy标志传递给GCC(g ++),它似乎接受了该标志,但行为看起来并没有使libbar.so延迟加载(不具备libbar.so ,我期望在第一次调用libbar.so时有一个异常,但实际上在进入libfoo.so之前就引发了异常)。另一方面,Clang(clang ++)留下了一个警告,说它忽略了选项标志。

此致,


clang使用GNU ld进行链接(至少在3.4及更早版本中),因此您在那里找不到任何帮助。 - Mats Petersson
之前被问到过:https://dev59.com/XE7Sa4cB1Zd3GeqP1Sxv - oakad
1
@oakad,谢谢 :) 我也找到了那篇帖子。所以你认为GNU ld真的不支持任何延迟加载机制,我们必须将dlopen()放入我们的源代码中吗?(哦,即使这是真的,我也不想接受这个事实...) - Doofah
1
根据我对ld手册的阅读,-zlazy(应该是-z lazy)会使您链接的库(在您的情况下为libfoo.so)延迟加载其调用者。对于您想要的内容,必须在构建libbar.so时应用此标志。但是手册还说“惰性绑定是默认的”,因此libbar.so应该已经链接了这个标志。 - msc
1
我之前的评论是错误的。我误解了手册。-z lazy 是用于符号的惰性绑定,而不是库的惰性加载。 - msc
显示剩余3条评论
3个回答

13
延迟加载不是运行时特性。MSVC++在没有Windows的帮助下实现了它。就像在Linux上只有dlopen一样,在Windows上只有GetProcAddress这种运行时方法。
那么,延迟加载是什么?很简单:对DLL的任何调用都必须通过指针进行(因为你不知道它将在哪里加载)。这一直由编译器和连接器为您处理。但是使用延迟加载,MSVC++最初将此指针设置为调用LoadLibrary和GetProcAddress的存根。
Clang可以在没有ld的帮助下执行相同的操作。在运行时,它只是一个普通的dlopen调用,而Linux无法确定Clang插入了它。

@ MSalters,再次感谢。您的意思是Clang可以自己处理延迟加载吗?实际上,我希望能够隐式地通过向编译器或链接器传递选项标志来获得像MSVC ++提供给我们的辅助函数(尽管我忘记了辅助函数的名称),因为该机制将所有内容隐藏在延迟加载的函数调用后面(因此用户不必关心这样的辅助函数是否在我们的源代码下被调用)。 - Doofah
是的,**__delayLoadHelper2** 是辅助函数! 可能一些编译器或链接器(例如 Sun 的某些东西或 Apple 的 ld64)必须提供这样的辅助函数,但我不知道在 Linux 上发生了什么。(而我对 Linux 是非常陌生的 ;) - Doofah
@user3591878:Linux上的GCC有一个不幸的习惯,混淆了操作系统和编译器之间的区别,但这是UNIX的传统(请参见ldld.so)。你是新手对Linux并不重要,因为Linux本身并不重要。重要的是GCC和CLang。它们可以在任何具有dlopen的操作系统上实现延迟加载。但这并不意味着它们已经这样做了。 - MSalters
@MSalters,我明白你的意思,非常感谢你让问题更加清晰 :) 无论如何,我得到的结果似乎表明调用dlopen()是这种情况下唯一的选择... - Doofah
@user3591878:不好意思,没有。 - MSalters
2
@MSalters "并不意味着他们实际上已经这样做了" - 没有内置的解决方案,但可以通过一些小的努力来完成,参见Implib.so - yugr

6

使用代理设计模式可以以便携的方式实现此功能。

在代码中,它可能是这样的:

#include <memory>

// SharedLibraryProxy.h
struct SharedLibraryProxy
{
    virtual ~SharedLibraryProxy() = 0;

    // Shared library interface begin.
    virtual void foo() = 0;
    virtual void bar() = 0;
    // Shared library interface end.

    static std::unique_ptr<SharedLibraryProxy> create();
};

// SharedLibraryProxy.cc
struct SharedLibraryProxyImp : SharedLibraryProxy
{
    void* shared_lib_ = nullptr;
    void (*foo_)() = nullptr;
    void (*bar_)() = nullptr;

    SharedLibraryProxyImp& load() {
        // Platform-specific bit to load the shared library at run-time.
        if(!shared_lib_) { 
            // shared_lib_ = dlopen(...);
            // foo_ = dlsym(...)
            // bar_ = dlsym(...)
        }
        return *this;
    }

    void foo() override {
        return this->load().foo_();
    }

    void bar() override {
        return this->load().bar_();
    }
};

SharedLibraryProxy::~SharedLibraryProxy() {}

std::unique_ptr<SharedLibraryProxy> SharedLibraryProxy::create() {
    return std::unique_ptr<SharedLibraryProxy>{new SharedLibraryProxyImp};
}

// main.cc
int main() {
    auto shared_lib = SharedLibraryProxy::create();
    shared_lib->foo();
    shared_lib->bar();
}

@ Maxim,感谢您留下实用的伪代码。这个设计模式看起来非常有帮助,可以桥接两个库 :) - Doofah
这仅适用于函数。除非在运行时链接器中实现,否则变量无法实现。问题仍然是是否可能。 - Lothar
@Lothar 你是怎么得出这个结论的?这种机制适用于函数和对象。 - Maxim Egorushkin

2
为了补充MSalters的回答,我们可以通过创建一个小的静态存根库来模仿Windows在Linux上的懒加载方法。该库在第一次调用其任何函数时会尝试dlopen所需的库(如果dlopen失败,则发出诊断消息并终止),然后将所有调用转发给它。
这样的存根库可以手动编写,由项目/库特定脚本生成或由通用工具Implib.so生成:
$ implib-gen.py libxyz.so
$ gcc myapp.c libxyz.tramp.S libxyz.init.c ...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接