库文件的制作与使用
库文件
一、编译过程
首先,程序的编译是一个复杂的过程,虽然平时一般可以将源代码文件一步到位编译生成最终的可执行文件。但其中实际上会经过如下图所示的四个步骤:
1.预处理:解释并展开源程序当中的所有的预处理指令,此时生成 *.i 文件。
2.编译:词法和语法的分析,生成对应硬件平台的汇编语言文件,此时生成 *.s 文件。
3.汇编:将汇编语言文件翻译为对应处理器的二进制机器码,此时生成 *.o 文件。
4.链接:将多个 *.o 文件合并成一个不带后缀的可执行文件。
以上图的 hello.c 为例,逐步生成这些中间文件的编译命令是:
gcc hello.c -o hello.i -E
gcc hello.i -o hello.s -S
gcc hello.s -o hello.o -c
gcc hello.o -o hello -lc
二、ELF格式
概述
对于上述编译过程,重点关注最后一步库文件的链接(gcc hello.o -o hello -lc):链接实际上是将多个.o文件合并在一起的过程。这些 *.o 文件合并前是 ELF 格式,合并后也是 ELF 格式。
ELF全称是 Executable and Linkable Format,即可执行可链接格式。ELF文件由多个不同的段(section)组成,如下图所示:
ELF格式的合并,实际上就是将多个文件中各自对应的段合并在一起,形成一个统一的ELF文件。在此过程中,必然需要对各个 *.o 文件中的静态数据(包括常量)、函数入口的地址做统一分配和管理,这个过程就叫做 重定位,因此未经链接的单独的 *.o 文件又被称为可重定位文件,经过链接处理合并了相同的段的文件称为可执行文件。
库的本意是library图书馆,库文件就是一个由很多 *.o 文件堆积起来的集合。
相关命令
(1) readelf 可以用来查看 ELF 格式文件的具体细节:
# 查看文件格式头部信息
readelf -h a.out
# 查看各个section信息
readelf -S a.out
# 查看符号表
readelf -s a.out
(2) ldd 可以用来查看 ELF 格式文件的动态库依赖关系
ldd a.out
三、库的基本概念
库文件分为两类:静态库和动态库。如:
静态库:libx.a
动态库:liby.so
//库文件的名称遵循这样的规范:
lib库名.后缀
其中,lib是任何库文件都必须有的前缀,库名就是库文件真正的名称,比如上述例子中两个库文件分别叫x和y,在链接它们的时候写成 -lx 和 -ly ,后缀根据静态库和动态库,可以是 .a 或者 .so:
- 静态库的后缀:
.a(archive,意即档案) - 动态库的后缀:
.so(share object,意即共享对象)
注意:不管是静态库,还是动态库,都是可重定位文件*.o的集合。
四、动态库与静态库的区别
不管是动态库还是静态库,它们都是 *.o 文件的集合。如果把一个 *.o 文件比作一本图书,那么库文件就是书店或图书馆,静态库和动态库的关系和区别是:
-
静态库(相当于书店,只卖不借)
- 原理:编译时,库中的代码将会被复制到每一个程序中
- 优点:程序运行时不依赖于库、执行效率稍高
- 缺点:牺牲存储空间、无法对用户升级迭代
-
动态库(相当于图书馆,只借不卖)
- 原理:编译时,程序仅确认库中功能模块的匹配关系,并未复制
- 缺点:程序运行时依赖于库、执行效率稍低
- 优点:节省存储空间、方便对用户升级迭代
表面上看,静态库和动态库各有千秋,彼此的优缺点是相对的,但在实际应用中,动态库应用场合要远多于静态库,因为虽然动态库的运行时装载特性会使得程序性能有略微的下降,但换来的是不仅仅节省了大量的存储空间,更重要的是使得主程序和库松耦合,不互相捆绑,当库升级的时候,应用程序无需任何改动即可获得新版库文件的功能,这极大地提高了程序的灵活性。
五、静态库的制作
假设功能文件 a.c、b.c 包含了一些通用的程序模块,可以被其他程序复用,那么可以将它们制作成静态库,具体的步骤是:
*第一步,制作 .o 原材料
gcc a.c -o a.o -c
gcc b.c -o b.o -c
*第二步,将 .o 合并成一个静态库
ar crs libx.a a.o b.o
*可见制作静态库非常简单,制作完成之后,可以用命令 ar 查看库中所包含的 .o 文件:
ar -t libx.a
六、静态库的常见操作
1. 查看静态库中的 *.o 列表
ar t libx.a #(t意即table,以列表方式列出*.o文件)
a.o
b.o
2. 删除静态库中的 *.o 文件
ar d libx.a b.o #(d意即delte,删除掉指定的*.o文件)
ar t libx.a
a.o
3. 向静态库增加 *.o 文件
ar r libx.a b.o #(r意即replace,添加或替换(重名时)指定的*.o文件)
ar t libx.a
a.o
b.o
4. 提取静态库中的 *.o 文件
ar x libx.a #(x意即extract,将库中所有的*.o文件释放出来)
ar x libx.a a.o #(指定释放库中的a.o文件)
七、静态库的使用
库文件最大的价值,在于代码复用。假设在上述库文件所包含的 *.o 文件中,已经包含了若干函数接口,那么只要能链接这个库,就无需再重复编写这些接口,直接链接即可。
示例
下面以一个简单的具体例子来说明整个过程(静态库示例.zip):
(1) 编写 a.c
#include <stdio.h>
void someFunc(void)
{
printf("实现某个功能模块...\n");
}
(2) 将 a.c 制作为静态库
gcc a.c -o a.o -c
ar crs libx.a a.o
(3) 在有需要该功能模块的地方链接该库
#include <stdio.h>
#include "a.h"
int main(int argc, char const *argv[])
{
someFunc();
return 0;
}
(4) 编译并运行
注意:
大写L指定库的路径
小写的L指定库的名字(不包括前缀和后缀)
注意
- 编译语句中的
-L/path/to/lib/指明库文件libx.a的具体位置,否则系统找不到该库文件。 - 编译语句中的
-lx指明要链接的库文件的具体名称,注意不包含前缀后缀。 - 对于静态库而言,由于编译链接时会将
main.c所需要的库代码复制一份到最终的执行文件中,这直接导出静态库的如下特性:- 执行程序在编译之后与静态库脱离关系,其执行也不依赖于静态库。
- 执行程序执行时由于不依赖静态库,因此也省去了运行时动态。
八、库中的重名符号
符号指的是函数名、变量名,当库中不同的 *.o 文件有重名的符号时,被调用的版本则以它们在库中的排列在前面的为准。
假设库 a.o 和 b.o 都包含了如下函数:
void func()
{
printf("我是%s中的函数%s\n", __FILE__, __FUNCTION__);
}
那么,当main函数调用函数func时,函数所调用的最终版本取决于a.o 和b.o 在库中的排列顺序:
ar t libx.a
a.o
b.o
./main
我是a.c中的函数func
可以通过改变 *.o 文件在库文件的出现次序来改变程序执行结果:
提出库文件中的b.o
ar d libx.a b.o
将b.o添加到库中并且查到a.o的前面
ar rb a.o libx.a b.o
ar t libx.a
b.o
a.o
执行main程序
./main
我是b.c中的函数func
总结:
如果程序所链接的库中存在多个重名的符号,那么具体调用哪个版本取决于库中各个 *.o 文件的排列次序,排在前面的优先被调用。
九、多个库的相互依赖
假设有两个库文件:liba.a 和 libb.a,它们分别只包含了 a.o 和 b.o,假设这两个源程序有如下依赖关系(库的相互依赖.zip):
// a.c
#include <stdio.h>
void fa()
{
printf("我是%s\n", __FUNCTION__);
}
// b.c
#include "a.h"
void fb()
{
fa();
}
很明显,b.c中的功能接口是依赖于 a.c 的,换句话说,库文件 libb.a 是依赖于 liba.a 的。
现在再来写一个调用 fb() 的主函数:
////////////////////////////////////////////////////////
//
// 文件: main.c
// 描述: 用以演示库的相互依赖
//
///////////////////////////////////////////////////////
#include <stdio.h>
#include "b.h"
int main(int argc, char const *argv[])
{
fb();
return 0;
}
编译情况如下:
gcc main.c -o main -L. -lb -la
gcc main.c -o main -L. -la -lb
./libb.a(b.o): In function `fb':
b.c:(.text+0xa): undefined reference to `fa'
collect2: error: ld returned 1 exit status
从以上编译信息来看,得出结论:
- 当编译链接多个库,且这些库之间有依赖关系时,被依赖的基础库要放在编译语句的后面。
- 在以上示例中,库
libb.a依赖于liba.a,即liba.a是被依赖的基础库,因此-la要放在-lb的后面才能通过编译。
注意:以上结论对于静态库、动态库都适用。
十、动态库的命名
动态库、静态库的名称都遵循这样的规范:
lib库名.后缀
对于动态库而言,在后缀后面还经常会带着版本号:
lib库名.后缀.版本号
比如系统的标准库路径下:
此处,符号链接的作用不是“快捷方式”,而是为了可以让动态库在升级版本的时候更加方便地向前兼容。一般而言,完整的动态库文件名称是:
lib库名.so.主版本号.次版本号.修订版本号
比如: libx.so.1.3.2
当动态库迭代升级时,其版本号会发生相应的改变。比如下面的版本更迭:
- 2021年3月08日发布:
libx.so.1.0.0 - 2021年4月02日发布:
libx.so.1.0.1 - 2021年4月23日发布:
libx.so.1.0.2 - 2021年5月18日发布:
libx.so.1.0.3 - 2021年8月09日发布:
libx.so.1.1.0 - 2021年9月12日发布:
libx.so.1.1.1
可以看到,修订版本号的更迭会比较频繁,次版本号次之,主版本号再次之。为了避免每次版本号的修改而重新编译,动态库一般会用一个只带主版本号的符号链接来链接程序,如:
ls -l
lrwxrwxrwx 1 root root 15 Jan 16 2020 libbsd.so.0 -> libbsd.so.0.8.7
-rw-r--r-- 1 root root 80104 Jan 16 2020 libbsd.so.0.8.7
这样一来,未来不管版本号如何变迁,只要主版本号不变,那么用户链接的库名永远都是 libbsd.so.0,而无需关心具体某个版本。而如果连主版本号都发生了改变,这一般是因为库不再向前兼容,比如删除了某些原有的接口,这种情况下,用户就需要重新编译程序。
十一、动态库的制作
不管是静态库还是动态库,都是用来被其他程序链接的一个个功能模块。与静态库一致,制作动态库的步骤如下:
- 将
*.c编译生成*.o - 将
*.o编译成动态库
例如:
ls
a.c b.c
# 第一步:将源码编译为 *.o
gcc a.c -o a.o -c -fPIC
gcc b.c -o b.o -c -fPIC
ls
a.c b.c a.o b.o
# 第二步:将 *.o 编译为动态库
gcc -shared -fPIC -o libx.so a.o b.o
ls
a.c b.c a.o b.o libx.so
十二、动态库的使用
动态库的编译跟静态库并无二致,如:
pwd
/home/gec
ls lib/
libx.so
gcc main.c -o main -L./lib -lx
- 说明:
- -L 选项后面跟着动态库所在的路径。
- -l 选项后面跟着动态库的名称。
运行时链接
动态库的最大特征,就是编译链接后程序并不包含动态库的代码,这些程序会在每次运行时,动态地去寻找并定位其所依赖的库文件中的模块,这是他们为什么被称为动态库的原因。
也就是说,如果程序运行时找不到动态库,运行就会失败,例如:
./main
./a.out: error while loading shared libraries:
libmyFunc.so:
cannot open shared object file:
No such file or directory
出现上述错误的原因,就是因为运行程序main时,无法找到其所依赖的动态库 libx.so,解决这个问题,解决该问题的方向大致分为两个方向:
把库文件存入系统默认寻找的路径:
操作系统默认会在两个目录下寻找
- /lib
- /usr/lib
因此我们只需要把库文件存入以上路径中的某一个即可
告诉操作系统我们的库存在于那里:
1.编译时预告:
gcc main.c -o main -L. -lx -Wl,-rpath=/home/gec/lib
2.设置环境变量:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/gec/lib
注意:
该方法只是一个临时设置,如果需要永久设置则需要把以上指令写入到配置文件中,让他每一次启动新终端都自动执行(~/.bashrc)
3.修改系统默认库路径(修改配置文件):(不推荐)
sudo vi /etc/ld.so.conf.d/libc.conf #打开配置文件
############以下为配置文件的内容###############
# libc default configuration
/usr/local/lib
/usr/lib/EvenLib/ # 在末尾增加了自定义的库文件的路径
~
~
~
~
# 保存退出后通过ldconfig 来重新刷新配置文件的内容(重新生效)
sudo ldconfig
在以上文件中,添加动态库所在路径即可。
注意: 此处要小心编辑,一旦写错可能会导致系统无法启动,这是一种 污染 系统的做法,不推荐。
十三、动态加载工程问题
提出这么一个问题:A公司为B公司的一条自动化流水线开发一个检测装置,B公司要求可以检测流水线最终的产品是否合格。具体是:
- 第一版需要检测产品的涂层颜色是否均匀、外观是否有破损两个指标。
- 系统上线运行后,支持B公司可以自主增加新的检测项目。
这个工程需求,简单地讲就是需要A公司开发的检测系统,能自动链接目前尚未出现的、未来的接口,这就需要A公司不是开发出检测外观、涂层颜色等具体功能的软件,而是要给B公司提供一个具备可拓展的软件“框架”,使得B公司后续可以按照自己的实际需求来拓展检测装置的功能。
十四、动态加载库
动态库最大的优点,是将链接推迟到运行时,由于运行时才链接动态库,这就给链接的目标留下了选择的空间。结合以上工程需求,可以让程序在运行的时候,为其指定要链接的动态库,以达到可以按需链接动态库的目的,这种做法称为动态库的动态加载。
具体做法如下:
- 约定好函数接口,比如
void detection() - 将各个不同需求的实现代码封装到不同的库中,比如
libcolor.so、libshape.so - 编写相应配置文件,指定程序在启动后要链接的具体的库
相关API接口:
#include <dlfcn.h>
函数原型:
void *dlopen(const char *filename, int flags);
int dlclose(void *handle);
参数分析:
filename --> 目标库文件名字
flags --> 动态库加载的时机
RTLD_LAZY 延迟加载(符号被调用时加载)
RTLD_NOW 立即加载(打开后立即加载符号地址)
返回值:
成功返回句柄
失败返回NULL
在库中寻找指定的符号(函数)地址:
#include <dlfcn.h>
函数原型:
void *dlsym(void *restrict handle, const char *restrict symbol);
参数分析:
handle --> 指定在哪一个库中寻找(dlopen返回的句柄)
symbol --> 执行需要寻找的目标符号名(函数名)
返回值:
成功 返回该函数的地址
失败 返回NULL
示例代码:
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(int argc, char **argv )
{
// 根据配置文件打开指定的库
void *handle = dlopen( argv[1] , RTLD_NOW);
if(handle == NULL)
{
printf("加载动态库[%s]失败:%s\n", argv[1] , strerror(errno));
exit(0);
}
// 定义一个未知接口的指针
void * (*detect)(void*);
// 在库中查找事先约定好的接口
detect = dlsym(handle, "detect");
if(detect == NULL)
{
printf("查找符号[%s]失败:%s\n", "detect", strerror(errno));
exit(0);
}
// 潇洒地调用该接口
detect(NULL);
}
关于编译于使用:
编译时在某些系统中可能需要手动连接指定的动态连接库:
gcc main.c -ldl
运行时:
已当前的示例,运行时需要把具体使用的库文件通过命令行参数进行传递,以及需要把对应的库文件存入到指定的路径中(/lib 或者 /usr/lib )
./a.out libcolor.so
假装在检测颜色均匀程度..
./a.out libdamaged.so
假装在检测外观破损程度..
十五、动态库版本管理基本背景
动态库与静态的最大区别在于,动态库是支持动态升级(不需要重新编译程序就可以获得新的版本功能)的,即应用程序在第一次编译链接之后,在库文件没有做出不向前兼容的改版(主版本号发生了变更)的情况下,应用程序可以随着动态库的升级而获得新版特性且无需重新编译。
动态库版本
一般而言,动态库的全称形如下面所示:
libx.so.1.2.3
其中,版本号的顺序是 主版本号.次版本号.修订版本号,对于上面的例子而言:
- 主版本号是1
- 次版本号是2
- 修订版本号是3
主版本号的修订一般发生在重大变更的时候,因为主版本号的变更会导致所有依赖于此动态库的应用程序需要重新编译,比如库发生了库接口的重大变更导致不再向前兼容,或是库接口出现安全漏洞要求用户升级。
次版本号的修订一般发生在出现较大升级、且向前兼容的时候,比如新增了系列新的功能、修复了原有接口的BUG等。
SONAME
在动态库的升级过程中,版本号会发生变化,而链接器为了能动态链接最新的动态库文件,编译和运行阶段就不能直接面向具体的带全版本号的库文件名,而必须面向比较稳定的名称,比如只包含主版本号的所谓SONAME,SONAME可以理解为包裹在真实库文件外层的、对外可见的链接名称:
对外SONAME和内部实际库名
修订版本号一般发生在比较小且必要修订的时候,修订版本的变化相对比较频繁。
由于 SONAME 是不变的,且与实际库名相关联,于是在链接运行阶段,应用程序就可以通过固定的 SONAME 来链接正确的库文件,那么如何生成库文件的 SONAME 呢?假设有一个功能文件 a.c,现想要将其制作成动态库 liba.so.1.0.0 并带主版本号的 SONAME,编译指令如下:
gcc -shared -fPIC a.c -o liba.so.1.0.0 -Wl,-soname,liba.so.1
ls -l
总用量 20
-rw-rw-r-- 1 gec gec 84 Nov 16 19:17 a.c
-rwxrwxr-x 1 gec gec 15664 Nov 17 17:49 liba.so.1.0.0
指令说明:
gcc: 编译器名称-shared-fPIC: 编译生成位置无关的动态库-oliba.so.1.0.0: 指定生成动态库为liba.so.1.0.0-Wl,-soname,liba.so.1: 传递给链接器的参数,指明库文件的SONAME为liba.so.1(注意,逗号后面不能有空格)
此处,动态库文件 liba.so.1.0.0 是实际库名,但对于链接器而言,它所使用的是该动态库对外可见的 SONAME,此处被设定为 liba.so.1,这个属性可以通过命令 readelf 来获得:
ldconfig
由于程序在运行阶段,会以 SONAME 为依据来动态寻找动态库,因此必须产生一个 SONAME 软链接文件,并将其设置为指向实际库文件,有两个方式可以达到这个目的:
方式一,直接建立软链接:
ln -s liba.so.1.0.0 liba.so.1
ls -l
总用量 36
-rw-rw-r-- 1 gec gec 85 Nov 17 20:11 a.c
lrwxrwxrwx 1 gec gec 13 Nov 17 18:49 liba.so.1 -> liba.so.1.0.0
-rwxrwxr-x 1 gec gec 15664 Nov 17 17:49 liba.so.1.0.0
方式二,通过ldconfig自动建立软链接(推荐):
ldconfig -n .
ls -l
总用量 36
-rw-rw-r-- 1 gec gec 85 Nov 17 20:11 a.c
lrwxrwxrwx 1 gec gec 13 Nov 17 18:49 liba.so.1 -> liba.so.1.0.0
-rwxrwxr-x 1 gec gec 15664 Nov 17 17:49 liba.so.1.0.0
指令说明:
ldconfig -n .: 在当前目录下搜索所有动态库的SONAME,并为之创建软链接文件。
可以看到,两种方式的效果是完全一样的,但是方式一需要手工书写各个库名、SONAME等细节,非常容易出错,并且在指定目录中如果存在多个库版本时,方式一无法自动匹对SONAME自动建立各个正确的软链接文件,因此,推荐使用ldconfig来自动管理动态库及其SONAME。
编译阶段
有了 liba.so.1.0.0 和 liba.so.1 还不够,因为编译的时候需要指定没有任何版本信息的库文件,因此还需要创建一个不带版本的软链接文件,让其指向库文件的 SONAME,比如:
ln -s liba.so.1 liba.so
这样一来,就形成了三个如下关系的文件:
动态库、SONAME 和软链接
这就是动态库的完整的标准命名规范,如下图所示:

浙公网安备 33010602011771号