添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

动态库 静态库 混合使用下的单例模式bug

问题介绍

最近半年,项目里出现多次奇怪的 crash 。看 crash 堆栈都是进程退出的时候静态变量的销毁 core 掉了。从堆栈里看不出来哪里逻辑不对,从代码里也看不出来哪里改坏了这个变量,但是像 std::unordered_map 析构时候挂掉这种真是不知道怎么查。只能上 asan 去看是不是哪里把这个变量的内存写坏了,然而费时费力也没查出来什么。

最后没办法,二分回滚所有相关提交,最后定位到出问题的那次。可是看代码仍然看不出来哪里有问题,只好在这个提交的所有修改里进行二分注释来测试是否会引发关服 core ,最后定位到调用的一个库的一个接口就会引发 core 。既然定位到了问题,那我就将代码提前 return ,不再调用这个接口验证一下是否解决。 WTF ,即使这个函数不被执行到也会出现 core ,只有彻底把这个函数的调用注释掉才能彻底解决 core 的发生。幸亏这个库是我们自己写的,有源代码,于是对这个函数的源代码进行二分注释,发现这个函数内的一个静态变量被销毁了两次,从而引发了 core

静态单例

c++11 里,我们已经被教育了,单例模式直接上 static 的这个完美解决方案,保证多线程模式下单例数据只会初始化一次。

//sinleton_lib.h
#pragma once
extern int global_var;
class singleton_test
    singleton_test();
    singleton_test(const singleton_test& other) = delete;
public:
    static singleton_test& instance();
    ~singleton_test();
//sinleton_lib.cpp
#include "singleton_lib.h"
#include <iostream>
int global_var  = 1314;
singleton_test::singleton_test()
    std::cout<<"singleton_test create at "<<this<<std::endl;
singleton_test::~singleton_test()
    std::cout<<"singleton_test destroyed at "<<this<<std::endl;
singleton_test& singleton_test::instance()
    static singleton_test the_one;
    return the_one;

优雅,简单,易懂, perfect 。但是在下面我设计出来的情况里这个单例就不再是单例,而会出现多份数据。

多个动态库链接同一个静态库

  1. 首先我们把上面的代码封装为一个静态库
  2. 然后我们新建两个内容相同的动态库连接到这个静态库, 一个是 libdyn_test_1.so
#include "singleton_lib.h"
#include <iostream>
extern "C" void dyn_test()
    auto& cur_singleton = singleton_test::instance();
    std::cout<<"dyn_test_1 get singleton at "<<&cur_singleton<<std::endl;
    std::cout<<"dyn_test_1 get global var at "<<&global_var<<std::endl;

一个是 libdyn_test_2.so

#include "singleton_lib.h"
#include <iostream>
extern "C" void dyn_test()
    auto& cur_singleton = singleton_test::instance();
    std::cout<<"dyn_test_2 get singleton at "<<&cur_singleton<<std::endl;
    std::cout<<"dyn_test_2 get global var at "<<&global_var<<std::endl;
  1. 最后我们建立一个可执行文件 load_test ,连接到 singleton_test ,然后再运行时加载上面的两个动态库,执行对应的 dyn_test 接口,观察输出
//load_test.cpp
#include <iostream>
#include <dlfcn.h>
#include "singleton_lib.h"
void call_dyn_test(const std::string& lib_name, int load_flag)
    void (*foo)();
    void *handle = dlopen(lib_name.c_str(), load_flag);
    if(!handle)
        std::cout<<"cant open lib "<<lib_name<<std::endl;
        return;
    *(void **) (&foo) = dlsym(handle, "dyn_test");
    const char* error;
    if((error = dlerror()) != NULL)
        std::cout<<"fail to dlsym "<<error<<std::endl;
        return;
    (*foo)();
int main()
    int flag = RTLD_LOCAL|RTLD_NOW;
    const auto& the_one = singleton_test::instance();
    call_dyn_test("./libdyn_test_1.so", flag);
    call_dyn_test("./libdyn_test_2.so", flag);
    return 1;

最后的输出很是令人绝望:

singleton_test create at 0x564624be8159
singleton_test create at 0x7f6eca7fa091
dyn_test_1 get singleton at 0x7f6eca7fa091
dyn_test_1 get global var at 0x7f6eca7fa078
singleton_test create at 0x7f6eca7f5091
dyn_test_2 get singleton at 0x7f6eca7f5091
dyn_test_2 get global var at 0x7f6eca7f5078
singleton_test destroyed at 0x7f6eca7f5091
singleton_test destroyed at 0x7f6eca7fa091
singleton_test destroyed at 0x564624be8159

从这里的输出可以看出, load_test , libdyn_test_1 , libdyn_test_2 里对于全局变量 global_var 和静态单例 singleton_test 都各自拥有一份数据,分配在不同的地址上。这样导致了 singleton_test 单例被构造了三次,同时析构了三次。

多个静态单例实例带来的问题

如果这个单例对象一个接口是申请资源并设置内部指针,一个接口是释放资源并销毁内部指针。但是释放的调用和销毁的调用在不同的库里,逻辑不严谨直接 free 掉对应的数据指针,遇到上面的情况就崩掉了。 还有另外一种就是一个动态库里对静态库的全局变量做的修改,其他动态库是看不到的。例如上面的 global_var 变量, load_test , libdyn_test_1 , libdyn_test_2 任意一个进行的修改都无法在其他库里观察到,这样就会引发逻辑错误。

不同的动态库加载模式触发的不同问题

上面的不同动态库引用的静态库全局变量多实例的问题可以通过更换 dlopen 传入的 flag 来解决,我们把上面的 RTLD_LOCAL 切换为 RTLD_GLOBAL ,输出就不一样了:

singleton_test create at 0x564b6ee46159
singleton_test create at 0x7fcd07dd5091
dyn_test_1 get singleton at 0x7fcd07dd5091
dyn_test_1 get global var at 0x7fcd07dd5078
dyn_test_2 get singleton at 0x7fcd07dd5091
dyn_test_2 get global var at 0x7fcd07dd5078
singleton_test destroyed at 0x7fcd07dd5091
singleton_test destroyed at 0x564b6ee46159

可以看出,两个动态库里面引用的静态库的变量都指向了同一个地址。不过 load_test 里的地址还是跟动态库里不一样,前面我们提到的问题仍然还存在。

然而 RTLD_GLOBAL 只是解决了同一个地址的问题,静态库里全局变量数据还是会进行多次构造和析构。我们将 global_var 的类型替换一个非 pod 类型

//singleton_lib.h
struct raii_test
    int i = 0;
    raii_test();
    ~raii_test();
extern raii_test global_var;
//singleton_lib.cpp
raii_test::raii_test()
    std::cout<<"raii_test ctor at "<<this<<std::endl;
raii_test::~raii_test()
    std::cout<<"raii_test dtor at "<<this<<std::endl;
raii_test global_var;

重新编译执行一下程序,下面是输出:

raii_test ctor at 0x56113df4b158
singleton_test create at 0x56113df4b15d
raii_test ctor at 0x7fe5ac87e090
singleton_test create at 0x7fe5ac87e095
dyn_test_1 get singleton at 0x7fe5ac87e095
dyn_test_1 get global var at 0x7fe5ac87e090
raii_test ctor at 0x7fe5ac87e090
dyn_test_2 get singleton at 0x7fe5ac87e095