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

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

本文关键字:共享 副本 一个 变量 命名空间 可执行文件 dlopen 加载 为什么 静态      更新时间:2023-10-16

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

class BadLad {                                                                                      
public:                                                                                             
BadLad();                                                                                       
~BadLad();                                                                                      
};                                                                                                  
static std::unique_ptr<int> sCount;                                                                 
static BadLad sBadLad;

和坏人.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,只有插件.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;
}

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

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

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

编辑:请参阅此存储库中的完整源代码。

(迭代 2)

在你的案例中发生的事情非常有趣,也非常不幸。让我们一步一步地分析它。

  1. 您的程序与libBadLad.so链接。因此,此共享库在程序启动时加载。静态对象的构造函数在main之前执行。
  2. 然后,您的程序将打开libplugin.so.然后加载此共享库,并执行静态对象的构造函数。
  3. libplugin.solibBadLad.so相关联呢?由于进程已经包含libBadLad.so的映像,因此第二次不会加载此共享库libplugin.so也可以完全不反对它。
  4. 回到libplugin.so的静态对象。其中有两个,sCountsBadLad.两者都是按顺序构建的。
  5. sBadLad具有用户定义的非内联构造函数。它没有在libplugin.so中定义,所以它是针对已经加载的libBadLad.so解析的,它定义了这个符号。
  6. BadLad::BadLadlibBadLad.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

为什么会有分段错误?这是我们古老的静态初始化顺序惨败。故事的寓意是什么?静态变量是邪恶的