共享库中全局静态变量的析构函数未在 dlclose 上调用
Destructor of a global static variable in a shared library is not called on dlclose
在主程序中,我 dlopen
和 dlclose
(分别为 LoadLibrary
和 FreeLibrary
)一个共享库。共享库包含一个静态变量,该变量在 dlopen
时实例化,并在 dlclose
时销毁。此行为在 MSVC 2008 和 2013、GCC 3.4.6 和 Sunstudio 12.1 上是一致的。
然而,在 GCC 4.9.1 和 GCC 5.2.1 中,不再在 dlclose
上调用析构函数。相反,它在程序退出之前被调用。
静态变量的class的特殊性在于,在构造函数中,有一个模板函数get(全局范围)的调用returns 局部 static 变量。
我能够使用以下链接到共享库的 cpp 文件重现此行为:
#include <iostream>
template <typename T> // In my actual code, i is of type T, however, this has no effect
int get()
{
static int i = 0;
return i;
}
class Dictionary {
public:
Dictionary()
{
std::cout << "Calling Constructor" << std::endl;
get<int>();
}
~Dictionary(){
std::cout << "Calling Destructor" << std::endl;
}
private:
Dictionary(const Dictionary&);
Dictionary& operator=(const Dictionary&);
};
static Dictionary d;
我调查了可以进行的调整,以便在 dlclose 上调用析构函数,并得出以下结论:
- 如果函数 get 没有模板化
- 否则如果函数 get 中的变量 i 不是静态的
- 否则如果函数 get 是静态的
主程序代码如下:
#include <dlfcn.h>
#include <cassert>
#include <string>
#include <iostream>
void* LoadLib(std::string name)
{
void* libInstance;
name = "lib" + name + ".so";
libInstance = dlopen(name.c_str(), RTLD_NOW);
if ( ! libInstance ) std::cout << "Loading of dictionary library failed. Reason: " << dlerror() << std::endl;
return libInstance;
}
bool UnloadLib(void* libInstance)
{
int ret = dlclose(libInstance);
if (ret == -1)
{
std::cout << "Unloading of dictionary library failed. Reason: " << dlerror() << std::endl;
return false;
}
return true;
}
int main()
{
void* instance = LoadLib("dll");
assert(instance != 0);
assert(UnloadLib(instance));
std::cout << "DLL unloaded" << std::endl;
}
我使用以下命令构建了二进制文件:
g++ -m64 -g -std=c++11 -shared -fPIC dll.cpp -o libdll.so
g++ -m64 -g -std=c++11 -ldl main.cpp -o main.out
在程序退出前调用析构函数时得到的输出如下:
Calling Constructor
DLL unloaded
Calling Destructor
在 dlclose 上调用析构函数时得到的输出如下:
Calling Constructor
Calling Destructor
DLL unloaded
问题:
- 如果 GCC 版本之间的行为变化不是错误,您能否解释一下为什么不在 dlclose 上调用析构函数?
- 您能否解释一下每个调整:为什么在这种情况下会调用 dlclose 的析构函数?
不能保证卸载(调用析构函数)发生在 dlclose 上。在 musl(与 glibc 相反)上,构造函数仅在库第一次 运行 时 运行,而析构函数仅在退出时 运行。对于 portable 代码,不能假定 dlclose 会立即卸载符号。
卸载行为取决于动态链接时 glibc 的符号绑定,与 GCC 无关。
静态变量 get::i
具有 STB_GNU_UNIQUE
绑定。对于内联函数中的静态变量,对象的唯一性由 ELF 链接器保证。但是,对于动态加载,动态链接器通过标记符号 STB_GNU_UNIQUE
来确保唯一性。因此,通过其他代码尝试 dlopen 同一个共享库的另一次尝试将查找该符号并发现它是唯一的,并且 return 来自唯一符号 table 的现有符号。无法卸载具有唯一绑定的符号。
如果不需要,可以使用 -fno-gnu-unique
禁用唯一绑定。
参考资料
在主程序中,我 dlopen
和 dlclose
(分别为 LoadLibrary
和 FreeLibrary
)一个共享库。共享库包含一个静态变量,该变量在 dlopen
时实例化,并在 dlclose
时销毁。此行为在 MSVC 2008 和 2013、GCC 3.4.6 和 Sunstudio 12.1 上是一致的。
然而,在 GCC 4.9.1 和 GCC 5.2.1 中,不再在 dlclose
上调用析构函数。相反,它在程序退出之前被调用。
静态变量的class的特殊性在于,在构造函数中,有一个模板函数get(全局范围)的调用returns 局部 static 变量。
我能够使用以下链接到共享库的 cpp 文件重现此行为:
#include <iostream>
template <typename T> // In my actual code, i is of type T, however, this has no effect
int get()
{
static int i = 0;
return i;
}
class Dictionary {
public:
Dictionary()
{
std::cout << "Calling Constructor" << std::endl;
get<int>();
}
~Dictionary(){
std::cout << "Calling Destructor" << std::endl;
}
private:
Dictionary(const Dictionary&);
Dictionary& operator=(const Dictionary&);
};
static Dictionary d;
我调查了可以进行的调整,以便在 dlclose 上调用析构函数,并得出以下结论:
- 如果函数 get 没有模板化
- 否则如果函数 get 中的变量 i 不是静态的
- 否则如果函数 get 是静态的
主程序代码如下:
#include <dlfcn.h>
#include <cassert>
#include <string>
#include <iostream>
void* LoadLib(std::string name)
{
void* libInstance;
name = "lib" + name + ".so";
libInstance = dlopen(name.c_str(), RTLD_NOW);
if ( ! libInstance ) std::cout << "Loading of dictionary library failed. Reason: " << dlerror() << std::endl;
return libInstance;
}
bool UnloadLib(void* libInstance)
{
int ret = dlclose(libInstance);
if (ret == -1)
{
std::cout << "Unloading of dictionary library failed. Reason: " << dlerror() << std::endl;
return false;
}
return true;
}
int main()
{
void* instance = LoadLib("dll");
assert(instance != 0);
assert(UnloadLib(instance));
std::cout << "DLL unloaded" << std::endl;
}
我使用以下命令构建了二进制文件:
g++ -m64 -g -std=c++11 -shared -fPIC dll.cpp -o libdll.so
g++ -m64 -g -std=c++11 -ldl main.cpp -o main.out
在程序退出前调用析构函数时得到的输出如下:
Calling Constructor
DLL unloaded
Calling Destructor
在 dlclose 上调用析构函数时得到的输出如下:
Calling Constructor
Calling Destructor
DLL unloaded
问题:
- 如果 GCC 版本之间的行为变化不是错误,您能否解释一下为什么不在 dlclose 上调用析构函数?
- 您能否解释一下每个调整:为什么在这种情况下会调用 dlclose 的析构函数?
不能保证卸载(调用析构函数)发生在 dlclose 上。在 musl(与 glibc 相反)上,构造函数仅在库第一次 运行 时 运行,而析构函数仅在退出时 运行。对于 portable 代码,不能假定 dlclose 会立即卸载符号。
卸载行为取决于动态链接时 glibc 的符号绑定,与 GCC 无关。
静态变量 get::i
具有 STB_GNU_UNIQUE
绑定。对于内联函数中的静态变量,对象的唯一性由 ELF 链接器保证。但是,对于动态加载,动态链接器通过标记符号 STB_GNU_UNIQUE
来确保唯一性。因此,通过其他代码尝试 dlopen 同一个共享库的另一次尝试将查找该符号并发现它是唯一的,并且 return 来自唯一符号 table 的现有符号。无法卸载具有唯一绑定的符号。
如果不需要,可以使用 -fno-gnu-unique
禁用唯一绑定。
参考资料