动态库的制作与两种使用方式

前言

在《如何制作属于自己的静态库》中简单介绍了静态库的制作方法,但实际上动态库的使用更为广泛,至于原因,在《静态库和动态库的区别》一文中已有说明。本文介绍动态库的制作方法以及两种使用方式。

示例程序

test.c代码如下:

1
2
3
4
5
6
//来源:公众号【编程珠玑】 网站:https://www.yanbinghu.com
#include"test.h"
void test()
{
printf("I am test;hello,编程珠玑\n");
}

test.h代码如下:

1
2
#include<stdio.h>
void test();

代码比较简单,只有一个test函数,用于打印一段字符串。

制作动态库

只需要执行以下命令即可:

1
$ gcc test.c -fPIC -shared -o libtest.so

其中的-fPIC表示生成位置无关代码,以便在只有一个副本的情况下供多个应用程序共享

通过readelf命令查看elf文件类型:

1
2
3
4
5
6
7
8
9
10
$ readelf -h libtest.so
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64

从结果中可以看到,libtest.so为Shared object file。

使用动态库

常见有两种使用方式,一种是加载时链接,另一种是使用时链接。

来源:公众号【编程珠玑】
个人博客:https://www.yanbinghu.com
未经授权禁止以任何形式转载

加载时链接

加载时链接在代码中不需要做额外的动作,像使用静态库一样使用即可。例如main.c如下:

1
2
3
4
5
6
7
8
//来源:公众号【编程珠玑】 网站:https://www.yanbinghu.com
#include<stdio.h>
#include"test.h"
int main(void)
{
test();
return 0;
}

编译链接:

1
$ gcc -o main main.c -L . -ltest

其中-L指定从当前目录下寻找动态库libtest.so,否则会找不到。

然后我们还可以通过ldd命令查看其依赖的动态库:

1
2
3
4
5
$ ldd main
linux-vdso.so.1 => (0x00007ffd57757000)
libtest.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f84c13f6000)
/lib64/ld-linux-x86-64.so.2 (0x00007f84c17c0000)

其中就有我们自己制作的libtest.so。

运行:

1
2
$ ./main
./main: error while loading shared libraries: libtest.so: cannot open shared object file: No such file or directory

很不幸,程序并没有如预期的那样运行起来,而是报错了。这是为什么呢?

其实我们在使用ldd命令查看的时候,就注意到:

1
libtest.so => not found

它并不能找到这个动态库,因为它会默认从系统库的路径去查找这个库,但是我们并没有把这个库放到系统路径下,因此会找不到了。

我们有两种方法解决这个问题:

  • 将libtest.so库放到系统路径下
  • 指定当前进程动态库搜索路径

第一种方法:

1
2
3
$ cp libtest.so /usr/lib
$ ./main
I am test;hello,编程珠玑

第二种方法:

1
2
3
$ export LD_LIBRARY_PATH=./
$ ./main
I am test;hello,编程珠玑

导入LD_LIBRARY_PATH环境变量,指定库搜索路径,使得main程序能够找到libtest.so。

此时再看:

1
2
3
4
5
$ ldd main
linux-vdso.so.1 => (0x00007ffcdebdf000)
libtest.so => ./libtest.so (0x00007f494a45f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f494a095000)
/lib64/ld-linux-x86-64.so.2 (0x00007f494a661000)

libtest.so不再是not found了。

使用时链接

为了使用这种方式,需要使用几个函数dlopen,dlsym,dlclose,dlerror,其原型分别如下:

1
2
3
4
5
#include <dlfcn.h>
void *dlopen(const char *filename, int flags);
void *dlsym(void *handle, const char *symbol);
int dlclose(void *handle);
char *dlerror(void);

其中dlopen用于打开一个动态库,filename是动态库的名称,flags是打开标志,一般为RTLD_LAZY,表示当要调用的时候才去解析符号;而RTLD_NOW则在dlopen之前就会去解析,还有其他选项这里就不多介绍了。

dlsym函数用于从动态库中查找需要使用的函数;

dlclose函数用于卸载已加载的动态库;

dlerror函数用于打印动态库相关错误。

我们修改main.c,来看看具体如何使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//来源:公众号【编程珠玑】 网站:https://www.yanbinghu.com
#include<stdio.h>
#include <dlfcn.h>
#include"test.h"
int main(void)
{
printf("start to call test\n");
char *error = NULL;
/*打开动态库*/
void *handle = dlopen("libtest.so",RTLD_LAZY);
if(NULL == handle)
{
error = dlerror();
printf("open error:%s\n",error);
return -1;
}
/*返回类型为函数指针*/
void (*fun)() = dlsym(handle,"test");
if(NULL == fun)
{
error = dlerror();
printf("open error:%s\n",error);
dlclose(handle);
return -1;
}
/*调用函数*/
(*fun)();

/*关闭*/
dlclose(handle);
printf("end to call test\n");
return 0;
}

这种方式的动态库使用可以大致分为以下几个步骤:

  • 使用dlopen打开动态库
  • 使用dlsym找到需要使用的符号
  • 调用动态库中的函数
  • dlopen关闭(卸载)动态库

在文本的代码中,用到了函数指针,相关内容可参考《高级指针话题-函数指针》。
编译运行:

1
2
3
4
$ gcc  main.c -ldl -L . -o main  #需要链接libdl.so库
$ ./main
start to call test
open error:libtest.so: cannot open shared object file: No such file or directory

运行时,我们发现并没有如预期的那样。但是可以看到,程序已经打印了start to call test,然后才报错,说明程序是在运行起来之后再尝试去从动态库中查找test符号的。

当然了,至于问题原因,我们在前面已经提到了,是由于没有设置动态库搜索路径或者在系统默认库路径下没有我们需要的libtest.so。按照前面提供的两种方法修正后,重新运行:

1
2
3
4
$ ./main
start to call test
I am test;hello,编程珠玑
end to call test

一切正常。

这种方式有以下好处:

  • 编译时无需链接需要的动态库,我们注意到第二种方式编译时没有加-ltest
  • 如果程序的某些场景不需要动态库的函数,那么它就不会去加载该动态库

再看动态库

如果我们修改test.c的代码,我们不再需要重新编译main.c,而只需要更新动态库即可。
假如我们将test.c代码中的打印语句修改为如下:

1
2
3
4
5
6
//来源:公众号【编程珠玑】 网站:https://www.yanbinghu.com
#include"test.h"
void test()
{
printf("hello www.yanbinghu.com\n");
}

重新生成动态库libtest.so,重新执行main:

1
2
3
4
$ ./main
start to call test
hello www.yanbinghu.com
end to call test

我们发现,不需要重新编译main进程,就可以达到替换test函数实现的效果

总结

动态库应用广泛,其制作过程可能不作深入要求,但是其基本使用还是非常有必要了解的。本文总结如下:

  • 程序运行时不能脱离动态库
  • 动态库有两种常见使用方式,一种是加载是链接,一种是运行时链接
  • 只要函数声明没有改变,动态库中函数实现的更新不需要重新编译可执行文件
守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!