【问题标题】:Why the main executable and a shared library loaded by dlopen share one copy of a namespace static variable?为什么 dlopen 加载的主可执行文件和共享库共享一个命名空间静态变量的副本?
【发布时间】:2018-07-15 16:37:09
【问题描述】:

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

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 的副本。

主程序如下所示:

#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 中的完整源代码。

【问题讨论】:

  • 您可以仔细查看相关部分中的2nd post。我认为底层机制是一样的。
  • 欢迎来到 dll 地狱。基本上每个 dll 都将拥有与其代码一样喜欢的静态/全局变量。
  • @TheDude 那篇帖子说确实有两个副本,但主要的副本以某种方式掩盖了共享库中的副本?但是为什么链接器更喜欢这样做,这不违背对静态变量的普遍理解吗?
  • 我相信这是线程安全的事情,因为共享库可以托管在不同的线程上下文中,并且静态变量实例化保证是线程安全的。但是,链接器对线程上下文一无所知。我不确定,否则我会将其发布为答案。 dlopen() 是 POSIX 标准顺便说一句。
  • 我想到的一个想法是,将static 变量定义放入这些 TU 中的匿名命名空间,它可以按照您期望为单独的翻译单元提供单独副本的方式工作。这些应该是那里真正的私人副本。

标签: c++ static linker shared-libraries dlopen


【解决方案1】:

(迭代 2)

在您的情况下发生的事情非常有趣且非常不幸。让我们一步一步来分析。

  1. 您的程序链接到libBadLad.so。因此,该共享库在程序启动时加载。静态对象的构造函数在main之前执行。
  2. 然后您的程序将打开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.so 解析为sCount,而不是从libplugin.so 解析为sCount,因为函数本身在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

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

【讨论】:

  • 你完全正确!如果我将 BadLad 构造函数内联,sCount 将永远不会超过 1。
猜你喜欢
  • 1970-01-01
  • 2012-05-19
  • 2019-11-16
  • 1970-01-01
  • 2019-04-17
  • 1970-01-01
  • 2015-09-05
  • 2011-01-31
  • 1970-01-01
相关资源
最近更新 更多