发布了上一篇文章后,有朋友来讨论了这个问题,感觉说的还是模糊了一些,虽然发现了问题的所在,也提供了解决方案,
但是并没有找到到底是谁怎么释放了这个staic变量,也没有说明GNU为什么会这样处理这种导出的local static变量。
周末花了不少时间研究并写了一个小小的例程来演示问题和多种解决方案,发布在了我的github,有兴趣的同学可以看一看。
https://github.com/stonewell/code-snippets/tree/master/function%5Fstatic%5Fvariable
先来说明一下GNU的情况,这个问题在GNU中叫做 STB_GNU_UNIQUE 文档是这样描述的
-fno-gnu-unique
On systems with recent GNU assembler and C library, the C++ compiler uses the
STB_GNU_UNIQUE binding to make sure that definitions of template static data members and
static local variables in inline functions are unique even in the presence of RTLD_LOCAL;
this is necessary to avoid problems with a library used by two different RTLD_LOCAL plugins
depending on a definition in one of them and therefore disagreeing with the other one about
the binding of the symbol. But this causes dlclose to be ignored for affected DSOs;
if your program relies on reinitialization of a DSO via dlclose and dlopen,
you can use -fno-gnu-unique.
也就是说GNU的libc特殊处理了这种情况,就是为了提供进程内真正的static变量,无论是不是 RTLD_LOCAL 还是有多份拷贝,
同时gcc也提供了解决方法, -fno-gnu-unique 编译选项就能关闭这个功能,同时使用readelf查看生成的代码也可以直观的看到。
默认使用 STB_GNU_UNIQUE 的时候
0000000000006f70 0000002a00000006 R_X86_64_GLOB_DAT 00000000000071c0 _ZZN21CMockableSingletonPtrI7FooImplS0_E20GetRefToSingletonPtrEvE13pSingletonPtr + 0
42: 00000000000071c0 8 OBJECT UNIQUE DEFAULT 25 _ZZN21CMockableSingletonPtrI7FooImplS0_E20GetRefToSingletonPtrEvE13pSingletonPtr
97: 00000000000071c0 8 OBJECT UNIQUE DEFAULT 25 _ZZN21CMockableSingletonPtrI7FooImplS0_E20GetRefToSingletonPtrEvE13pSingletonPtr
使用 -fno-gnu-unique 的时候
0000000000006f70 0000002a00000006 R_X86_64_GLOB_DAT 00000000000071c0 _ZZN21CMockableSingletonPtrI7FooImplS0_E20GetRefToSingletonPtrEvE13pSingletonPtr + 0
42: 00000000000071c0 8 OBJECT WEAK DEFAULT 25 _ZZN21CMockableSingletonPtrI7FooImplS0_E20GetRefToSingletonPtrEvE13pSingletonPtr
97: 00000000000071c0 8 OBJECT WEAK DEFAULT 25 _ZZN21CMockableSingletonPtrI7FooImplS0_E20GetRefToSingletonPtrEvE13pSingletonPtr
使用 -version-script 的时候
57: 00000000000040d0 8 OBJECT LOCAL DEFAULT 25 _ZZN21CMockableSingletonPtrI7FooImplS0_E20GetRefToSingletonPtrEvE13pSingletonPtr
在了解了GNU的处理方式后,再回头来看是谁释放了这个变量呢?从理论上来说GNU的处理应该不会出现这个问题,上一篇文章中举的例子实际上是个程序错误,
也被朋友吐槽说代码bug了,所以这次开发的例程里展示的是没有错误的版本(我们的目标。。。。没有蛀牙)。
例程使用了多份拷贝的版本,prog主程序会
- 先用dlopen加载libfoo_global.so,调用一个函数,dlclose卸载libfoo_globa.so
- 加载libfoo_global_2.so (这是libfoo_globa.so的一份拷贝), 调用函数,卸载libfoo_global_2.so
- 再次加载libfoo_global_2.so, 调用函数,卸载libfoo_global_2.so
lib目录里是默认编译,lib_2里是使用 -version-script , lib_3里是使用 -fno-gnu-unique
运行lib中的代码
$> prog/prog lib/libfoo_global.so lib/libfoo_global_2.so
run first round, call CallBar3 which do not trigger Singleton construct
lib handle:0x559d75defee0
callbar func:CallBar3, 0x7f1c9ad5b421
callbar3 called,first round
run second round, call CallBar trigger singleton construct, but dlclose will destruct the singleton
lib handle:0x559d75df0570
callbar func:CallBar, 0x7f1c9ad533b5
0x559d75df0c00, output construct +++++++++++++++++==
0x559d75df0be0, foo impl costruct, output:0x559d75df0c00
foo instance:0x559d75df0be0
0x559d75df0be0, output:0x559d75df0c00, this is FooImpl::bar:second round
0x559d75df0c00, this is output:second round, xxxxxxxxxxxx
0x559d75df0be0, output:0x559d75df0c00, foo impl destruct
0x559d75df0be0, output:0, foo impl destruct
run third round, if static variable not init in first round, crash here
lib handle:0x559d75df0570
callbar func:CallBar, 0x7f1c9ad533b5
foo instance:0x559d75df0be0
0x559d75df0be0, output:0x559d75ddd010, this is FooImpl::bar:third round
Segmentation fault
运行lib_2中的代码
$> prog/prog lib_2/libfoo_global.so lib_2/libfoo_global_2.so
run first round, call CallBar3 which do not trigger Singleton construct
lib handle:0x55b21353bee0
callbar func:CallBar3, 0x7f4d03a9a241
callbar3 called,first round
run second round, call CallBar trigger singleton construct, but dlclose will destruct the singleton
lib handle:0x55b21353c550
callbar func:CallBar, 0x7f4d03a9a1d5
0x55b21353c060, output construct +++++++++++++++++==
0x55b21353bec0, foo impl costruct, output:0x55b21353c060
foo instance:0x55b21353bec0
0x55b21353bec0, output:0x55b21353c060, this is FooImpl::bar:second round
0x55b21353c060, this is output:second round, xxxxxxxxxxxx
0x55b21353bec0, output:0x55b21353c060, foo impl destruct
0x55b21353bec0, output:0, foo impl destruct
run third round, if static variable not init in first round, crash here
lib handle:0x55b21353c550
callbar func:CallBar, 0x7f4d03a9a1d5
0x55b21353c060, output construct +++++++++++++++++==
0x55b21353bec0, foo impl costruct, output:0x55b21353c060
foo instance:0x55b21353bec0
0x55b21353bec0, output:0x55b21353c060, this is FooImpl::bar:third round
0x55b21353c060, this is output:third round, xxxxxxxxxxxx
0x55b21353bec0, output:0x55b21353c060, foo impl destruct
0x55b21353bec0, output:0, foo impl destruct
运行lib_3中的代码
$> prog/prog lib_2/libfoo_global.so lib_2/libfoo_global_2.so
run first round, call CallBar3 which do not trigger Singleton construct
lib handle:0x560dcfb76ee0
callbar func:CallBar3, 0x7fbef0260421
callbar3 called,first round
run second round, call CallBar trigger singleton construct, but dlclose will destruct the singleton
lib handle:0x560dcfb77550
callbar func:CallBar, 0x7fbef02603b5
0x560dcfb77060, output construct +++++++++++++++++==
0x560dcfb76ec0, foo impl costruct, output:0x560dcfb77060
foo instance:0x560dcfb76ec0
0x560dcfb76ec0, output:0x560dcfb77060, this is FooImpl::bar:second round
0x560dcfb77060, this is output:second round, xxxxxxxxxxxx
0x560dcfb76ec0, output:0x560dcfb77060, foo impl destruct
0x560dcfb76ec0, output:0, foo impl destruct
run third round, if static variable not init in first round, crash here
lib handle:0x560dcfb77550
callbar func:CallBar, 0x7fbef02603b5
0x560dcfb77060, output construct +++++++++++++++++==
0x560dcfb76ec0, foo impl costruct, output:0x560dcfb77060
foo instance:0x560dcfb76ec0
0x560dcfb76ec0, output:0x560dcfb77060, this is FooImpl::bar:third round
0x560dcfb77060, this is output:third round, xxxxxxxxxxxx
0x560dcfb76ec0, output:0x560dcfb77060, foo impl destruct
0x560dcfb76ec0, output:0, foo impl destruct
lib_2和lib_3的结果都可以清楚看到,每次libfoo_global_2.so加载和卸载都会初始化和释放静态变量
lib的结果显示,静态变量在第一次加载libfoo_global_2.so的时候被初始化,卸载时被释放了,
而第二次加载时候没有初始化,导致crash,代码中是没有强制释放这个静态变量的,那么为什么会释放呢?
请注意run first round, 加载libfoo_global.so调用CallBar3函数的时候是没有触发和使用静态变量的,根据我的分析
GNU把libfoo_global.so标记为不可卸载,后续的libfoo_global_2.so就是可以卸载的,静态变量是在libfoo_global_2.so
中初始化,也就随着libfoo_global_2.so的卸载而释放了,但是C运行时已经标记这个静态变量已经初始化了,导致下次使用crash。
也就是说第一次加载并释放libfoo_global_2.so后,无论加载libfoo_global.so还是libfoo_global_2.so都会crash。
那么如果在第一次加载 libfoo_global.so 的时候就触发变量初始化是不是就没有问题了呢?答案是肯定的
$> prog/prog lib/libfoo_global.so lib/libfoo_global_2.so
run first round, call CallBar which do trigger Singleton construct
lib handle:0x55752c808ee0
callbar func:CallBar, 0x7f70e956f3b5
0x55752c809570, output construct +++++++++++++++++==
0x55752c809550, foo impl costruct, output:0x55752c809570
foo instance:0x55752c809550
0x55752c809550, output:0x55752c809570, this is FooImpl::bar:first round
0x55752c809570, this is output:first round, xxxxxxxxxxxx
run second round, call CallBar trigger singleton construct, but dlclose will destruct the singleton
lib handle:0x55752c8095d0
callbar func:CallBar, 0x7f70e95673b5
foo instance:0x55752c809550
0x55752c809550, output:0x55752c809570, this is FooImpl::bar:second round
0x55752c809570, this is output:second round, xxxxxxxxxxxx
run third round, if static variable not init in first round, crash here
lib handle:0x55752c808ee0
callbar func:CallBar, 0x7f70e956f3b5
foo instance:0x55752c809550
0x55752c809550, output:0x55752c809570, this is FooImpl::bar:third round
0x55752c809570, this is output:third round, xxxxxxxxxxxx
0x55752c809550, output:0x55752c809570, foo impl destruct
0x55752c809550, output:0, foo impl destruct
结论就是,如果想要使用 STB_GNU_UNIQUE 带来的好处,就要尽早初始化静态变量。
在实际情况中libstdc++中就大量的使用了这个功能