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

Why the main executable and a shared library loaded by dlopen share one copy of a namespace static variable?

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

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

static std::unique_ptr<int> sCount;                                                                 
static BadLad sBadLad;

和badlad.cpp

#include "badlad.h"

BadLad::BadLad() {
    if (!sCount) {
        sCount.reset(new int(1));
        std::cout<<"BadLad, reset count, "<<*sCount<<std::endl;
    }
    else {
        ++*sCount;
        std::cout<<"BadLad, "<<*sCount<<std::endl;
    }
}

BadLad::~BadLad() {
    if (sCount && --*sCount == 0) {
        std::cout<<"~BadLad, delete "<<*sCount<<std::endl;
        delete(sCount.release());
    }
    else {
        std::cout<<"~BadLad, "<<*sCount<<std::endl;
    }
}

我希望 sCount 和 sBadLad 在包含 badlad.h.

的每个 cpp 文件中都是唯一的

然而,我在下面的实验中发现情况并非如此:

主程序如下所示:

#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;
}

在执行主程序时,我可以看到 sCount 变量在调用 main 之前首先创建并设置为 1,这是预期的。但是在调用第一个 dlopen 之后,sCount 增加到 2,随后在调用 dlclose 时减少到 1。第二个 dlopen/dlclose.

也是如此

所以我的问题是,为什么只有一份 sCount?为什么 linker 不将副本分开(我认为这是大多数人所期望的)?如果我 link libPlugin.so 直接而不是 dlopen 到 main,它的行为是一样的。

我 运行 在 macOS 上使用 clang-4 (clang-900.0.39.2)。

编辑:请查看 this repo 中的完整源代码。

(迭代 2)

你的情况很有趣也很不幸。下面我们一步一步来分析。

  1. 您的程序 link 针对 libBadLad.so。因此,该共享库会在程序启动时加载。静态对象的构造函数在 main.
  2. 之前执行
  3. 然后您的程序会打开 libplugin.so。然后加载此共享库,并执行静态对象的构造函数。
  4. libplugin.so 被 link 反对的 libBadLad.so 怎么样?由于进程已经包含了libBadLad.so的图像,没有第二次加载这个共享库libplugin.so 也可以完全不 link 反对它。
  5. 回到libplugin.so的静态对象。其中有两个,sCountsBadLad两者都是按顺序构造的
  6. sBadLad 有一个用户定义的非内联构造函数。 它没有在libplugin.so中定义,所以它是针对已经加载的libBadLad.so解析的,它定义了这个符号。
  7. BadLad::BadLad libBadLad.so 调用。
  8. 这个构造函数引用了一个静态变量sCount这从 libBadLad.so 解析为 sCount,而不是从 libplugin.so 解析为 sCount,因为函数本身在 libBadLad.so 中。这已经初始化并指向一个值为 1 的 int
  9. 计数递增。
  10. 与此同时,来自 libplugin.sosCount 静静地坐着,正在初始化为 nullptr
  11. 库被卸载并再次加载等

这个故事的寓意是什么? 静态变量是邪恶的。避免。

请注意,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

为什么会出现分段错误?这是我们很好的旧静态初始化命令惨败。这个故事的主旨? 静态变量是邪恶的