今天在调试时发现了一个奇怪的core:double free or corruption (fasttop)
,从堆栈看是由于 _dl_fini
函数多次重复释放了某些 STL 容器导致的,此时就算在 main
函数中只保留个简单 return 0
也会出错,因此猜想肯定和某些全局变量有关。后面经过各种修改尝试,终于发现这是由于引用的 .so
动态库和主程序中定义了同名的全局 STL 容器导致的,此时的行为简直就是一个神坑,很有必要记录一下……
先说最终结论吧:
- 多个动态库或者是动态库与主程序间可以有同名全局符号,包括全局变量和函数等,此时链接过程是不会出错的。
- 这些同名全局符号的地址是相同的!
- 链接过程中会从前往后依次查找符号,对于
.so
及 .a
来说,如果遇到相同的全局变量是不会报错的,此时 GCC 会默默的选择第一个,这种情况连 Warning 都不会有。这与多个 .o
是不同的,在多个 .o
中定义相同的全局变量无法正常链接。
- 以上行为的问题在于,绝大部分情况下各模块期望的行为都是调用自己的全局变量及函数,而不是调用其他模块的,因此大概率会造成运行时的各种异常。
- 尤有甚者,若全局变量并不是基本类型而是
class
,那虽然此变量只有一个内存地址,然而其构造与析构函数会被调用多次,若其中有动态分配的内存,多次 delete
就会导致 double free
异常。
- 以上构造与析构行为是通过编译时向
_init()
及 _fini()
中添加 hook 函数实现的,构造顺序是链接顺序,析构顺序是其逆序。前文提到的 _dl_fini()
函数应该是 _fini()
的动态库版本。
上面这些行为看上去已经很坑了吧,然而这并不是全部……以上行为仅适用于编译时直接指定需要链接库的情况,若是在程序运行过程中使用 dlopen
动态加载 .so
时行为不太一样;若通过 LD_PRELOAD
指定动态库那行为又不一样了……
使用 dlopen
加载时的行为可以简要归纳如下:
- 主程序中的全局符号是永远都不会被加载进来的动态库给覆盖的,无论是变量还是函数。这与很多文章中说的不太一样,然而实际使用 GCC 9.3 测试的结果就是如此,估计是较新的 GCC 版本做了什么修改导致的。
- 多次调用
dlopen
加载多个动态库,若这些动态库间存在相同的全局符号,则它们之间是有可能相互覆盖的,这取决于 dlopen
的 flag
。若使用 RTLD_GLOBAL
,则后面加载进来的动态库会使用已有的全局符号;若使用 RTLD_LOCAL
,则每个动态库间的符号是独立的。
- 上述行为中,对应全局变量的构造及析构每次都会进行,也就是后面加载进来的动态库会在之前内存的基础上再来构造一次,退出的时候也会析构多次。
以上很多行为显然应该都不是预期行为的,那如何解决这些问题呢,大概有这些方法:
- 创建
.so
时加上编译选项 -Wl,-Bsymbolic
,这会强制采用本地的全局变量定义。
- 可以通过
__attribute__ ((visibility("xxx")))
来控制符号可见性,并通过编译选项 -fvisibility=xxx
来控制默认符号可见性。
- 将不需要导出的全局变量声明为
static
的。
- 最根本的做法,通过 namespace 等手段从根本上避免同名变量及函数的存在!
最后给出几个简单测试程序,可以对照着理解上面的各种行为。
my_calss.h
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <iostream>
void g_fun();
class MyClass { public: MyClass(int a) : a_(a) { std::cout << "Construct! " << a_ << " @ " << this << std::endl; g_fun(); }
~MyClass() { std::cout << "Destruct! " << a_ << " @ " << this << std::endl; }
private: int a_; };
|
my_lib1.cc
:
1 2 3 4 5 6 7 8
| #include <iostream> #include "my_class.h"
MyClass g_var(1);
void g_fun() { std::cout << __FILE__ << ":" << __LINE__ << std::endl; }
|
my_lib2.cc
:
1 2 3 4 5 6 7 8
| #include <iostream> #include "my_class.h"
MyClass g_var(2);
void g_fun() { std::cout << __FILE__ << ":" << __LINE__ << std::endl; }
|
app1.cc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include "my_class.h"
MyClass g_var(10);
void g_fun() { std::cout << __FILE__ << ":" << __LINE__ << std::endl; }
int main() { std::cout << "----------" << std::endl; g_fun(); std::cout << "----------" << std::endl; return 0; }
|
app2.cc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <iostream> #include <unistd.h> #include <dlfcn.h> #include "my_class.h"
MyClass g_var(12);
void g_fun() { std::cout << __FILE__ << ":" << __LINE__ << std::endl; }
int main() { dlopen("./libmylib1.so", RTLD_NOW); dlopen("./libmylib2.so", RTLD_NOW); std::cout << "----------" << std::endl; g_fun(); std::cout << "----------" << std::endl; return 0; }
|
app3.cc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <iostream> #include <unistd.h> #include <dlfcn.h> #include "my_class.h"
MyClass g_var(12);
void g_fun() { std::cout << __FILE__ << ":" << __LINE__ << std::endl; }
int main() { dlopen("./libmylib1.so", RTLD_NOW | RTLD_GLOBAL); dlopen("./libmylib2.so", RTLD_NOW | RTLD_GLOBAL); std::cout << "----------" << std::endl; g_fun(); std::cout << "----------" << std::endl; return 0; }
|
Makefile
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| mylib1: my_class.h my_lib1.cc g++ -fPIC -shared -o libmylib1.so my_lib1.cc
mylib2: my_class.h my_lib2.cc g++ -fPIC -shared -o libmylib2.so my_lib2.cc
app1: app1.cc my_class.h mylib1 mylib2 g++ -L./ -lmylib1 -lmylib2 -o app1 app1.cc
app2: app2.cc my_class.h g++ -ldl -o app2 app2.cc
app3: app3.cc my_class.h g++ -ldl -o app3 app3.cc
all: app1 app2 app3
|
测试程序运行结果为:
app1
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| $./app1 Construct! 2 @ 0x404194 app1.cc:6 Construct! 1 @ 0x404194 app1.cc:6 Construct! 10 @ 0x404194 app1.cc:6 ---------- app1.cc:6 ---------- Destruct! 10 @ 0x404194 Destruct! 10 @ 0x404194 Destruct! 10 @ 0x404194
|
app2
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| $./app2 Construct! 12 @ 0x404194 app2.cc:9 Construct! 1 @ 0x7fd81b9f106c my_lib1.cc:7 Construct! 2 @ 0x7fd81b9ec06c my_lib2.cc:7 ---------- app2.cc:9 ---------- Destruct! 2 @ 0x7fd81b9ec06c Destruct! 1 @ 0x7fd81b9f106c Destruct! 12 @ 0x404194
|
app3
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| $./app3 Construct! 12 @ 0x404194 app3.cc:9 Construct! 1 @ 0x7efd3799d06c my_lib1.cc:7 Construct! 2 @ 0x7efd3799d06c my_lib1.cc:7 ---------- app3.cc:9 ---------- Destruct! 2 @ 0x7efd3799d06c Destruct! 2 @ 0x7efd3799d06c Destruct! 12 @ 0x404194
|
本文只是一个简单的总结,关于此问题的更多深入讨论可以参考以下文章:
控制共享库的符号可见性 第 1 部分 - 符号可见性简介
浅谈动态库符号的私有化与全局化
linux动态库的种种要点
Linux动态链接库so版本兼容
浅析静态库链接原理
全局符号
Linux下全局符号覆盖问题
What exactly does -Bsymblic do?