C++ 为什么dlopen加载的主可执行文件和共享库共享名称空间静态变量的一个副本?

C++ 为什么dlopen加载的主可执行文件和共享库共享名称空间静态变量的一个副本?,c++,static,linker,shared-libraries,dlopen,C++,Static,Linker,Shared Libraries,Dlopen,据我所知,命名空间范围静态变量在每个编译单元中应该有一个副本。如果我有这样的头文件: class BadLad { public: B

据我所知,命名空间范围静态变量在每个编译单元中应该有一个副本。如果我有这样的头文件:

class BadLad {                                                                                      
public:                                                                                             
    BadLad();                                                                                       
    ~BadLad();                                                                                      
};                                                                                                  

static std::unique_ptr<int> sCount;                                                                 
static BadLad sBadLad;
#include <dlfcn.h>

int main() {
    void* dll1 = dlopen("./libplugin.so", RTLD_LAZY);
    dlclose(dll1);

    void* dll2 = dlopen("./libplugin.so", RTLD_LAZY);
    dlclose(dll2);

    return 0;
}
在执行主程序时,我可以看到
scont
变量首先被创建,并在调用main之前设置为1,这是预期的。但是,在调用第一个
dlopen
之后,
scont
增加到2,然后在调用
dlclose
时减少到1。第二个dlopen/dlclose也会发生同样的情况

所以我的问题是,为什么只有一份Scont?为什么链接器不将副本分开(我认为这是大多数人所期望的)?如果我直接将libPlugin.so链接到main而不是dlopen,则其行为相同

我用clang-4(clang-900.0.39.2)在macOS上运行这个

编辑:请参阅中的完整源代码。

(迭代2)

你的情况很有趣,也很不幸。让我们一步一步地分析它

  • 您的程序链接到
    libBadLad.so
    。因此,在程序启动时加载此共享库。静态对象的构造函数在
    main
    之前执行
  • 然后,您的程序将打开
    libplugin.so
    。然后加载该共享库,并执行静态对象的构造函数
  • 那么
    libBadLad.so
    这个
    libplugin.so
    链接的对象呢?由于进程已包含
    libBadLad.so
    ,因此不会第二次加载此共享库<代码>libplugin。所以完全可以不链接它
  • 回到
    libplugin.so的静态对象。它们有两种,
    scont
    sBadLad
    。两者都是按顺序构建的
  • sBadLad
    具有用户定义的非内联构造函数。它没有在
    libplugin.so
    中定义,因此它是针对已加载的
    libBadLad.so
    进行解析的,后者定义了此符号
  • BadLad::BadLad
    from
    libBadLad.so
    被调用
  • 此构造函数引用一个静态变量
    scont
    。这将解析为
    libBadLad.so
    中的
    scont
    ,而不是
    libplugin.so
    中的
    scont
    ,因为函数本身位于
    libBadLad.so
    中。这已初始化,并指向值为1的
    int
  • 计数是递增的
  • 同时,
    libplugin中的
    scont
    。因此
    安静地坐着,被初始化为
    nullptr
  • 库被卸载并再次加载,等等
  • 这个故事的寓意是什么?静态变量是邪恶的。避免

    <>注意,C++标准没有任何关于这方面的说法,因为它不处理动态负载。 然而,类似的效果可以在没有任何动态加载的情况下再现

       // foo.cpp
       #include "badlad.h"
    
       // bar.cpp
       #include "badlad.h"
       int main () {}
    
    构建和测试:

       # > g++ -o test foo.cpp bar.cpp badlad.cpp
       ./test
       BadLad, reset count to, 1
       BadLad, 2
       BadLad, 3
       ~BadLad, 2
       Segmentation fault
    

    为什么分割错误?这是我们旧的静态初始化命令失败。这个故事的寓意是什么?静态变量是有害的。

    您可以在相关部分中详细查看。我认为基本机制是一样的。欢迎来到dll地狱。基本上,每个dll都有静态/全局变量,就像它的代码一样。@post说确实有两个副本,但主副本不知怎么地掩盖了共享库中的副本?但是为什么链接器更喜欢这样做,这不违背对静态变量的一般理解吗?我相信这是线程安全的事情,因为共享库可以托管在不同的线程上下文中,并且静态变量实例化可以保证线程安全。不过,链接器对线程上下文一无所知。我对此不确定,否则我会把它作为一个答案贴出来。是POSIX标准BTW。我想到的一个想法是,将
    静态
    变量定义放在那些TU中的匿名名称空间中,这可能与您希望为单独的翻译单元提供单独副本的方式相同。这些应该是真正的私人副本在那里。你完全正确!如果我让BadLad构造函数内联,Scont将永远不会超过1。
       # > g++ -o test foo.cpp bar.cpp badlad.cpp
       ./test
       BadLad, reset count to, 1
       BadLad, 2
       BadLad, 3
       ~BadLad, 2
       Segmentation fault