多库动态加载场景下静态存储期对象的唯一性保证
动态加载场景下共享库中静态对象的唯一性问题
这个问题问到点子上了——这可是动态链接里容易踩坑的细节,咱给你理得明明白白:
首先明确前提:你说的S是共享库(比如Linux下的.so、macOS的.dylib),不是静态库(.a)——如果是静态库的话情况完全不同,后面会专门说明。
主流系统的默认行为:O保持唯一性
在Linux、macOS这类主流Unix-like系统,以及Windows的默认动态链接器规则下,O会是唯一的实例,原因如下:
- 当你用
dlopen加载A时,动态链接器会自动解析并加载A的依赖库S,把S映射到进程的地址空间中。 - 之后再
dlopen加载B时,动态链接器会检查:当前进程里是不是已经加载了和B依赖的S“匹配”的库(匹配规则一般基于库的soname、路径或者唯一标识)。发现已经加载过S后,就不会再加载一份新的,而是直接复用已经存在的S实例。 - 所以A和B里调用的O,本质上都是同一个内存地址里的对象,自然保持唯一性,和C直接链接A、B的结果完全一致。
可以验证的小例子
写几行代码就能直观确认:
- 共享库S的代码(s.cpp):
#include <iostream> int O = 0; // 构造函数,加载时打印O的地址 __attribute__((constructor)) void init_O() { std::cout << "O initialized at: " << &O << std::endl; } void set_O(int val) { O = val; } int get_O() { return O; } // 暴露获取O地址的接口 int* get_O_addr() { return &O; }
编译成共享库:g++ -shared -fPIC s.cpp -o libs.so
- 共享库A的代码(a.cpp):
extern void set_O(int); extern int get_O(); extern int* get_O_addr(); void set_A_O(int val) { set_O(val); } int get_A_O() { return get_O(); } int* get_A_O_addr() { return get_O_addr(); }
编译:g++ -shared -fPIC a.cpp -L. -ls -o liba.so
- 共享库B的代码(b.cpp):
extern void set_O(int); extern int get_O(); extern int* get_O_addr(); void set_B_O(int val) { set_O(val); } int get_B_O() { return get_O(); } int* get_B_O_addr() { return get_O_addr(); }
编译:g++ -shared -fPIC b.cpp -L. -ls -o libb.so
- 可执行文件C的代码(c.cpp):
#include <dlfcn.h> #include <iostream> int main() { // 加载A和B void* handleA = dlopen("./liba.so", RTLD_NOW); void* handleB = dlopen("./libb.so", RTLD_NOW); // 解析函数 typedef void (*SetFunc)(int); typedef int (*GetFunc)(); typedef int* (*GetAddrFunc)(); SetFunc setA = (SetFunc)dlsym(handleA, "set_A_O"); GetFunc getA = (GetFunc)dlsym(handleA, "get_A_O"); GetAddrFunc getAAddr = (GetAddrFunc)dlsym(handleA, "get_A_O_addr"); SetFunc setB = (SetFunc)dlsym(handleB, "set_B_O"); GetFunc getB = (GetFunc)dlsym(handleB, "get_B_O"); GetAddrFunc getBAddr = (GetAddrFunc)dlsym(handleB, "get_B_O_addr"); // 测试:通过A修改O,看B读取的结果 setA(42); std::cout << "A读取O的值:" << getA() << ",O的地址:" << getAAddr() << std::endl; std::cout << "B读取O的值:" << getB() << ",O的地址:" << getBAddr() << std::endl; dlclose(handleA); dlclose(handleB); return 0; }
编译:g++ c.cpp -ldl -o c
运行后你会看到:
- 只打印一次
O initialized at: [地址] - A和B输出的O地址完全相同,且B读取到的值是42,证明是同一个实例。
特殊情况:O会变成多个实例
只有在以下极端场景下,O才会出现多个实例:
- S是静态库:如果S是静态库(
.a),那么A和B在编译时会把S的代码(包括O)直接复制到各自的共享库文件里。这时候dlopen加载A和B,进程里就会有两个独立的O实例,彼此互不影响。 - 强制加载多份S:比如在
dlopen时使用了某些系统特定的标志(比如Linux的RTLD_PRIVATE,不过这个标志非常小众),或者S被编译时用了特殊选项强制不共享,这时候动态链接器会为A和B分别加载一份S,自然O也会有多个实例。
Windows平台的补充
Windows下DLL的行为和Unix-like系统类似:默认同一个DLL只会被加载一次,全局变量(对应这里的O)保持唯一。除非你用LOAD_LIBRARY_AS_DATAFILE这类特殊加载标志(几乎不会在正常业务代码里用到),才会加载多份DLL,导致多个实例。
内容的提问来源于stack exchange,提问作者Yuki
相关产品推荐
相关产品推荐

