C++又一坑:动态链接库中的全局变量
前几天我们项目的日志系统出现了一点问题,但是一直没有时间去深究。 昨天在同事的帮助下,无意中猜了一种可能性,结果还真被我猜中了,于是今天就特别研究了一下,记录下来。
其实主要问题是三个模块。
- 模块 a, 静态库 a
- 模块 b, 二进制 b, 静态引用a, 动态加载c
- 模块 c, 动态链接库c, 静态引用a
关键在于静态库a里有一个静态全局变量,没错就是我们的日志模块。 原先的这个静态的模块中的静态全局变量是有 构造函数 的,也就是构造函数干了点事情。
我们都知道,程序载入在进入主函数前会依次初始化全部的全局和静态变量。载入动态链接库时也不例外。 这时候矛盾就来了,二进制b在进入主函数前会初始化模块a中的全局变量,执行构造函数;然而载入动态链接库c时,也会启动对c内的全局变量进行初始化,也会执行同一个对象的构造函数。这样, 一个对象就会执行两次构造函数 。
在我们的程序里,就是第二次执行构造函数的时候把全局变量的成员置空了。导致的结果是,我们的模块一开始有效,正常运行了一会会之后,就失效了。 当然在c里,并没有构造函数一说,对象构造时除了内存分配,什么都没干,所以 在纯c里并不会出现问题 。
这是碰到的问题,但是是不是在所有环境里都这样呢?或者使用静态成员函数又如何? 以下做了一个简单的测试:
模块a
a.h
struct foo_class {
int m;
foo_class();
~foo_class();
static foo_class _;
};
a.cpp
#include <cstdio>
#include "a.h"
foo_class foo_class::_;
foo_class::foo_class() {
m = 10;
printf("foo_class::foo_class(), this-> 0x%llx\n", this);
foo_class::~foo_class() {
printf("foo_class::~foo_class(), this-> 0x%llx\n", this);
}
编译选项
gcc -O0 -g -ggdb a.cpp -o libtest_a.a -c -fPIC
模块b
b.cpp
#include <dlfcn.h>
#include <errno.h>
#include <cstdio>
#include <cstdlib>
#include "a.h"
int main() {
void (*dll_func)() = NULL;
char* error = NULL;
foo_class::_.m += 1000;
printf("&foo_class::_ = 0x%llx, foo_class::_.m = %d\n", &foo_class::_, foo_class::_.m);
void* handle = dlopen("libtest_c.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return -1;
dlerror();
*(void **) (&dll_func) = dlsym(handle, "dll_func");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
return -2;
(*dll_func)();
dlclose(handle);
return 0;
}
编译选项
gcc -O0 -g -ggdb b.cpp -o test_b -fPIC -ldl -L$PWD -ltest_a -lstdc++
模块c
c.cpp
#include <cstdio>
#include "a.h"
extern "C" {
void dll_func() {
foo_class::_.m += 100;
printf("&foo_class::_ = 0x%llx, foo_class::_.m = %d\n", &foo_class::_, foo_class::_.m);
}
编译选项
gcc -O0 -g -ggdb c.cpp -o libtest_c.so -shared -fPIC -L$PWD -ltest_a -lstdc++
这是三个模块的代码和编译选项。我分别至于Linux和Windows内的GCC编译测试。 在 Linux 中的GCC 4.4.6 运行结果如下:
foo_class::foo_class(), this-> 0x600f98
&foo_class::_ = 0x600f98, foo_class::_.m = 1010
foo_class::foo_class(), this-> 0x600f98
&foo_class::_ = 0x600f98, foo_class::_.m = 110
foo_class::~foo_class(), this-> 0x600f98
foo_class::~foo_class(), this-> 0x600f98
从结果中可以看出来,在Linux中多个动态链接库和主程序引用的同一个全局变量(地址相同),但是每一个二进制实例都会完成一次构造。这就造成了同一个实例多次构造,导致我们最初碰到的结果。
在 Windows 中Cygwin的GCC 4.8.2 中运行结果如下:
foo_class::foo_class(), this-> 0x100406010
&foo_class::_ = 0x100406010, foo_class::_.m = 1010
foo_class::foo_class(), this-> 0x5aa426010
&foo_class::_ = 0x5aa426010, foo_class::_.m = 110