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

时间: 12-18

linux 动态库插件技术(c/c++):动态链接库的相关文章

windows动态库与Linux动态库

Linux动态库和windows动态库的目的是基本一致的,但由于操作系统的不同,他们在许多方面还是不尽相同.但是尽管有差异Linux动态库的windows动态库还是可以移植的,有一些规则以及经验是必须的知道的. 两种系统动态库比较分析 Windows和Linux采用动态链接库技术 (1)动态库程序编写,在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数作为初始化的人口,通常在导出函数的声明时需要有_declspec(dllexport)关键字.Linux下的gcc编

技巧:Linux 动态库与静态库制作及使用详解

技巧:Linux 动态库与静态库制作及使用详解 标准库的三种连接方式及静态库制作与使用方法 Linux 应用开发通常要考虑三个问题,即:1)在 Linux 应用程序开发过程中遇到过标准库链接在不同 Linux 版本下不兼容的问题: 2)在 Linux 静态库的制作过程中发现有别于 Windows 下静态库的制作方法:3)在 Linux 应用程序链接第三方库或者其他静态库的时候发现链接顺序的烦人问题.本文就这三个问题针对 Linux 下标准库链接和如何巧妙构建 achrive(*.a) 展开相关介

Android NDK开发及调用标准linux动态库.so文件

源:Android NDK开发及调用标准linux动态库.so文件 预备知识及环境搭建 1.NDK(native development Kit)原生开发工具包,用来快速开发C.C++动态库,并能自动将so文件和java应用一起打包成apk.对应:jni层c++开发 2.Cygwin:是windows平台上模拟Linux运行环境的工具,即window平台上的linux环境工具,so文件需要在linux平台上编译运行.对应:arm linux平台 3.CDT:eclipse下的C/C++开发工具,

Linux动态库相关知识整理

动态库和静态库在C/C++开发中很常见,相比静态库直接被编译到可执行程序, 动态库运行时加载使得可执行程序的体积更小,更新动态库可以不用重新编译可执 行程序等诸多好处.作者是一个Linux后台开发,这些知识经常用到,所以 整理了一下这方面的知识.静态库相对简单,本文只关心Linux平台下的动态库. 创建动态库 这里我把一个短小却很有用的哈希函数编译成动态库做为示例,ELFhash用于对字符串做哈希,返回一个无符号整数. //elfhash.h #include <stdio.h> unsign

Linux动态库搜索路径的技巧

众所周知,Linux动态库的默认搜索路径是/lib和/usr/lib.动态库被创建后,一般都复制到这两个目录中.当程序执行时需要某动态库,并且该动态库还未加载到内存中,则系统会自动到这两个默认搜索路径中去查找相应的动态库文件,然后加载该文件到内存中,这样程序就可以使用该动态库中的函数,以及该动态库的其它资源了.在Linux 中,动态库的搜索路径除了默认的搜索路径外,还可以通过以下三种方法来指定. 方法一:在配置文件/etc/ld.so.conf中指定动态库搜索路径. 可以通过编辑配置文件/etc

linux动态库与静态库使用比较

在windows下,动态库dll的使用往往伴随着lib的指引,而linux使用动态库和静态库则有较大的不同. linux静态库和动态库的区别 1. 静态库 名字一般是libxxx.a:利用静态函数库编译成的文件比较大,因为整个 函数库的所有数据都会被整合进目标代码中,编译后的执行程序不需要外部的函数库支持,但是,升级比较麻烦.每一次版本更新都需要重新编译. 2. 动态库 名字一般是libxxx.so;动态库没有被编译进最终程序,只有在需要的时候,动态加载到内存中.编译后的程序不包含动态库部分,程

Linux 动态库相关知识整理

动态库和静态库在C/C++开发中很常见,相比静态库直接被编译到可执行程序,动态库运行时加载使得可执行程序的体积更小,更新动态库可以不用重新编译可执行程序等诸多好处.作者是一个Linux后台开发,这些知识经常用到,所以整理了一下这方面的知识.静态库相对简单,本文只关心Linux平台下的动态库. 创建动态库 这里我把一个短小却很有用的哈希函数编译成动态库做为示例,ELFhash用于对字符串做哈希,返回一个无符号整数. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //elf

linux动态库默认搜索路径设置的三种方法

众所周知, Linux 动态库的默认搜索路径是 /lib 和 /usr/lib .动态库被创建后,一般都复制到这两个目录中.当程序执行时需要某动态库, 并且该动态库还未加载到内存中,则系统会自动到这两个默认搜索路径中去查找相应的动态库文件,然后加载该文件到内存中,这样程序就可以使用该动态库中的函 数,以及该动态库的其它资源了.在 Linux 中,动态库的搜索路径除了默认的搜索路径外,还可以通过以下三种方法来指定. 方法一:在配置文件 /etc/ld.so.conf 中指定动态库搜索路径.每次编辑

Linux动态库(.so)搜索路径

主要内容: 1.Linux动态库.so搜索路径 编译目标代码时指定的动态库搜索路径: 环境变量LD_LIBRARY_PATH指定的动态库搜索路径: 配置文件/etc/ld.so.conf中指定的动态库搜索路径: 默认的动态库搜索路径/lib: 默认的动态库搜索路径/usr/lib. 2.通过实例验证五种动态库的搜索路径以及其先后顺序 众所周知,Linux动态库的默认搜索路径是/lib和/usr/lib.动态库被创建后,一般都复制到这两个目录中.当程序执行时需要某动态库,并且该 动 态库还未加载到