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

标签 c++ static linker shared-libraries dlopen

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

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 文件中都是唯一的。

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

  • 我将 badlad 编译为共享库 libBadLad.so
  • 我创建了另一个共享库 libPlugin.so 链接 libBadLad.so,只有plugin.cpp包含badlad.h,所以我期待 libPlugin.so 中有一份sCount
  • 我创建了一个链接 libBadLad.so 的主程序,我希望有 一份 sCount 在 main.

主程序如下所示:

#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?为什么链接器不将拷贝分开(我认为这是大多数人所期望的)?如果我直接将 libPlugin.so 链接到 main 而不是 dlopen,它的行为是一样的。

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

编辑:请参阅 this repo 中的完整源代码.

最佳答案

(迭代 2)

发生在你身上的事很有趣也很不幸。下面我们一步一步来分析。

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

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

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

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

关于c++ - 为什么主可执行文件和 dlopen 加载的共享库共享一个命名空间静态变量的拷贝?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48629063/

相关文章:

c++ - 一对原始类型的良好散列函数

c++ - MingW 上更新的 G++ 收到大量错误消息

python - 使用 Pycparser 解析变量依赖关系

java - 无法访问枚举初始化程序中的静态字段

c++ - 未找到 assimp-vc140-mt.dll ASSIMP

linux - 我的 lib_LTLIBRARIES 库链接,但 check_LTLIBRARIES 没有链接?

C++ STM32用户定义类构造函数问题

c++ - 在VS Code调试器中更好地打印2d数组-C++

c# - 为什么常量可以隐式转换而静态只读字段不能?

c++ - 包括不工作的 guard ? (500 个类型为 LNK2005 x 的错误已在 y.obj 中定义)