Linux共享库概述
共享库是一种将库函数打包成一个单元使之能够在运行时被多个进程共享的技术。这种技术能够节省磁盘空间和RAM。
在继续阐述共享库之前,先来说说静态库,它是比共享库更早的存在。静态库也称为归档文件,它的作用就是将一组经常被用到的目标文件组织进单个库文件,这样以来,就可以使用它来构建多个可执行程序,并且在构建各个应用程序的时候无需重新编译原来的源代码。
从以上的描述中,可以看出,静态库必须和可执行文件一起被链接进目标文件中,这样的话,不同的可执行程序使用了相同的目标模块时,每个可执行程序都有自己的一份静态库副本,这样做有以下几个缺点:
- 存储同一目标模块会浪费磁盘空间,并且实际上浪费的空间还是很大的。
- 如果使用了同一模块的几个不同的可执行程序在同一时刻运行,那么每个程序会独立地在虚拟内存中保存一份目标模块的副本,从而提高了系统中虚拟内存的使用量。
- 若是需要修改静态库中的错误,那么需要重新生成静态库,再链接合并到目标文件中。
每一种新生事物的出现都是为了解决之前已经存在的问题,所以这里就引出了动态库,它就是来解决以上这些问题的:
- 共享库的关键思想就是目标木块的单个副本由所有需要这些模块的程序共享,目标模块不会被复制到链接过的可执行文件中,而是,当第一个需要共享库的木块的程序启动时,库的单个副本就会在运行时被加载进内存。当后面使用同一共享库的其他程序启动时,它们会使用已经加载进内存的库的副本。使用共享库,就意味着可执行程序需要的磁盘空间和虚拟内存更少了,这就解决了一个问题。
- 由于目标模块没有被复制进可执行文件中,而是在共享库中集中维护,因此在修改目标模块时,无需重新链接程序,甚至正在运行的程序正在使用现有版本共享库的时候也能够进行这样的变更。
下面一张图示,很形象具体地说明了从创建动态库到链接进可执行文件的过程:
接下来的另一图示则是很形象具体地说明克创建程序被加载进内存时执行:
## 使用共享库的有用工具
接下来介绍一些和共享库相关的有用工具
ldd
ldd(列出动态依赖)命令显示了一个程序所需的共享库,执行如下:
$ ldd xxx
它会以library-name => resolves-to-path的形式将程序所使用的动态库列举出来
- objdump
该命令可以获取到各类信息,具体请参数函数命令手册
- nm
nm命令会列出目标库或者可执行程序中定义的一组符号。这个命令的一个作用是找出哪些库定义了一个符号
共享库的运行
为了能够在运行时找到共享库,动态链接库遵循了一组标准的搜索规则,其中包括搜索一组大多数共享库安装的目录(如:/lib/ //usr/lib 等),当然也可以在用户自定义的目录下搜索,这一点,可以参考相关参考。
共享库的一些特性
动态加载库
当一个可执行文件开始运行后,动态链接器会加载程序的动态依赖列表中的所有共享库,但有些时候,延迟加载库是比较有用的,如只在需要的时候再加载一个插件,动态链接器是通过一组函数来实现的。先总体认识下这些函数:
dlopen();
dlsym();
dlclose();
dlerror();
打开共享库
#include <dlfcn.h>
void *dlopen(const char * linfilename,int flag);
以上函数通过用户传入的地址目录,打开了一个共享库,并返回了一个供后续调用使用的句柄。
获取符号的地址
#include <dlfcn.h>
void *dlsym(void * linfilename,char * symbol);
该函数实际上利用刚才返回的调用句柄,并传入函数名,这样就可以调用共享库中提供的函数了。
关闭共享库
#include <dlfcn.h>
void *dlclose(void * handle);
该函数会减小handle的引用的库的打开的引用系统计数,如果这个引用技术变成了0,并且其他库已经不需要用到该库中的符号,那么就会卸载这个库。系统会在这个库的依赖树中的库执行(递归)同样地过程。值得注意的是,当进程终止时,会隐式地对所有库执行dlclose()。
错误判断
#include <dlfcn.h>
void *dlerror(void );
该函数主要是在dlopen调用错误后,获取到一个表明错误原因的字符串指针。
后话
应用开发的话,我们经常会使用ndk,使用ndk-build脚本来生成共享库,有时会发生一些莫名其妙的问题。因为共享库的相关内容与gcc有莫大关系,gcc的编译参数可谓繁杂,我在学习共享库相关内容时,有这么一个思考,有时候一些莫名其妙的问题是否与一些默认的编译参数与我们所需要的是否是不一样导致的?这只是一点不成熟的思考,希望有兴趣的各位能一起讨论!