• 诡异的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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    首先来说该项目会产生一个可执行程序和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)
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    // 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

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

    // factory.h
    
    template
    struct Factory {
    
        static Factory& instance() {
            static Factory f;
            return f;
        }
    
        T t{};
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    // file1.cpp
    
    void runFile1() {
        File1Sub sub;
        sub.run();
    
        std::cout << "addr:" << &(Factory::instance().t) 
                    << ", value:" << Factory::instance().t << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    file1中首先会去调用File1Sub的run函数,然后打印Factory的成员的值和地址。
    其实file2中也是做类似的事情:

    // file2.cpp
    
    void runFile2() {
        File2Sub sub;
        sub.run();
    
        std::cout << "addr:" << &(Factory::instance().t) 
        << ", value:" << Factory::instance().t << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    // factory_register.h
    
    struct FactoryRegister
    {
        FactoryRegister(int val) {
            Factory::instance().t = val;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    继续看下file1_sub和file2_sub

    // file1_sub.cpp
    void File1Sub::run() {
        FactoryRegister r(12);
    }
    
    // file2_sub.cpp
    void File2Sub::run() {
        FactoryRegister r(22);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

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

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

    # 编译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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    然后我们运行main试试:

    # ./main
    addr:0x7f04b67cf06c, value:12
    addr:0x7f04b67cf06c, value:22
    
    • 1
    • 2
    • 3

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

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

    # ./main
    addr:111cf37048, value:12
    addr:111cf5d048, value:0
    
    • 1
    • 2
    • 3

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

    问题分析

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

    # nm -C libfile1.so | grep "Factory"
    000000000020106c u Factory::instance()::f
    
    # nm -C libfile2.so | grep "Factory"
    000000000020106c u Factory::instance()::f
    
    # nm -C libfile1_sub.so | grep "Factory"
    0000000000000914 W FactoryRegister::FactoryRegister(int)
    000000000020104c u Factory::instance()::f
    
    # nm -C libfile2_sub.so | grep "Factory"
    0000000000000914 W FactoryRegister::FactoryRegister(int)
    000000000020104c u Factory::instance()::f
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我只罗列了关键的信息,可以看到factory的单例对象在四个库中都有,FactoryRegister::FactoryRegister构造函数就只有在file_sub库中有。
    然后我观测qnx编译的库也是类似的。

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

    # qnx
    addr:111cf37048, value:12
    addr:111cf5d048, value:22
    
    • 1
    • 2
    • 3

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

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

    到这里我们其实也应该知道了大致的原因了,就是因为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

  • 相关阅读:
    MASA MAUI Plugin (五)Android 指纹识别
    2022年最新辽宁建筑安全员模拟题库及答案
    【学习】应急响应
    ExtJS - ExtJS最佳实践
    C++特性——引用与指针详解
    wish测评自养号,卖家怎么快速出单?
    浅谈安科瑞无线测温产品在埃及某房建配电项目中的应用
    Python大数据之PySpark(八)SparkCore加强
    spark jdbc操作
    Linux 简单命令
  • 原文地址:https://blog.csdn.net/leapmotion/article/details/134411472