引言:库在我们日常工作中会经常用到,也经常听到有人提起静态库与动态库,那么本文将了解动态库与静态库的区别,学习制作静态库与动态库以及如何使用它们!
什么是库文件呢?
所谓库文件,大家可以将其等价为压缩包文件,该文件内部通常包含不止一个目标文件(也就是二进制文件)。库文件中每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块,以便提供给使用者一些可以直接拿来用的变量、函数或类。
所以,其实库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。例如,C 语言库文件提供有大量的函数(如 scanf()
、printf()
、strlen()
等),C++ 库文件不仅提供有使用的函数,还有大量事先设计好的类(如 string
字符串类)。
那什么是又是库呢?为什么需要库?
库,即程序库,是特殊的一种程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行,必须作为其它执行程序的一部分来完成某些功能。程序库可分静态库(static library)和共享库(shared library)。
库的好处:
代码保密:将源文件打包成库分发给别人使用可以保护源码的实现不被公开,起到保密作用(详细可以看下文关于库文件与头文件的分析)。
当然,有人可能会想到,可以使用反编译的工具反编译库文件,就可以获得源码,实际上,对于 Java 而言,反编译的还原度比较高,甚至可以达到95%以上,但对于 C/C++语言,反编译的还原度比较低,可以起到保护作用。
提高开发效率:库的存在可以使得程序模块化,可以加快程序的再编译,可以实现代码重用,可以使得程序便于升级,极大的提高了程序员的开发效率,因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。
方便部署、分发和使用:我们将打包好的库文件方便、快速的分发给别人使用,并且库文件的调用方法也很简单,以 C 语言中的 printf() 输出函数为例,程序中只需引入
库文件与头文件
我们在分发库文件给别人使用时,往往需要还需要提供相应的头文件。有人可能会问,调用库文件为什么还要牵扯到头文件呢?
实际上,头文件和库文件并不是一码事,它们最大的区别在于:头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分。大家可以这样理解:所有的库文件都提供有相应的头文件作为调用它的接口。也就是说,库文件是无法直接使用的,只能通过头文件间接调用。
头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。
事实上,库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。既然是目标文件,所以库文件用于程序的链接阶段,通常编译器提供有 2 种实现链接的方式,分别称为静态链接方式和动态链接方式:
采用静态链接方式实现链接操作的库文件,称为静态链接库、静态库。
静态库在程序的链接阶段被复制到了程序中。
采用动态链接方式实现链接操作的库文件,称为动态链接库、动态库。
动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
静态链接
静态链接库实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。
优点:
缺点:
运行前就加载程序文件中,所以生成的程序比较大,需要更多的系统资源,在装入内存时会消耗更多的时间。
如果程序文件中多次调用库中的同一功能模块,则该模块代码势必就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。
更新、部署、发布麻烦,即如果库有了更新,必须重新编译整个源程序文件。比如,静态库更新了,分发给别人后,需要把更新的库和头文件重新编译,使用者的应用程序需要重新编译部署。
动态链接
动态链接库,又称为共享链接库。和静态链接库不同,采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,GCC 编译器不会直接将该功能模块的代码拷贝到文件中,而是将功能模块的位置信息记录到文件中,直接生成可执行文件。
显然,这样生成的可执行文件是无法独立运行的。采用动态链接库生成的可执行文件运行时,GCC 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。
所以:动态链接时,链接器在链接时仅仅建立与所需库函数的之间的链接关系,在程序运行时才将所需资源调入可执行程序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pQPgTBQE-1658820801128)(assets/
)]
优点:
由于可执行文件中只记录的是功能模块的地址,真正的实现代码并没有放入程序文件中,所以相对于静态编译,有着较小的程序体积。
实现进程之间的资源共享(避免重复拷贝):由于可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,这意味着,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库称为共享链接库的原因)。
更新、部署、发布简单,简化了程序的升级:比如,动态库更新了,分发给别人后,只需要把更新的库重新编译即可,使用者的应用程序不需要重新编译部署。
缺点:
GCC 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,GCC 编译器才会选择相应的静态链接库。如果两种都没有(或者 GCC 编译器未找到),则链接失败。
什么时候使用静态库,什么时候使用动态库?
建议如果库比较少,编译建议使用静态库,如果库比较大,编译建议使用动态库。
静态库命名规则:
在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示,libxxx.a;
在 Windows 系统中,静态链接库文件的后缀名为 .lib,libxxx.lib。
(1) 静态库制作
步骤1:将 c 源文件生成对应的 .o 文件,只进行汇编不进行链接,【注意】头文件和 main.c 不需要要汇编
yxm@192:~/calc$ ls
add.c div.c head.h main.c mult.c sub.c
yxm@192:~/calc$ gcc -c add.c -o add.o
yxm@192:~/calc$ gcc -c sub.c -o sub.o
yxm@192:~/calc$ gcc -c mult.c -o mult.o
yxm@192:~/calc$ gcc -c div.c -o div.o
yxm@192:~/calc$ ls
add.c add.o div.c div.o head.h main.c mult.c mult.o sub.c sub.o
步骤2:使用打包工具 ar 将准备好的 .o 文件打包为 .a 文件 libxxx.a:
ar rcs libxxx.a xxx.o xxx.o
yxm@192:~/calc$ ar -rcs libcalc.a add.o mult.o sub.c div.o
yxm@192:~/calc$ ls
add.c add.o div.c div.o head.h libcalc.a main.c mult.c mult.o sub.c sub.o
yxm@192:~/calc$ rm add.o div.o mult.o sub.o add.c div.c mult.c sub.c
yxm@192:~/calc$ ls
app head.h libcalc.a main.c
(2)静态库使用
静态库制作完成之后,需要将 .a 文件和头文件一起发布给用户(具体原因请参看本文前面部分),用户再引用静态库编译成可执行文件 。
假设用户的测试文件为 main.c,静态库文件为 libcalc.a,编译命令如下:
yxm@192:~/calc$ ls
head.h libcalc.a main.c
yxm@192:~/calc$ gcc main.c -I ./ -L ./ -lcalc -o app
yxm@192:~/calc$ ls
app head.h libcalc.a main.c
yxm@192:~/calc$ ./app
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
参数说明(详细参考Linux 下 GCC 编译常用总结):
-L:表示要链接的库所在目录,即 libxxx.a 所在的目录。
-I(大写 i): 表示库文件对应的头文件所在的目录。
-l(小写L):指定链接时需要的库名,即 libxxx.a 去掉前缀和后缀之后的部分。
【注意】-l 与后面的库名可以有空格,也可以没有空格,两个方式都对。
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程
动态库命名规则:
在 Linux 发行版系统中,动态链接库的后缀名通常用 .so 表示,libxxx.so;
在 Windows 系统中,动态链接库文件的后缀名为 .lib,libxxx.dll。
(1)动态库制作
步骤一:gcc 生成 .o 目标文件,此时要加编译选项:-fPIC(fpic也可)
gcc -c –fpic/-fPIC a.c b.c
参数:-fPIC 创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。
yxm@192:~/calc$ ls
add.c div.c head.h main.c mult.c sub.c
yxm@192:~/calc$ gcc -c add.c -o add.o -fPIC
yxm@192:~/calc$ gcc -c sub.c -o sub.o -fPIC
yxm@192:~/calc$ gcc -c mult.c -o mult.o -fPIC
yxm@192:~/calc$ gcc -c div.c -o div.o -fPIC
yxm@192:~/calc$ ls
add.c add.o div.c div.o head.h main.c mult.c mult.o sub.c sub.o
步骤二:生成共享库,此时要加链接器选项: -shared(指定生成动态链接库)
gcc -shared a.o b.o -o libcalc.so
yxm@192:~/calc$ gcc -shared add.o div.o mult.o sub.o -o libcalc.so
yxm@192:~/calc$ ls
add.c add.o div.c div.o head.h libcalc.so main.c mult.c mult.o sub.c sub.o
(2)动态库使用测试
静态库制作完成之后,需要将 .so 文件和头文件一起发布给用户(具体原因请参看本文前面部分),用户再引用动态库编译成可执行文件(编译方法跟静态库方式一样) 。
假设用户拿到 .so 文件和头文件后,测试文件目录如下:
yxm@192:~/library$ tree
.
├── include
│ └── head.h
├── lib
│ └── libcalc.so
├── main.c
└── src
├── add.c
├── div.c
├── mult.c
└── sub.c
3 directories, 7 files
编译命令(编译方法跟静态库方式一样)如下:
yxm@192:~/library$ gcc main.c -I include/ -L lib/ -lcalc -o main
yxm@192:~/library$ ls
include lib main main.c src
yxm@192:~/library$ ./main
./main: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
然后运行:./main,发现竟然报错了!!!
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道动态库的绝对路径。此时就需要系统动态载入器(dynamic linker/loader),即 ldd。通过 ldd 可以查看可执行文件的依赖的动态库。
yxm@192:~/library$ ldd main
linux-vdso.so.1 (0x00007ffdb9fc1000)
libcalc.so => not found # 没有找到可执行文件的依赖的动态库
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4c15eb4000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4c162a5000)
既然需要动态库绝对路径,那如何定位共享库文件呢?
对于elf格式的可执行程序,是由ld-linux.so来完成的,它先后搜索elf文件的 DT_RPATH段 ——> 环境变量LD_LIBRARY_PATH ——> /etc/ld.so.cache文件列表 ——> /lib/,/usr/lib目录找到库文件后将其载入内存。详细方式参考下文:如何让系统找到动态库。
(3)如何让系统找到动态库
DT_RPATH段 由操作系统定义,不能改变,所以只能同通过修改环境变量 LD_LIBRARY_PATH、/etc/ld.so.cache文件列表和**/lib/,/usr/lib**目录的方法来让系统找到动态库。
方法一:临时设置,修改环境变量 LD_LIBRARY_PATH 的值:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径
,$LD_LIBRARY_PATH 表示旧的 LD_LIBRARY_PATH 值,: 表示追加新值。
yxm@192:~/library$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yxm/library/lib
yxm@192:~/library$ ldd main
linux-vdso.so.1 (0x00007fffad786000)
libcalc.so => /home/yxm/library/lib/libcalc.so (0x00007efdad23e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efdace4d000)
/lib64/ld-linux-x86-64.so.2 (0x00007efdad440000)
yxm@192:~/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
【注意】这种方法运行成功后,如果关闭终端,再重新打开一个新的终端,会发现系统依旧无法找到动态库,因为是在终端中配置 LD_LIBRARY_PATH 值,终端一旦关闭,配置也将清除,所以无法找到动态库。
方法二:永久设置,把 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径
,设置到 ~/.bashrc 或者 /etc/profile 文件中
# 用户级设置环境变量
yxm@192:~/library$ vim ~/.bashrc
# 在 .bashrc 文件的最后一行添加如下内容:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yxm/library/lib
yxm@192:~/library$ ./bashrc # 设置环境变量生效,相当于 source /bashrc
yxm@192:~/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
# 系统级设置环境变量
yxm@192:~/library$ vim /etc/profile
# 在 profile 文件的最后一行添加如下内容:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yxm/library/lib
yxm@192:~/library$ source /etc/profile # 设置环境变量生效
yxm@192:~/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
方法三:修改 /etc/ld.so.cache 文件列表,但是 ld.so.cache 是一个二进制文件,所以不能直接修改,但是可以间接修改,即将动态库文件所在目录的路径添加到 /etc/ld.so.conf 文件中。
yxm@192:~/library$ sudo vim /etc/ld.so.conf
# 在 ld.so.conf 文件的最后一行添加如下内容:
/home/yxm/library/lib
yxm@192:~/library$ sudo ldconfigs # 设置生效,该命令会重建/etc/ld.so.cache文件
yxm@192:~/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
方法四:拷贝自己制作的共享库到 /lib 或者 /usr/lib 下(不是/lib64目录),不过这种方法不推荐使用,因为这两个目录本身就自带了一些系统的库文件,如果把自定义的动态库文件放到这两个文件夹下,容易导致命名冲突,可能会替换掉系统自带的文件,导致系统的程序的运行可能出现问题。
学了本文,你可以充分了解静态库与动态库的区别,学会制作并使用静态库与动态库。其实静态库与动态库的制作与使用是 GCC 使用的进一步扩展(如果需要了解 GCC 基础使用请移步 Linux 下 GCC 编译常用总结),由于比较重要所以单独用一篇文章展现,与大家一起共勉,不足之处请指正。