假设我有以下场景:
一个静态库(我们称之为 DummyStatic.a),声明并定义以下类:
class DummySingleton {
private:
static DummySingleton the_instance_;
public:
DummySingleton& instance() noexcept; // defined in source file
};
所以这个类在DummySingleton类内部的头文件中声明了the_instance_,并在源文件中定义了它。 到目前为止一切都很好,但假设我有以下设置:
shared_lib_a.so和shared_lib_b.so都静态链接DummyStatic.a
static_lib_x.a正在链接shared_lib_a.so和shared_lib_b.so
DummyExecutable 是一个链接 static_lib_x.a 的可执行文件(不会调用任何函数,它只包含空 int main() {..} )
如果我按照上面描述的方式运行此设置,ASAN 会在 the_instance_ 变量上向我发出 ODR 违规警告,如果我声明一个 在 DummySingleton 中使用 static std::string 并在源文件中定义它,我会因双重释放错误而崩溃。
现在,如果我去掉静态 DummySingleton the_instance_ 并使其内联(C++ 17 内联)在源文件内 或者我在源文件中定义静态,如下所示:
DummySingleton.cpp:
static DummySingleton ds{};
DummySingleton& get_ds() {
return ds;
}
一切看起来都很好,ASAN 也没有抱怨。
我不明白这里发生了什么,有人可以向我解释一下吗?
是否因为包含我的类定义的标头的每个共享库都会尝试定义静态成员,但第二种方法(文件静态)不会导致此问题,因为该变量是定义它的翻译单元的内部?并且它没有在标头中公开,也不会在共享库之间复制?
在代码的原始变体中,每个静态库将定义类静态变量以及调用该变量的构造函数和析构函数的两段代码。这些代码分别在库加载和卸载时运行。
现在,由于共享库默认的工作方式,这些代码片段将构造和析构首先加载变量的任何实例,而不是位于同一库中的实例。
这是详细过程。
假设首先加载library1.so,并将其符号添加到可执行文件的全局符号表中。该库的初始化代码想要初始化 DummySingleton 的类静态变量,假设您定义了一个
std::string DummySingleton::s
。为了做到这一点,代码使用全局符号表解析 DummySingleton::s
,一切都很好。
现在library2.so已加载。它还要初始化
DummySingleton::s
,也去全局;符号表。但它在那里找到了library1.so的DummySingleton::s
,因为library1.so是先加载的。所以library2.so再次初始化同一个变量,导致内存泄漏。位于 Library2.so 中的 DummySingleton::s
实例从未被看到或触及过。
更糟糕的是,当卸载库时,它们会出于同样的原因尝试两次销毁
DummySingleton::s
的同一个实例,从而导致双重释放。
现在,当您声明变量 file-static 或 function-static 时,它会获得一个隐藏的 guard 变量,以防止其被初始化两次。与全局表相同的过程再次发生,但是这次初始化代码首先检查保护变量,如果它被设置,则不会发生第二次初始化。
从技术上讲,这仍然是 ODR 违规,因为访问静态变量的 function 被定义了两次。但这种违规行为是良性的,不会触发 ASAN。