Linux Dynamic Library (.so) 使用指南

作者: 韩炳涛 | 来源:发表于2016-07-08 11:53 被阅读2610次

    1. Dynamic Library的编译

    假设我们有下面两个文件a.h, a.cpp,放在同一目录下。两个文件的内容分别是:

    // a.h
    
    extern "C" void foo();
    
    // a.cpp
     
    #include <iostream>
    #include "a.h"
     
    using namespace std;
     
    extern "C" void foo() {
        cout << "a.foo" << endl;
    }
    

    使用下面的命令行可以产生liba.so动态链接库:

    g++ -fPIC -c a.cpp
    g++ -shared -o liba.so a.o

    上面第一行的-fPIC是要求编译器生成位置无关代码(Position Independent Code),这对于动态库来说是必须的。关于位置无关代码的细节,可以查看后面列出的参考文献,不再赘述。第二行使用-shared要求编译器生成动态库,而不是一个可执行文件。

    另外,我们声明和定义foo函数时使用了extern "C",这是希望c++编译器不要对函数名进行改名(mangle)。对于共享库来说,这样定义接口函数更容易在Dynamic Loading时使用。至于什么是Dynamic Loading,在2.2节描述。

    2. 动态库的使用

    2.1 Dynamic Linking方式

    Dynamic Linking方式,是指在链接生成可执行文件时,通过-l指定要连接的共享库,这种方式和使用静态库非常相似。

    假设我们有一个main_dyn_link.cpp文件,内容如下:

    // main_dyn_link.cpp
     
    #include "a.h"
     
    int main(int argc, char *argv[]) {
        foo();
        return 0;
    }
    

    我们可以使用下面的命令,将和其liba.so一起编译链接为可执行文件test:

    g++ main_dyn_link.cpp -o test -L`pwd` -la

    当我们运行这个test程序时,会报错,因为系统找不到liba.so文件。默认情况下,系统只会在/usr/lib、/usr/local/lib目录下查找.so文件。为了能够让系统找到我们的liba.so,我们要么把liba.so放到上述两个目录中,要么使用LD_LIBRARY_PATH环境变量将liba.so所在的目录添加为.so搜索目录。这里我们使用第二种方法,在命令行输入:

    export LD_LIBRARY_PATH=`pwd`

    这时,程序就能正常运行了。
    此外还有其他方法能够让系统找到liba.so,可以查看下面的参考文档1,不再赘述。

    2.2 Dynamic Loading方式

    使用dlopen、dlsym等函数,我们可以在运行期加载任意一个共享库。我们把前面的main.cpp改为使用Dynamic Loading的方式:

    // main_dyn_load.cpp
     
    #include <dlfcn.h>
    #include <iostream>
    #include "a.h"
     
    using namespace std;
     
    typedef void (*Foo)();
     
    Foo get_foo() {
        void *lib_handle = dlopen("liba.so", RTLD_LAZY);
        if (!lib_handle) {
            cerr << "load liba.so failed (" << dlerror() << ")" << endl;
            return 0;
        }
     
        char *error = 0;
        Foo foo_a = (Foo) dlsym(lib_handle, "foo");
        if ((error = dlerror()) != NULL) {
            cerr << "get foo failed (" << error << ")" << endl;
            return 0
        }
     
        return foo_a;
    }
     
    int main(int argc, char *argv[]) {
        Foo foo_a = get_foo();
        foo_a();
        return 0;
    }
    

    首先,为了使用dlopen、dlsym、dlerror等函数,我们需要包含dlfcn.h头文件。

    第12行,我们使用dlopen函数,传递liba.so的路径名(本例是当前目录),系统会尝试加载liba.so。如果成功,返回给我们一个句柄。RTLD_LAZY是说加载时不处理unresolved symbols。对于本例,就是加载liba.so时,不会去查找foo的地址,只有在第一次调用foo时才会去找foo的实际地址。需要了解进一步详细信息可以查找手册(命令行输入:man dlopen)。

    第19行,我们使用dlsym函数,传递dlopen返回的句柄和我们想要获取的函数名称。如果这个
    名称是存在的,dlsym会返回其相应的地址。这就是为什么我们需要把.so的接口函数声明为extern "C",否则,我们就必须给dlsym传递经过c++编译器mingle之后的奇怪名字,才能找到相应的函数。

    出现任何错误的时候,dlerror会返回相应的错误信息字符串;否则它会返回一个空指针。dlerror提供的信息对我们定位问题是非常有帮助的。

    一旦获取了函数地址,我们可以把它保存在函数指针中(第29行),随后就可以像使用函数一样来使用它(第30行)。

    接着,我们编译main.cpp,并生成可执行文件:

    g++ main_dyn_load.cpp -o test -ldl

    因为我们使用的是Dynamic Loading,因此就不需要在编译时链接liba.so了(去掉了-la),因为我们使用了dlxxx函数,所以需要增加链接-ldl。

    3. 使用Dynamic Library的注意事项

    Dynamic Library使用要比Static Library复杂,下面是一些需要注意的问题。

    3.1 不同的.so内包含同名全局函数

    3.1.1 Dynamic Linking

    .so允许出现同名的强符号。因此,如果不同的.so包含同名的全局函数,链接时编译器不会报错。编译器会使用命令行中先链接的那个库的版本。例如,我们再增加一个b.cpp文件:

    // b.cpp
      
    #include <iostream>
    #include "a.h"
      
    using namespace std;
      
    extern "C" void foo() {
        cout << "b.foo" << endl;
    }
    

    将其编译、生成为libb.so:

    g++ -fPIC -c b.cpp
    g++ main_dyn_link.cpp -o test -shared -L`pwd` -la -lb

    这时,test将使用liba.so版本的foo,也就是将打印a.foo。如果我们把上面第二行的-la -lb倒过来:

    g++ main_dyn_link.cpp -o test -shared -L`pwd` -lb -la

    这时,test将使用libb.so版本的foo,也就是将打印b.foo。
    这个不会成为太大的问题,因为使用静态库也是这样的。

    3.1.2 Dynamic Loading

    使用Dynamic Loading,我们可以从两个.so中分别取出不同的版本,并按照自己的意图来使用。我们修改一下main_dyn_load.cpp文件,使之使用两个foo版本:

    // main_dyn_load.cpp
     
    #include <dlfcn.h>
    #include <iostream>
    #include "a.h"
     
    using namespace std;
      
    typedef void (*Foo)();
     
    Foo get_foo(const char *lib_path) {
        void *lib_handle = dlopen(lib_path, RTLD_LAZY);
        if (!lib_handle) {
            cerr << "load liba.so failed (" << dlerror() << ")" << endl;
            return 0;
        }
      
        char *error = 0;
        Foo foo_a = (Foo) dlsym(lib_handle, "foo");
        if ((error = dlerror()) != NULL) {
            cerr << "get foo failed (" << error << ")" << endl;
            return 0;
        }
      
        return foo_a;
    }
     
    int main(int argc, char *argv[]) {
        Foo foo_a = get_foo("liba.so");
        Foo foo_b = get_foo("libb.so");
        foo_a();
        foo_b();
        return 0;
    }
    

    首先,稍微重构了一下get_foo函数,使之能够接收一个.so路径作为参数,然后它回取出相应.so里面的foo函数的地址。

    第29和第30行,我们分别从liba.so和libb.so中取出了foo函数地址,将他们保存在foo_a和foo_b两个函数指针中,并在第31和第32行分别进行了调用。

    最后,程序将会打印a.foo和b.foo。

    3.2 .so反向调用bin里面的函数

    bin可以调用.so定义的函数,以及.so可以调用其它.so定义的函数,这是毫无疑问的。那么,.so能反过来调用bin里面的函数么?答案是肯定的,只要我们在编译bin时制定-rdynamic选项就可以了。

    我们只举Dynamic Linking的例子,因为Dynamic Loading也是一样的。

    我们在main_dyn_linking里面定义一个新的函数bar:

    // main_dyn_link.cpp
     
    #include <iostream>
    #include "a.h"
     
    using namespace std;
    extern "C" void bar() {
        cout << "main.bar" << endl;
    }
     
    int main(int argc, char *argv[]) {
        foo();
        return 0;
    }
    

    然后,我们在a.cpp里面调用这个函数:

    // a.cpp
      
    #include <iostream>
    #include "a.h"
      
    using namespace std;
     
    extern "C" void bar();
    extern "C" void foo() {
        cout << "a.foo" << endl;
        bar();
    }
    

    编译,注意增加-rdynamic选项:

    g++ -fPIC -c a.cpp
    g++ -shared -o liba.so a.o
    g++ main_dyn_link.cpp -o test -L`pwd` -la -rdynamic

    执行程序,将会打印:
    a.foo main.bar

    3.3 不同的.so内出现同名的全局变量

    终于要面对这个非常tricky的场景了。这里说的全局变量,既包括通常意义的『全局变量』,也包括类的静态成员变量,因为后者本质上就是改了名字全局变量。

    3.3.1 Dynamic Linking

    我们先来考虑Dynamic Linking的情况。我首先添加一个类:MyClass,并把它实现为singleton。因为singleton模式是使用类静态成员最常见的场景之一。
    先来定义MyClass的头文件:

    // my_class.h
     
    class MyClass {
    public:
        MyClass();
        ~MyClass();
        void what();
        static MyClass &get_instance();
    private:
        int _count;
        static MyClass _instance;
    };
    

    接着定义MyClass的源文件:

    // my_class.cpp
     
    #include <iostream>
    #include "my_class.h"
     
    using namespace std;
     
    MyClass MyClass::_instance;
     
    MyClass::MyClass()
        : _count(0) {
        cout << "the count init to 0" << endl;
    }
     
    MyClass::~MyClass() {
        cout << "(" << this << ") destory" << endl;
    }
     
    void MyClass::what() {
        _count++;
        cout << "(" << this << ") the count is " << _count << endl;
    }
     
    MyClass &MyClass::get_instance() {
        return _instance;
    }
    

    每次调用what方法,MyClass对象内部计数会加1,并随后打印对象的地址和当前的计数值。
    我们在a.cpp和b.cpp里面分别调用MyClass::what方法。

    // a.cpp
      
    #include <iostream>
    #include "a.h"
    #include "my_class.h"
      
    using namespace std;
      
    extern "C" void bar();
    extern "C" void foo() {
        cout << "a.foo" << endl;
        bar();
        MyClass::get_instance().what();
    }
    

    我们需要把my_class.cpp编译到liba.so和libb.so中:

    g++ -fPIC -c a.cpp
    g++ -fPIC -c my_class.cpp
    g++ -shared -o liba.so a.o my_class.o
     
    g++ -fPIC -c b.cpp
    g++ -shared -o libb.so b.o my_class.o
     
    g++ main_dyn_link.cpp -o test -L\`pwd\` -la -lb -rdynamic
    

    执行这个程序,我们发现,尽管在不同的.so内都包含了my_class.cpp(里面定义了_instance静态静态变量),但最终全局只有一个_instance实例。但是,这个实例被初始化了两次和析构了两次。重复析构可能会导致core,因此在.so场景下使用单例模式要更加小心(或选择其它的单例实现方法)。

    3.3.2 Dynamic Loading

    现在我们看看Dynamic Loading的情况。这次,我们使用main_dyn_load.cpp进行编译:

    g++ main_dyn_load.cpp -o test -ldl -rdynamic

    这次,我们惊讶的发现,居然存在两个不同的_instance实例!当然,重复初始化和析构不存在了,每个对象上都只进行了一次初始化和析构。

    这说明,在Dynamic Loading情况下,不同的.so中同名全局变量都会是不同的实例。

    等等,如果你以为这是全部真相那就错了。如果我们在bin中也定义同名的全局变量会怎么样呢?我们修改一下main_dyn_load.cpp中的bar函数,使之也调用MyClass::get_instance().what()方法:

    // main_dyn_load.cpp
     
    #include <dlfcn.h>
    #include <iostream>
    #include "a.h"
    #include "my_class.h"
    using namespace std;
      
    typedef void (*Foo)();
     
    extern "C" void bar() {
        cout << "main.bar" << endl;
        MyClass::get_instance().what();
    } 
    Foo get_foo(const char *lib_path) {
        void *lib_handle = dlopen(lib_path, RTLD_LAZY);
        if (!lib_handle) {
            cerr << "load liba.so failed (" << dlerror() << ")" << endl;
            return 0;
        }
      
        char *error = 0;
        Foo foo_a = (Foo) dlsym(lib_handle, "foo");
        if ((error = dlerror()) != NULL) {
            cerr << "get foo failed (" << error << ")" << endl;
            return 0;
        }
      
        return foo_a;
    }
    int main(int argc, char *argv[]) {
     
        Foo foo_a = get_foo("liba.so");
        Foo foo_b = get_foo("libb.so");
        foo_a();
        foo_b();
        return 0;
    }
    

    我们还需要把my_class.cpp也直接编译到bin里面,否则会找不到get_instance()、what()等符号。

    g++ main_dyn_load.cpp my_class.o -o test -ldl -rdynamic

    执行程序,结果再次令人意外:

    全局变量再次合为一个,而且被重复初始化-析构了三次。

    总结上述规律,在Dynamic Loading场景下,如果.so中出现了同名全局变量,那么每个.so都会有其单独的全局变量实例,每个实例单独初始化/析构;如果bin中也包括同名的全局变量,那么系统将只有唯一一份实例,在这个实例上会出现多次重复的初始化/析构。

    这再次说明,在.so中使用全局变量(以及类的静态成员变量)要非常谨慎,整个系统也要形成统一的规范,否则很可能出现未预期的行为。

    3.4 dynamic_cast

    从一个.so中创建的对象,在另外一个.so中进行dynamic_cast,即使第二个.so完全编译了子类的定义,dynamic_cast也可能会失败。为了演示,先修改一下MyClass的定义:

    // my_class.h
     
    class MyBase {
    public:
        virtual ~MyBase() {}
    };
    class MyClass : public MyBase {
    public:
        MyClass(const char *name);
        ~MyClass();
        void what();
    private:
        int _count;
        const char *_name;
    };
    

    接着修改MyClass的实现:

    // my_class.cpp
      
    #include <iostream>
    #include "my_class.h"
      
    using namespace std;
      
    MyClass::MyClass(const char *name)
        : _count(0), _name(name) {
        cout << "the count init to 0" << endl;
    }
      
    MyClass::~MyClass() {
        cout << "(" << this << ") destory" << endl;
    }
      
    void MyClass::what() {
        _count++;
        cout << "(" << this << ") created in " << _name << ", the _count is " << count << endl;
    }
    

    为了能够让.so产生出MyClass对象,我们给.so增加一个接口函数:create。此外,我们把foo改为接收一个MyBase对象的指针。

    // a.h
     
    class MyBase;
    extern "C" void foo(MyBase*);
    extern "C" MyBase *create();
    

    在a.cpp和b.cpp中实现create函数。并且,在foo函数中使用dynamic_cast强制向下转型:

    // a.cpp
      
    #include <iostream>
    #include "a.h"
    #include "my_class.h"
      
    using namespace std;
      
    extern "C" void bar();
     
    extern "C" void foo(MyBase* base) {
        cout << "a.foo" << endl;
        bar();
        MyClass *cls = dynamic_cast<MyClass*>(base);
        if (!cls) {
            cerr << "dynamic_cast failed" << endl;
            return;
        }
        cls->what();    
    }
     
    extern "C" MyBase *create() {
        return new MyClass("liba.so");
    } 
    
    // b.cpp
      
    #include <iostream>
    #include "a.h"
    #include "my_class.h"
    using namespace std;
      
    extern "C" void foo(MyBase *base) {
        cout << "b.foo" << endl;
        MyClass *cls = dynamic_cast<MyClass*>(base);
        if (!cls) {
            cerr << "dynamic_cast failed" << endl;
            return;
        }
        cls->what();   
    }
     
    extern "C" MyBase *create() {
        return new MyClass("libb.so");
    }
    

    最后,修改main_dyn_load.cpp文件,使之从liba.so创建对象,再libb.so中转型、使用;然后反方向再来一次。

    #include <dlfcn.h>
    #include <iostream>
    #include "a.h"
    #include "my_class.h"
    #include "fn.h"
     
    using namespace std;
     
    typedef void (*Foo)(MyBase*);
    typedef MyBase *(*Create)();
     
    extern "C" void bar() {
        cout << "main.bar" << endl;
    }
     
    int main(int argc, char *argv[]) {
        Foo foo_a = get_fn<Foo>("liba.so", "foo");
        Foo foo_b = get_fn<Foo>("libb.so", "foo");
        Create create_a = get_fn<Create>("liba.so", "create");
        Create create_b = get_fn<Create>("libb.so", "create");
        MyBase *base_a = create_a();
        MyBase *base_b = create_b();
        foo_a(base_a);
        foo_b(base_b);
        foo_a(base_b);
        foo_b(base_a);
        return 0;
    }
    

    第17到第20行,使用工具函数get_fn从.so中获取函数地址,get_fn的源码在附件中。第21行和第22行分别在liba.so和libb.so中创建了对象。第23行,liba.so创建的对象在liba.so中转型,第24行同样测试了libb.so的情形。第25行和第26行测试了交叉转型的情况。

    编译:

    g++ -fPIC -c a.cpp
    g++ -fPIC -c my_class.cpp
    g++ -shared -o liba.so a.o my_class.o

    g++ -fPIC -c b.cpp
    ++ -shared -o libb.so b.o my_class.o

    g++ main_dyn_load.cpp -o test -L`pwd` -ldl -rdynamic

    程序运行结果如下:

    可以看到,出错的代码就是第25和第26行,说明在一个.so中创建的对象无法在另一个.so中转型成功。

    怎样解决这个问题呢?答案是把my_class.o也编译到bin里面。如下:

    g++ main_dyn_load.cpp my_class.o -o test -L`pwd` -ldl -rdynamic

    编译、运行,可以看到这次转型成功了:

    为什么会这样呢?这其实和3.3.2的情景是一样的:dynamic_cast时使用的类的虚函数表和RTTI元数据也是全局变量。当bin没有同名的全局变量时,各个.so拥有各自独立的虚函数表实例,导致转型时认为不是同一个继承体系而失败。而当bin也编译了同样的虚函数表时,所有的虚函数表就只会出现为同一个实例了。

    5. 总结

    .so带来了灵活性的同时,也使我们要面对很多tricky的场景,一不小心就可能落到坑里。因此,使用.so必须小心,只在安全的范围内应用,并且在整个系统要有统一的规范。如果在使用.so的过程中发现了任何问题,欢迎随时与作者交流。

    6. 参考资料

    1. Static, Shared Dynamic and Loadable Linux Libraries
    2. Program Library HOWTO Shared Libraries
    3. Shared libraries with GCC on Linux
    4. Anatomy of Linux dynamic libraries
    5. Resolving ELF Relocation Name / Symbols
    6. PLT and GOT - the key to code sharing and dynamic libraries
    7. Linkers and Loaders
    8. C++ dynamic_cast实现原理

    相关文章

      网友评论

        本文标题:Linux Dynamic Library (.so) 使用指南

        本文链接:https://www.haomeiwen.com/subject/jatrjttx.html