诡异的bug之dlopen

本文给大家分享一个比较诡异的bug,是关于dlopen的,我大致罗列了我项目中使用代码方式及结构,更好的复现这个问题,也帮助大家进一步理解dlopen.

问题复现

以下是项目代码的文件结构:

# tree
.
├── file1
│   ├── file1.cpp
│   └── file1_sub
│       ├── file1_sub.cpp
│       └── file1_sub.h
├── file2
│   ├── file2.cpp
│   └── file2_sub
│       ├── file2_sub.cpp
│       └── file2_sub.h
├── include
│   ├── factory.h
│   └── factory_register.h
└── main.cpp

首先来说该项目会产生一个可执行程序和4个库:

main.cpp  -> main(可执行程序)
file1_sub.cpp -> libfile1_sub.so
file1.cpp -> libfile1.so(依赖libfile1_sub.so)
file2_sub.cpp -> libfile2_sub.so
file2.cpp -> libfile2.so(依赖libfile2_sub.so)

代码比较简单,仅仅是main函数中打开libfile1.so和libfile12.so两个so库,并调用相应的函数runFile1和runFile2,我保证这里是最复杂的代码了:)

c++ 复制代码
// main.cpp

typedef void (*Func)();

int main() {
    void *handler1 = dlopen("./libfile1.so", RTLD_LAZY | RTLD_GLOBAL);
    if (handler1 == NULL) {
        printf("ERROR:%s :dlopen1\n", dlerror());
        return -1;
    }

    Func file1Func = (Func) dlsym(handler1, "_Z8runFile1v");
    if (file1Func == NULL) {
        printf("ERROR:%s :dlsym1\n", dlerror());
        return -1;
    }

    void *handler2 = dlopen("./libfile2.so", RTLD_LAZY | RTLD_GLOBAL);
    if (handler2 == NULL) {
        printf("ERROR:%s :dlopen2\n", dlerror());
        return -1;
    }

    Func file2Func = (Func) dlsym(handler2, "_Z8runFile2v");
    if (file2Func == NULL) {
        printf("ERROR:%s :dlsym2\n", dlerror());
        return -1;
    }

    file1Func();
    file2Func();

    for (;;) {}
    return 0;
}

然后再继续看file1和file2中分别做了什么, 因为file1和file2都会用到factory这个,那就先来看下factory.h

c++ 复制代码
// factory.h

template<typename T>
struct Factory {

    static Factory& instance() {
        static Factory f;
        return f;
    }

    T t{};
};

一个很简单模板类,就一个T的成员。比较关键的是,这个提供了单例对象。而后我们都会使用这个单例对象。

c++ 复制代码
// file1.cpp

void runFile1() {
    File1Sub sub;
    sub.run();

    std::cout << "addr:" << &(Factory<int>::instance().t) 
                << ", value:" << Factory<int>::instance().t << std::endl;
}

file1中首先会去调用File1Sub的run函数,然后打印Factory的成员的值和地址。

其实file2中也是做类似的事情:

c++ 复制代码
// file2.cpp

void runFile2() {
    File2Sub sub;
    sub.run();

    std::cout << "addr:" << &(Factory<int>::instance().t) 
    << ", value:" << Factory<int>::instance().t << std::endl;
}

然后我们再来看下file1_sub和file2_sub的run做了什么事情,在这之前还扔需要看下factory_register文件,因为这两个类会用到:

c++ 复制代码
// factory_register.h

struct FactoryRegister
{
    FactoryRegister(int val) {
        Factory<int>::instance().t = val;
    }
};

FactoryRegister仅仅就是在构造函数中调用一下Factory并给其成员赋值。

继续看下file1_sub和file2_sub

c++ 复制代码
// file1_sub.cpp
void File1Sub::run() {
    FactoryRegister r(12);
}

// file2_sub.cpp
void File2Sub::run() {
    FactoryRegister r(22);
}

最简单的语言来说就是,file_sub来设定单例的值,file来获取单例的值。

这里看到file_sub1,file_sub2,file1,file2使用的是同一个单例对象。不过稍微绕一点的是使用factory_register来赋值,这在实际项目中也是会遇到的,假如你想在main函数之前就将factory注册成功呢,就需要一个static或者全局变量来操作factory,这里就是提供了factory_register这个实现。

我们使用如下指令来编译:

shell 复制代码
# 编译main,dlopen需要用到dl库
g++ main.cpp -ldl -o main

# 编译file_sub库
g++ file1/file1_sub/file1_sub.cpp -fPIC -shared -o libfile1_sub.so
g++ file2/file2_sub/file2_sub.cpp -fPIC -shared -o libfile2_sub.so

# 编译file库(需要依赖file_sub库)
g++ file1/file1.cpp -fPIC -shared -L. -lfile1_sub -o libfile1.so
g++ file2/file2.cpp -fPIC -shared -L. -lfile2_sub -o libfile2.so

然后我们运行main试试:

c++ 复制代码
# ./main
addr:0x7f04b67cf06c, value:12
addr:0x7f04b67cf06c, value:22

一切完美,都是相同的变量地址,值也设定成功了。

不过我的问题也不是出现在这里,项目中使用qnx,我们编译完运行的结果却是这样的:

c++ 复制代码
# ./main
addr:111cf37048, value:12
addr:111cf5d048, value:0

是不是很意外,地址不一样也就算了,关键的值还没有赋值成功,太诡异了。

问题分析

我们先在linux上分析一波,我们猜想问题肯定出现在factory和factory_register这两个文件,我们在各个库上看下这两个符号:

c++ 复制代码
# nm -C libfile1.so | grep "Factory"
000000000020106c u Factory<int>::instance()::f

# nm -C libfile2.so | grep "Factory"
000000000020106c u Factory<int>::instance()::f

# nm -C libfile1_sub.so | grep "Factory"
0000000000000914 W FactoryRegister::FactoryRegister(int)
000000000020104c u Factory<int>::instance()::f

# nm -C libfile2_sub.so | grep "Factory"
0000000000000914 W FactoryRegister::FactoryRegister(int)
000000000020104c u Factory<int>::instance()::f

我只罗列了关键的信息,可以看到factory的单例对象在四个库中都有,FactoryRegister::FactoryRegister构造函数就只有在file_sub库中有。

然后我观测qnx编译的库也是类似的。

那我们也先不要FactoryRegister,赋值的地方直接调用Factory的instance来设定,看下运行结果:

# qnx
addr:111cf37048, value:12
addr:111cf5d048, value:22

虽然地址不一样,但是值确实是赋值成功了,也可以达到预期。

这里其实到了一个相对盲区的地方,一般来说我们其实是动态库之间不应该出现相同符号的。

到这里我们其实也应该知道了大致的原因了,就是因为qnx和linux上使用dlopen时针对同名符号解析是不同的。

通过在qnx上符号的地址查看,可以得出下图:

libfile1.so对factory的引用都是在自己所在的so中,libfile1_sub.so对factory的引用是在libfile1.so,但是libfile2_sub.so对FactoryRegister引用需要到libfile1_sub.so中,进一步到libfile1.so中对factory设定。

所以在libfile.so中获取的factory的地址是不一样的。而libfile2.so对factory成员值的获取是0。

关于dlopen

我们使用的dlopen的mode是RTLD_LAZY | RTLD_GLOBAL

  • RTLD_LAZY表示该库函数符号会延迟到使用调用时采取解析重定位等,与之相反的是RTLD_NOW。
  • RTLD_GLOBAL表示该库中的符号会加入到全局符号表中,以便于后边使用dlopen的库使用。与之相反的是RTLD_LOCAL表示该库的符号仅给该组中库使用。(这里的组表示该库及随之一起加载的依赖的库)

由上边的排查,我们知道实际上是由于dlopen函数对相同符号解析位置的设定导致这个问题的出现,我们打开qnx的官方文档对于dlopen符号查找位置顺序解释:

  1. 加载的动态库
  2. LD_PRELOAD环境变量指定ELF文件(这里我们没有用到)
  3. 全局列表
  4. 加载的动态库所依赖的动态库

那我们再回来看下各个符号的查找细节:

  • file1中factory的符号在本库是有的,对factory引用就直接到本库中找就行的。
  • file1_sub中FactoryRegister放到全局列表中,file1_sub中FactoryRegister对factory的引用就会优先到file1中查找。
  • file2的factory也是定位到本库的
  • file2_sub对FactoryRegister引用就会先到file2中查找,但是file2中是没有的,然后回去全局列表中查找,就找到了file1_sub中。

所以对file2就不会拿到预期的值,去掉FactoryRegister的引用就可以到预期了。

总结

本文我们从例子中看出来dlopen的解析符号的位置和顺序会影响程序的正确性。

我们大致总结三点:

  1. dlopen等函数不仅仅是依赖于运行时库还依赖操作系统,不同操作系统上表现可能不一样
  2. 尽量不要多个不同的ELF文件含有相同的符号,比如这个例子中我们就可以让单独一个so库对factory及factory_register进行封装,大家都使用这个库保证符号的单一性。
  3. 排查问题时可以使用readelf,nm,pmap等指令查看elf文件中的符号,以及运行时符号所在的位置等

ref

http://www.qnx.com/developers/docs/qnxcar2/index.jsp?topic=%2Fcom.qnx.doc.neutrino.sys_arch%2Ftopic%2Fdll_SYMBOLNAME.html

相关推荐
生命几十年3万天10 小时前
通宵修bug
bug
LilKevinRay10 小时前
【SpringMVC】记录一次Bug——mvc:resources设置静态资源不过滤导致WEB-INF下的资源无法访问
java·笔记·mvc·bug
会发光的猪。11 小时前
前端vue3若依框架pnpm run dev启动报错
前端·javascript·vue.js·前端框架·bug
小汤猿人类2 天前
苍穹外卖Bug集合
bug
且行且知3 天前
CubeIDE BUG-project‘hello‘has no explict encoding set hello
bug
一条破秋裤3 天前
针对解决前后端BUG的个人笔记
笔记·bug
马浩同学3 天前
【ESP32】ESP-IDF开发 | I2C从机接收i2c_slave_receive函数的BUG导致程序崩溃解决(idf-v5.3.1版本)
c语言·单片机·嵌入式硬件·mcu·bug
偏振万花筒4 天前
【BUG分析】clickhouse表final成功,但存在数据未合并
clickhouse·bug
神米米4 天前
Unity3D包管理bug某些版本Fbx Exporter插件无法搜索到的问题
bug
行者记4 天前
Qt小bug —— QTableWidget排序后更新数据显示不全
开发语言·qt·bug