linux 动态库插件技术(c/c++):动态链接库

概述

插件技术的目的是为了更好的扩展性.动态链接库是其中 一种实现方式.

这里主要论述几个问题.

1)linux上关于这些api的描述.看完linux上关于dlopen等函数的描述基本就可以写出简单的动态链接库使用.

2)关于c++使用动态链接库的一些问题和注意事项.

3)扩展,编译器的各选项,动态链接库和静态链接库.


linux api:dlopen,dlsym,dlerror,dlclose

摘自ubuntu kylin 14.04,内核3.13.0-32generic

#include<dlfcn.h>
void *dlopen(const char *filename,int flag);
char *dlerror(void);
void *dlsym(void *handle,const char *symbol);
int dlclose(void *handle);
链接时后面加 -ldl 选项.//这里注意,-ldl一定放在编译的最后,之前写程序时如果不加在最后会报错的.

描述:

这四个函数实现动态链接库加载接口.

dlerror():

返回可读字符串,返回dlopen,dlsym或dlclose的错误.dlerror只是保存最近一次调用时返回的错误信息.


dlopen():

dlopen通过filename加载动态链接库文件,返回void*类型的handle指向该动态链接库.如果filename是NULL,在返回handle指向的该主程序.如果filename包含"/",则解释为绝对或相对路径.否则,动态链接器按照以下方式搜索库.

//ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西.//目前自己没涉及这么深的东西,只是简单的应用.可以暂时略过关于ELF的内容

1)(ELF ONLY)如果可执行文件包含DT_RPATH标签,但不包含DT_RPATH标签,则会在DT_RPATH标签列出的目录里面搜索.

2)如果环境变量LD_LIBRARY_PATH已定义且包含了冒号分割的目录列表,则搜索目录列表.

3)(ELF only)如果可执行程序包含DT_RUNPATH标签,则搜索标签中列出的目录

4)检查缓存文件/etc/ld.so.cache是否包含filename的库

5)按顺序搜索目录/lib/ 和/usr/lib

如果filename库依赖于其他共享库,这么库也会动态加载进来,按照上述搜索方式查找.

flag参数:

RTLD_LAZY:执行懒惰式绑定,只有当指向符号的代码执行时,才会解析符号.如果符号一直没有指向,则一直不会被解析.lazy binding只是针对函数引用时才生效,当加载库时,指向变量的引用经常立即受限制.

RTLD_NOW:如果该值指定,或环境变量LD_BIND_NOW是非空值,所有在库中未定义的符号在dlopen返回前都会被解析.如果执行未完成,则返回错误.

下面的参数可以通过or在flag中 指定.

RTLD_GLOBAL:动态库中定义的符号可被其后打开的其它库解析.


RTLD_LOCAL: 与RTLD_GLOBAL作用相反,动态库中定义的符号不能被其后打开的其它库重定位。如果没有指明是RTLD_GLOBAL还是RTLD_LOCAL,则缺省为RTLD_LOCAL。


RTLD_NODELETE(glibc2.2以后): 在dlclose()期间不卸载库,并且在以后使用dlopen()重新加载库时不初始化库中的静态变量。这个flag不是POSIX-2001标准。


RTLD_NOLOAD(glibc2.2以后): 不加载库。可用于测试库是否已加载(dlopen()返回NULL说明未加载,否则说明已加载),也可用于改变已加载库的flag,如:先前加载库的flag为RTLD_LOCAL,dlopen(RTLD_NOLOAD|RTLD_GLOBAL)后flag将变成RTLD_GLOBAL。这个flag不是POSIX-2001标准。


RTLD_DEEPBIND(glibc2.3.4以后):在搜索全局符号前先搜索库内的符号,避免同名符号的冲突。这个flag不是POSIX-2001标准。

如果filename是一个NULL指针,返回主程序的handle.当传递给dlsym函数调用时,handle将会查找主程序中的符号,查找程序启动时的所有共享库,以及查找dlopen加载的带RTLD_GLOBAL的库.

库中的外部引用使用库以及库依赖的列表以及其他之前带有RTLD_GLOBAL标示打开的库解析.如果可行执行文件连接时使用-rdynamic或--export-dynamic,则可执行文件中的全局符号可以用来解析动态加载的库.意味着动态加载的库,可以引用可执行文件中的符号.后文会再涉及这个-rdynamic参数.

如果相同的库,使用dlopen再次加载,相同的handle会返回.dl库维护handle的计数引用,一个动态库不会解除,直到dlclose函数被调用.如果存在_init()流程,只调用一次.但随后RTLD_NOW调用可能强制早些使用RTLD_LAZY加载的库进行符号解析.

如果dlopen失败,返回NULL.


dlsys()

使用dlopen的handle和一个符号名字,返回符号在内存的位置.如果符号在指定的库,和自动被dlopen加载的库中找不到该符号,则返回NULL.dlsym返回值可能是返回NULL,应该将dlsym的返回值保存到变量中,然后检查保存的值是否为NULL.正确的方式测试错误是调用dlerror清空任何旧的错误情形,然后调用dlsym,然后再调用dlerror.
有两个特殊的假handle,RTLD_DEFAULT和RTLD_NEXT,(在handle的位置填写这其中一个).第一个会通过默认的库搜索顺序查找 符号.第二个,会在符号范围内中查找下一个函数.
这两个具体参考连接:(第二个连接更具有参考价值)
http://blog.csdn.net/ustcxiangchun/article/details/6310085
http://docs.oracle.com/cd/E19253-01/819-7050/6n918j8n4/index.html#chapter3-fig-15


dlclose()

减少动态链接库的引用数,如果引用计数减少至0,则卸载动态链接库.
成功时,返回0,失败时返回非0值.

弃的符号_init()和_fini()

如果链接器识别到了特殊符号_init和_fini.如果动态链接库,引入_init(),则在加载库完成后,返回dlopen之前会执行init代码.相反,如果包含_fini,则在库被卸载前执行相应的代码.上述两个不推荐使用,库应该使用__attribute__((constructor))和__attribute__((destructor))函数属性.执行时与init和fini类似.更多的查看gcc信息.

gcc扩展:dladdr()和dlvsym()

glibc增加连个函数,但是POSIX中并没有.
 #define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include <dlfcn.h>

       int dladdr(void *addr, Dl_info *info);

       void *dlvsym(void *handle, char *symbol, char *version);

       The function dladdr() takes a function pointer and tries to resolve name and file where it is located.  Information is stored in the  Dl_info
       structure:

           typedef struct {
               const char *dli_fname;  /* Pathname of shared object that
                                          contains address */
               void       *dli_fbase;  /* Address at which shared object
                                          is loaded */
               const char *dli_sname;  /* Name of nearest symbol with address
                                          lower than addr */
               void       *dli_saddr;  /* Exact address of symbol named
                                          in dli_sname */
           } Dl_info;
如果符号地址addr没有找到,则dli_sname和dli_saddr设置为NULL.
dladdr返回0表示错误,返回非0值表示成功.
dlvsym,glibc2.1版本提供与dlsym一样的功能,增加一个版本字符串.
个人觉得目前没有用到这么深,可以忽略这两个函数的用途,在手册中,bug中描述有时dladrr可能会产生意外.

EXAMPLE

终于到了使用的时候,之所以拿出来翻译这个地方的原因主要在于,在网上搜索的一些资料中是错的.甚至编译不通过,不清楚网上的是什么版本,至少在今天使用时编译总是报错.而且看看示例中正确使用.有一个小细节需要注意.
 Load the math library, and print the cosine of 2.0:

       #include <stdio.h>
       #include <stdlib.h>
       #include <dlfcn.h>

       int
       main(int argc, char **argv)
       {
           void *handle;
           double (*cosine)(double);
           char *error;

           handle = dlopen("libm.so", RTLD_LAZY);
           if (!handle) {
               fprintf(stderr, "%s\n", dlerror());
               exit(EXIT_FAILURE);
           }

           dlerror();    /* Clear any existing error */

           <span style="color:#ff0000;">/* Writing: cosine = (double (*)(double)) dlsym(handle, "cos");
              would seem more natural, but the C99 standard leaves
              casting from "void *" to a function pointer undefined.
              The assignment used below is the POSIX.1-2003 (Technical
              Corrigendum 1) workaround; see the Rationale for the
              POSIX specification of dlsym(). */

           *(void **) (&cosine) = dlsym(handle, "cos");</span>

           if ((error = dlerror()) != NULL)  {
               fprintf(stderr, "%s\n", error);
               exit(EXIT_FAILURE);
           }

           printf("%f\n", (*cosine)(2.0));
           dlclose(handle);
           exit(EXIT_SUCCESS);
       }
这里最重要的一点在于*(void **)(&cosine)=dlsym(handle,"cos");
正如注释中所说,强制转换可能更自然一些,易懂,但是C99标准里面将void *转换成函数指针是未定义的.所以采用样例上的方式更好些.
编译时,如果程序在foo.c程序文件里面,则使用如下命令
gcc -rdynamic  -o foo foo.c -ldl
如果库中包含了_init和_fini,编译时:gcc -shared -nostartfiles -o bar bar.c.
手册只是描述了主程序的编译.如果编写自己的库,使用方式已经告诉,编译方式如下.
gcc -shared -fPIC  hello.c -o libhello.so

这里两个参数 -shared和-fPIC.-shared,是共享库.-fPIC(position independent code)使.so文件的代码段变为真正意义上的共享.如果不加-fPIC,则加载.so文件的代码段时,代码段引用的数据对象需要重定位, 重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的copy.每个copy都不一样,取决于 这个.so文件代码段和数据段内存映射的位置.不加fPIC编译出来的so,是要再加载时根据加载到的位置再次重定位的.(因为它里面的代码并不是位置无关代码).


c plus plus 编写动态链接库

但是c++编写dlopen时不像c语言这么简单,设计一些问题.
参考连接:http://blog.chinaunix.net/uid-12072359-id-2960897.html

导致的原因

1)c与c++编译时命名不同.简单来说c++为了支持一些特性,重载等,命名规则较复杂.不是简单的符号.而c编译时是简单的符号.简单讲,按照c的方式查找c++的东西找不到
2)c++ 包含类.我们需要是加载一个类的实例,而不是简单的一个函数指针.

解决方案

1)extern "c"
说白了就是使用c的编译时命名方式.
C++有个特定的关键字用来声明采用C binding的函数:extern "C" 。 用 extern "C"声明的函数将使用函数名作符号名,就像C函数一样。因此,只有非成员函数才能被声明为extern "C",并且不能被重载。尽管限制多多,extern "C"函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。

2)利用多态性加载类.
基类指向通过函数实例化派生类.
可执行文件中定义一个带虚成员函数的接口基类,而在模块中定义派生实现类。在模块中,定义两个附加的helper函数,就是众所周知的“类工厂函数(class factory functions)其中一个函数创建一个类实例,并返回其指针; 另一个函数则用以销毁该指针。这两个函数都以extern "C"来限定修饰。
//----------
//main.cpp:
//----------
#include "polygon.hpp"
#include <iostream>
#include <dlfcn.h>

int main() {
    using std::cout;
    using std::cerr;

    // load the triangle library
    void* triangle = dlopen("./triangle.so", RTLD_LAZY);
    if (!triangle) {
        cerr << "Cannot load library: " << dlerror() << '\n';
        return 1;
    }

    // reset errors
    dlerror();
    
    // load the symbols
    create_t* create_triangle = (create_t*) dlsym(triangle, "create");
    const char* dlsym_error = dlerror();
    if (dlsym_error) {
        cerr << "Cannot load symbol create: " << dlsym_error << '\n';
        return 1;
    }
    
    destroy_t* destroy_triangle = (destroy_t*) dlsym(triangle, "destroy");
    dlsym_error = dlerror();
    if (dlsym_error) {
        cerr << "Cannot load symbol destroy: " << dlsym_error << '\n';
        return 1;
    }

    // create an instance of the class
    polygon* poly = create_triangle();

    // use the class
    poly->set_side_length(7);
        cout << "The area is: " << poly->area() << '\n';

    // destroy the class
    destroy_triangle(poly);

    // unload the triangle library
    dlclose(triangle);
}


//----------
//polygon.hpp:
//----------
#ifndef POLYGON_HPP
#define POLYGON_HPP

class polygon {
protected:
    double side_length_;

public:
    polygon()
        : side_length_(0) {}

    virtual ~polygon() {}

    void set_side_length(double side_length) {
        side_length_ = side_length;
    }

    virtual double area() const = 0;
};

// the types of the class factories
typedef polygon* create_t();
typedef void destroy_t(polygon*);

#endif

//----------
//triangle.cpp:
//----------
#include "polygon.hpp"
#include <cmath>

class triangle : public polygon {
public:
    virtual double area() const {
        return side_length_ * side_length_ * sqrt(3) / 2;
    }
};


// the class factories
extern "C" polygon* create() {
    return new triangle;
}

extern "C" void destroy(polygon* p) {
    delete p;
}

注意事项:
1)在模块或者说共享库中,同时提供一个创造函数和一个销毁函数.
2)接口类的析构函数在任何情况下都必须是虚函数(virtual)


扩展


编译器选项

编译时,主程序模块中的有一个-rdynamic.编写库时的两个参数-shared -fPIC.
-rdynamic:RTLD_GLOBAL使的动态库之间的对外接口是可见的,但是动态库是不能调用主程序中的全局符号,为了解决这个问题, gcc引入了一个参数-rdynamic,在编译载入共享库的可执行程序的时候最后在链接的时候加上-rdynamic,会把可执行文件中所有的符号变成全局可见,对于这个可执行程序而言,它载入的动态库在运行中可以直接调用主程序中的全局符号,而且如果共享库(自己或者另外的共享库 RTLD_GLOBAL) 加中有同名的符号,会选择可执行文件中使用的符号,这在一些情况下可能会带来一些莫名其妙的运行错误。
参考链接:http://blog.csdn.net/uestcleo/article/details/7727057

编写程序时,碰到的一个错误是,-shared -fPIC时不要加-c参数了,否则编译失败.


动态库和静态库

简单讲,静态库,是在每个程序进行链接的时候将库在目标程序中进行一次拷贝,当目标程序生成的时候,程序可以脱离库文件单独运行。生成的程序中已经包含了该库的内容.

共享库可以被多个应用程序共享,实在程序运行的时候进行动态的加载。

参考:
http://www.cnblogs.com/luoxiang/p/4168607.html
http://www.cnblogs.com/skynet/p/3372855.html



















文章来自:http://blog.csdn.net/cdhql/article/details/42001927
© 2021 jiaocheng.bubufx.com  联系我们
ICP备案:鲁ICP备09046678号-3