为什么主可执行文件和 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 文件中都是唯一的
然而,我在下面的实验中发现情况并非如此:
- 我将 badlad 编译成一个共享库
libBadLad.so
。
- 我创建了另一个共享库
libPlugin.so
link
libBadLad.so
,只有 plugin.cpp 包括 badlad.h
,所以我预计
libPlugin.so. 中有一份 sCount
- 我创建了一个 links libBadLad.so 的主程序,我希望有
一份
sCount
in 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?为什么 linker 不将副本分开(我认为这是大多数人所期望的)?如果我 link libPlugin.so 直接而不是 dlopen 到 main,它的行为是一样的。
我 运行 在 macOS 上使用 clang-4 (clang-900.0.39.2)。
编辑:请查看 this repo 中的完整源代码。
(迭代 2)
你的情况很有趣也很不幸。下面我们一步一步来分析。
- 您的程序 link 针对
libBadLad.so
。因此,该共享库会在程序启动时加载。静态对象的构造函数在 main
. 之前执行
- 然后您的程序会打开
libplugin.so
。然后加载此共享库,并执行静态对象的构造函数。
libplugin.so
被 link 反对的 libBadLad.so
怎么样?由于进程已经包含了libBadLad.so
的图像,没有第二次加载这个共享库。 libplugin.so
也可以完全不 link 反对它。
- 回到
libplugin.so
的静态对象。其中有两个,sCount
和 sBadLad
。 两者都是按顺序构造的。
sBadLad
有一个用户定义的非内联构造函数。 它没有在libplugin.so
中定义,所以它是针对已经加载的libBadLad.so
解析的,它定义了这个符号。
BadLad::BadLad
从 libBadLad.so
调用。
- 这个构造函数引用了一个静态变量
sCount
。 这从 libBadLad.so
解析为 sCount
,而不是从 libplugin.so
解析为 sCount
,因为函数本身在 libBadLad.so
中。这已经初始化并指向一个值为 1 的 int
。
- 计数递增。
- 与此同时,来自
libplugin.so
的 sCount
静静地坐着,正在初始化为 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
为什么会出现分段错误?这是我们很好的旧静态初始化命令惨败。这个故事的主旨? 静态变量是邪恶的。
据我所知,命名空间作用域静态变量在每个编译单元中应该有一个副本。所以如果我有这样的头文件:
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
linklibBadLad.so
,只有 plugin.cpp 包括badlad.h
,所以我预计 libPlugin.so. 中有一份 - 我创建了一个 links libBadLad.so 的主程序,我希望有
一份
sCount
in main.
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?为什么 linker 不将副本分开(我认为这是大多数人所期望的)?如果我 link libPlugin.so 直接而不是 dlopen 到 main,它的行为是一样的。
我 运行 在 macOS 上使用 clang-4 (clang-900.0.39.2)。
编辑:请查看 this repo 中的完整源代码。
(迭代 2)
你的情况很有趣也很不幸。下面我们一步一步来分析。
- 您的程序 link 针对
libBadLad.so
。因此,该共享库会在程序启动时加载。静态对象的构造函数在main
. 之前执行
- 然后您的程序会打开
libplugin.so
。然后加载此共享库,并执行静态对象的构造函数。 libplugin.so
被 link 反对的libBadLad.so
怎么样?由于进程已经包含了libBadLad.so
的图像,没有第二次加载这个共享库。libplugin.so
也可以完全不 link 反对它。- 回到
libplugin.so
的静态对象。其中有两个,sCount
和sBadLad
。 两者都是按顺序构造的。 sBadLad
有一个用户定义的非内联构造函数。 它没有在libplugin.so
中定义,所以它是针对已经加载的libBadLad.so
解析的,它定义了这个符号。BadLad::BadLad
从libBadLad.so
调用。- 这个构造函数引用了一个静态变量
sCount
。 这从libBadLad.so
解析为sCount
,而不是从libplugin.so
解析为sCount
,因为函数本身在libBadLad.so
中。这已经初始化并指向一个值为 1 的int
。 - 计数递增。
- 与此同时,来自
libplugin.so
的sCount
静静地坐着,正在初始化为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
为什么会出现分段错误?这是我们很好的旧静态初始化命令惨败。这个故事的主旨? 静态变量是邪恶的。