概述
插件技术的目的是为了更好的扩展性.动态链接库是其中 一种实现方式.
这里主要论述几个问题.
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