一、引言
通常情况下,对函数库的链接是放在编译时期(compile time)完成的。所有相关的对象文件(object file)与牵涉到的函数库(library)被链接合成一个可执行文件(executable file)。程序在运行时,与函数库再无瓜葛,因为所有需要的函数已拷贝到自己门下。所以这些函数库被成为静态库(static libaray),通常文件名为“libxxx.a”的形式。
其实,我们也可以把对一些库函数的链接载入推迟到程序运行的时期(runtime)。这就是动态链接库(dynamic link library)技术。
二、动态链接库的特点与优势
首先让我们来看一下,把库函数推迟到程序运行时期载入的好处:
1. 可以实现进程之间的资源共享。
什么概念呢?就是说,某个程序的在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了。如果有,则让其共享那一个拷贝;只有没有才链接载入。这样的模式虽然会带来一些“动态链接”额外的开销,却大大的节省了系统的内存资源。C的标准库就是动态链接库,也就是说系统中所有运行的程序共享着同一个C标准库的代码段。
2. 将一些程序升级变得简单。
用户只需要升级动态链接库,而无需重新编译链接其他原有的代码就可以完成整个程序的升级。Windows 就是一个很好的例子。
3. 甚至可以真正坐到链接载入完全由程序员在程序代码中控制。
程序员在编写程序的时候,可以明确的指明什么时候或者什么情况下,链接载入哪个动态链接库函数。你可以有一个相当大的软件,但每次运行的时候,由于不同的操作需求,只有一小部分程序被载入内存。所有的函数本着“有需求才调入”的原则,于是大大节省了系统资源。比如现在的软件通常都能打开若干种不同类型的文件,这些读写操作通常都用动态链接库来实现。在一次运行当中,一般只有一种类型的文件将会被打开。所以直到程序知道文件的类型以后再载入相应的读写函数,而不是一开始就将所有的读写函数都载入,然后才发觉在整个程序中根本没有用到它们。
三、动态链接库的创建
由于动态链接库函数的共享特性,它们不会被拷贝到可执行文件中。在编译的时候,编译器只会做一些函数名之类的检查。在程序运行的时候,被调用的动态链接库函数被安置在内存的某个地方,所有调用它的程序将指向这个代码段。因此,这些代码必须实用相对地址,而不是绝对地址。在编译的时候,我们需要告诉编译器,这些对象文件是用来做动态链接库的,所以要用地址不无关代码(Position IndependentCode (PIC))。
对gcc编译器,只需添加上-fPIC 标签,如:
gcc -fPIC -c file1.c
gcc -fPIC -c file2.c
gcc -shared -o libxxx.so file1.o file2.o
也可以直接生成.so:
gcc -fPIC -shared -o libxxx.so file1.c file2.c
注意到最后一行,-shared标签告诉编译器这是要建立动态链接库。这与静态链接库的建立很不一样,后者用的是 ar 命令。也注意到,动态链接库的名字形式为 “libxxx.so” 后缀名为 “.so”
3.1 编译参数解析
最主要的是GCC命令行的一个选项: -shared 。 该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件
-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
-L.:表示要连接的库在当前目录中
-ltest:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称
LD_LIBRARY_PATH:这个环境变量指示动态连接器可以装载动态库的路径。
当然如果有root权限的话,可以修改/etc/ld.so.conf文件,然后调用 /sbin/ldconfig来达到同样的目的,不过如果没有root权限,那么只能采用输出LD_LIBRARY_PATH的方法了。
3.2 注意
调用动态库的时候有几个问题会经常碰到,有时,明明已经将库的头文件所在目录 通过 “-I” include进来了,库所在文件通过 “-L”参数引导,并指定了“-l”的库名,但通过ldd命令察看时,就是死活找不到你指定链接的so文件,这时你要作的就是通过修改 LD_LIBRARY_PATH或者/etc/ld.so.conf文件来指定动态库的目录。通常这样做就可以解决库无法链接的问题了。
四、LINUX下动态链接库的使用
4.1 重要的dlfcn.h头文件
LINUX下使用动态链接库,源程序需要包含dlfcn.h头文件,此文件定义了调用动态链接库的函数的原型。下面详细说明一下这些函数。
4.1.1 dlerror
原型为: const char *dlerror(void);
当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。
4.1.2 dlopen
原型为: void *dlopen (const char *filename, intflag);
dlopen用于打开指定名字(filename)的动态链接库,并返回操作句柄。
filename: 如果名字不以/开头,则非绝对路径名,将按下列先后顺序查找该文件。
(1) 用户环境变量中的LD_LIBRARY值;
(2) 动态链接缓冲文件/etc/ld.so.cache
(3) 目录/lib,/usr/lib
flag表示在什么时候解决未定义的符号(调用)。取值有两个:
1) RTLD_LAZY : 表明在动态链接库的函数代码执行时解决。
2) RTLD_NOW : 表明在dlopen返回前就解决所有未定义的符号,一旦未解决,dlopen将返回错误。
dlopen调用失败时,将返回NULL值,否则返回的是操作句柄。
4.1.3 dlsym : 取函数执行地址
原型为: void *dlsym(void *handle, char*symbol);
dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的函数的执行代码地址。由此地址,可以带参数执行相应的函数。
如程序代码: void (*add)(int x,int y); /* 说明一下要调用的动态函数add */
add=dlsym("xxx.so","add");/* 打开xxx.so共享库,取add函数地址 */
add(89,369); /* 带两个参数89和369调用add函数 */
4.1.4 dlclose : 关闭动态链接库
原型为: int dlclose (void *handle);
dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
4.2 在程序中使用动态链接库函数
4.2.1 程序范例
下面的程序装载了动态链接库my.so,并用getdate,gettime取得当前日期与时间后输出。
----------------------------------------------------------------------
1 /************************************/
2 /* 文件名称: dy.c */
3 /* 功能描述: 动态链接库应用示范程序 */
6 /************************************/
7
8 #include "stdio.h" /* 包含标准输入输出文件 */
9
10 #include "dlfcn.h" /* 包含动态链接功能接口文件 */
11 #define SOFILE "./my.so" /* 指定动态链接库名称 */
12
13 #define SHARED /* 定义宏,确认共享,以便引用动态函数 */
14 #include "datetime.h" /* 包含用户接口文件 */
15
16 main()
17 {
18 DATETYPE d;
19 TIMETYPE t;
20 void *dp;
21 char *error;
22
23 puts("动态链接库应用示范");
24
25 dp=dlopen(SOFILE,RTLD_LAZY); /* 打开动态链接库 */
26
27 if (dp==NULL) /* 若打开失败则退出 */
28 {
29 fputs(dlerror(),stderr);
30 exit(1);
31 }
32
33 getdate=dlsym(dp,"getdate"); /*定位取日期函数 */
34
35 error=dlerror(); /* 检测错误 */
36 if (error) /* 若出错则退出 */
37 {
38 fputs(error,stderr);
39 exit(1);
40 }
41
42 getdate(&d); /* 调用此共享函数 */
43 printf("当前日期:%04d-%02d-%02d\n",d.year,d.mon,d.day);
44
45 gettime=dlsym(dp,"gettime"); /*定位取时间函数 */
46
47 error=dlerror(); /* 检测错误 */
48 if (error) /* 若出错则退出 */
49 {
50 fputs(error,stderr);
51 exit(1);
52 }
53
54 gettime(&t); /* 调用此共享函数 */
55 printf("当前时间:%02d:%02d:%02d\n",t.hour,t.min,t.sec);
56
57 dlclose(dp); /* 关闭共享库 */
58
59 exit(0); /* 成功返回*/
60
61 }
----------------------------------------------------------------------
程序说明:
第8行: 包含标准输入输出头文件,因为程序中使用了printf,puts,fputs等标准输入输出函数,需要让编译器根据头文件中函数的原型,检查一下语法;
第10-11行: 包含动态链接库功能头文件,并定义动态链接库名称;
第13-14行: 定义宏SHARED以便引用14行的头文件datetime.h中的动态函数说明;
第25行: 用dlopen打开SOFILE共享库,返回句柄dp;
第27-31行: 检测dp是否为空,为空则显示错误后退出;
第33行: 用dlsym取得getdate函数动态地址;
第35-40行: 如果dlerror返回值不为空,则dlsym执行出错,程序显示错误后退出;
第42-43行: 执行getdate调用,输出当前日期;
第45行: 用dlsym取得gettime函数动态地址;
第47-52行: 如果dlerror返回值不为空,则dlsym执行出错,程序显示错误后退出;
第54-55行: 执行gettime调用,输出当前时间;
第57行: 用dlclose关闭dp所指示的动态链接库;
第59行: 程序退出,返回0值。
2.2.2 编写维护文件
维护文件makefile内容如下:
----------------------------------------------------------------------
1 # makefile :
2
3 all : dy
4
5 DYSRC = dy.c
6
7 DYTGT = $(DYSRC:.c=.o)
8
9 %.o : %.c
10 cc -c $?
11
12 # 动态库应用示范程序
13 dy : $(DYTGT)
14 cc -rdynamic -s -o $@ $(DYTGT) -ldl
15
----------------------------------------------------------------------
维护文件说明:
第3行: 定义所有需要维护的模块;
第5行: 定义源程序;
第7行: 定义目标文件;
第9-10行: 定义.o文件依赖于.c文件,维护代码为“cc -c 变动的源文件名”;
第13-14行: 定义dy依赖于变量DYTGT指示的值,维护代码中采用-rdynamic选项以指定输出文件为动态链接的方式,选项-s指定删除目标文件中的符号表,最后的选项-ldl则指示装配程序ld需要装载dl函数库。
4.2.3 运行make命令
运行make后将产生执行文件dy,运行后将产生如下类似信息:
动态链接库应用示范
当前日期: 2012-12-27
当前时间: 21:10:30
当删除my.so文件时,将出现以下信息:
动态链接库应用示范
my.so: cannot open shared object file: 文件或目录不存在
五、小结
LINUX创建与使用动态链接库并不是一件难事。
编译函数源程序时选用-shared选项即可创建动态链接库,注意应以.so后缀命名,最好放到公用库目录(如/lib,/usr/lib等)下面,并要写好用户接口文件,以便其它用户共享。
使用动态链接库,源程序中要包含dlfcn.h头文件,写程序时注意dlopen等函数的正确调用,编译时要采用-rdynamic选项与-ldl选项,以产生可调用动态链接库的执行代码。