动态加载库中的静态C++对象在dlopen()返回之前被初始化吗?

4
下面的代码演示了预期的行为(也可以说是直观的)。就像可执行文件中的静态对象在进入 main() 之前被初始化一样,人们期望在动态加载库中的静态对象在 dlopen() 返回之前被初始化。
问题是:这种运行时加载的库的行为是否有任何保证,或者它只是一个便利的偶然事件或幸运的实现细节?我们能否依赖于库中静态对象的构造函数被调用,还是必须使用标有 __attribute__((constructor)) 的函数等替代方法来确保在 dlopen() 调用范围内实现某些所需的行为?
// libtest.cpp
#include <iostream>

namespace
{
    class Test
    {
    public:
        Test()  { std::cerr << "In Test()...\n"; }
        ~Test() { std::cerr << "In ~Test()...\n"; }
    };

    Test    test; // when is this initialized?
}

// testso.cpp
#include <dlfcn.h>
#include <iostream>

int main( int ac, char* av[] )
{
    if ( ac < 2 )
    {
        std::cerr << "Usage: " << av[0] << "library-name\n";
        return 1;
    }
    std::cerr << "Before dlopen()...\n";
    ::dlerror();
    void*    _handle(::dlopen( av[1], RTLD_NOW ));
    std::cerr << "After dlopen()...\n";
    if ( !_handle )
    {
        std::cerr << "Error: " << ::dlerror() << ", exiting...\n";
        return 2;
    }
    ::dlclose( _handle );
    std::cerr << "After dlclose()...\n";
    return 0;
}

编译并运行(请注意在dlopen()返回之前调用Test()):

$ g++ -o libtest.so -shared -fPIC libtest.cpp
$ g++ -o testso -ldl testso.cpp
$ ./testso ./libtest.so
Before dlopen()...
In Test()...
After dlopen()...
In ~Test()...
After dlclose()...
$ 

.


当一个共享库被动态链接时,其中的全局变量和静态变量会发生什么? - Donghui Zhang
第一个提示:如果可以避免,不要使用__attribute__((constructor))。它通常被认为是一种hack方法,而且不可移植。相反,使用一个导出的函数。只是提醒一下。 - Qix - MONICA WAS MISTREATED
一个导出的函数必须从外部调用;而构造函数则会自动运行,不需要任何外部操作(除了启动库本身的加载)。当需要后者的行为时,问题在于静态对象的构造函数是否能够实现这一点。对于像C这样没有本地构造函数概念的语言,“构造函数”就是这样的。 - arayq2
1个回答

4

首先,需要明确的是:dlopen() 非常依赖于平台,就像早期的疾病症状和WebMD一样,您应该始终查阅所在平台相关的 man 页面。

即使 dlopen() 函数族似乎符合 IEEE Standard 1003.1, 2004 Edition,这仍然是正确的。但我无法告诉您今天的系统有多么符合此类标准(例如,Windows一直以来都存在 糟糕的POSIX兼容性 问题)。


在OS/X / BSD上,是的

dlopen()会检查由path指定的mach-o文件。如果该文件与当前进程兼容且尚未加载到当前进程中,则会被加载和链接。在链接后,如果它包含任何初始化函数,则在dlopen()返回之前调用它们。

强调我自己添加的。


在Linux上,是的:
共享对象可以使用__attribute__((constructor))__attribute__((destructor))函数属性导出函数。构造函数在dlopen()返回之前执行,而析构函数在dlclose()返回之前执行。 强调是我的。
在Solaris系统中,是的:在加载新对象时,在dlopen()返回之前会调用对象内部的初始化代码。这个初始化是用户代码,并且因此可能会产生无法被dlopen()捕获的错误。

如果进程已经有多个线程,其他线程是否可能(意外地)在dlopen返回之前开始使用dl?当然,初始化程序可以做一些事情来让其他线程参与,但假设没有这样明确的操作。 - joeking
@joeking 这就是良好的并发实践所在 :) 我不会对系统调用的并发性做出任何假设,尤其是脆弱的 dlopen() 系列函数(虽然这只是个例子,但通常是个好建议)。 - Qix - MONICA WAS MISTREATED
是的 - 这是Windows和Linux具有相似功能但细节上存在巨大差异的领域。在Windows上,“DllMain()”是调用初始化程序的函数,它在某种程度上是单线程的,因为操作系统保持加载器锁定(尽管其确切影响并未清楚地记录)。结果是您必须小心从这些初始化程序调用哪些操作系统函数。 - joeking
如果它们没有被加载,很多东西都会出问题(静态类成员是最糟糕的)。所有初始化例程都属于“初始化”。你在这里的假设是安全的 :) - Qix - MONICA WAS MISTREATED
1
这份文档指出,.ctors部分包含了指向标记函数以及C++构造函数的指针,因此我认为这个问题已经解决了:-) - arayq2
显示剩余3条评论

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