Linux:动静态库详解
1. 什么是库
库(Library),就是一组预编译好的、可复用的目标文件集合,里面封装了实现特定功能的代码(比如数学计算、字符串处理、IO 操作等),供其他程序直接调用,避免重复造轮子。
这是对于库的定义,大家在平常的代码练习中,肯定都写过 #include <stdio.h> 或者 #include <iostream> ,这样的头文件,其实这就是库的一部分,并且也使用过头文件包含的,也是库里的函数,比如 printf 函数等等。但是我们对库的了解也到此为止了,所以说,库对于我们来说就像是最熟悉的陌生人。
今天我们就来谈一谈库到底是什么?
首先我们来看看,平时我们写的代码,怎么知道这段代码里面使用了哪些库:

对于上面这段代码,我们使用 ldd 命令查看到这段代码依赖的是 libc.so.6 库函数,我们去查找一下这个文件所在的路径:

经过前面的学习,我们就能看懂这里的 /libc.so.6 实际上是指向 libc-2.17.so 的软链接文件,而这个 libc-2.17.so 就是我们平时使用的C语言标准库的动态库文件。关于动静态库我在前面的文章中提到过:https://blog.csdn.net/2502_91842264/article/details/159547300?fromshare=blogdetail&sharetype=blogdetail&sharerId=159547300&sharerefer=PC&sharesource=2502_91842264&sharefrom=from_link
这里面也说明了,对于一个库的完整命名规则是:lib[ 库名称 ] .so / .a ,所以对于 libc-2.17.so 这个文件名称来说,实际上真正的库名称是 c-2.17 。并且C/C++在用gcc/g++编译器编译的时候,默认使用的是动态链接。

2. 静态库
2.1 静态库生成
在 Linux 编程学习场景里,有一天老师布置了编写一个可执行程序的作业。张三已经写好了实现的功能代码,此时李四因为沉迷打游戏没有写作业,就问张三要源代码打算抄一下。但张三既不想把.c源码直接发给李四,怕对方原样照搬抄袭,也担心被老师查出两人互相传抄作业。于是张三没有分享源代码,而是把功能文件编译成了.o目标文件。
这种文件是二进制形式,看不到原始代码,却完整保留了函数的调用逻辑。随后张三将.o文件发给李四,李四只需自己编写程序主框架,再把自己的代码和张三提供的.o文件链接起来,就能顺利生成完整的可执行程序。
而在这个过程中,李四只获得了来自于张三的 .o 文件,并没有看到源代码是怎么实现的,李四相当于只是单纯调用了一下张三给他提供的函数,这不就是相当于我们平时调用库函数的场景吗?因此我们可以得到一个结论:静态库本质上就是众多.o 目标文件的集合体。


我们来举个例子,对于这个目录我们有两个 .o 文件,现在将它编译链接成一个静态库:

这里的 ar -rc 是 Linux 下专门用来创建静态库(.a 文件)的归档命令,ar 全称 archive(归档),作用就是把多个 .o 目标文件打包、合并成一个静态库文件。后缀的 *.o 就表示所有 .o 为后缀的文件。其中的 -rc 表示:
-r:replace,表示将指定文件加入归档文件,如果归档文件已存在,就替换旧文件
-c:create,表示如果静态库文件不存在,就自动创建它
所以 -rc 会组合在一起,实现 “ 不存在则创建,存在则替换更新 ” 的效果。
此时形成的 libmystdio.a 就是静态库文件。

然后我们创建一个新的目录friend,再把这个静态库文件拷贝到friend目录里,此时我们在我们自己的main.c函数里编写一个printf函数,然后再编译成 .o 文件,最后使用 gcc -o target main.o -lmystdio -L./ 这个命令,表示我们要依赖 main.o 和静态库文件 mystdio 去链接形成一个可执行程序 target ,这样的话就可以在 friend 目录下使用来自于 lesson11 目录中实现的函数。
2.2 静态库使用
我们先修改一下Makefile文件:

修改之后我们就能使用make output指令去创建一个目录 mylib ,然后将所有的 .h 和 .a 文件都放到该 mylib 目录下,这就相当于打包了一个库,这样我们如果想把库给别人使用,就直接拷贝过去即可。那么别人该如何使用呢?

首先第一种方法,就是将头文件以及静态库文件分别存到系统的默认查找的文件当中,这样在编译链接时系统就可以直接查找到对应的文件。相当于把这个库安装到Linux系统中了 。

接着我们再直接编译:

要注意这里加了一个 -lmystdio ,是告诉 gcc 编译器要链接的是 mystdio 这个库文件,平时我们不写是因为默认链接的就是 C语言库文件 。
第二种方法可以不用将文件安装到系统中,而是在该目录下直接使用:
![]()
首先是 -I (大写的 i),这是告诉编译器除了要在默认文件下找头文件,还要在 ./mylib/include/ 这个路径下找,其次是 -L ,也是告诉编译器还要再 ./mylib/lib/ 路径下找 静态库 ,最后是 -l(小写的 l),表示具体依赖的是哪个库。
第三种方法就是使用软链接的形式,将当前静态库的路径与 /lib64/ 中设置的一个文件进行软链接,那么当操作系统默认在/lib64中查找库的时候,就可以直接找到该路径。这个方法和安装到系统中其实差不太多,我就不演示了。
3. 动态库
3.1 动态库生成

对于动态库的生成,我们使用这一份Makefile,其中动态库的后缀是 .so ,并且动态库也叫做共享库,所以 -shared 就表明生成的是动态库,另外 -fPIC 意思是产生与位置无关码,这个概念我们后面再讲。

对于动态库的生成和静态库其实大同小异,并且 make output 的逻辑也是差不多的,上图中我们就生成了打包好的动态库文件 libmystdio.so 。
不过这里有一个问题,我们在形成动态库的时候,使用的是 gcc -shared -o ,但是形成静态库的时候使用的是 ar -rc ,为什么这两个不一样?
这是因为:我们前面提到过,所谓的静态库(.a),本质就是将多个编译完成的.o目标文件进行简单的归档与打包,也就是说静态库就是一个 .o 文件的集合。它不具备独立的可执行程序格式,仅仅是把零散的二进制目标文件整合到一个文件中,方便统一管理和使用。静态库在链接阶段只会被链接器将代码片段复制到可执行程序中,本身不需要被操作系统加载运行,也不涉及复杂的地址重定向、动态加载等处理。因此我们不需要使用编译器进行编译链接,只需要 Linux 系统提供的归档工具ar即可完成。
而动态库(.so)与静态库有着本质的区别,它是一个可以被操作系统在程序运行时动态加载到内存中的独立程序模块,必须严格符合系统的可执行文件格式,支持地址无关、动态链接、运行时加载等特性。也就是说:对于动态库来说,程序在编译链接时不会把库中的代码复制进去,只有当程序运行过程中真正需要使用库函数时,才会将动态库加载进来并调用。
因此,动态库需要经过编译器的编译、链接等完整处理流程,完成地址重定向、符号解析等关键工作,才能被系统正常识别和加载,那就必须使用gcc编译器。-shared参数的作用就是告诉编译器,当前生成的是一个共享式的动态库,而非普通的可执行程序。
这也从侧面说明了,我们在平时工作环境中,动态库会使用的更多。
3.2 动态库使用

我们将生成的动态库分享给 friend 目录,并用指令生成了可执行程序 main ,但是当我们调用的时候却报错了,报错内容是找不到 libmystdio.so 这个文件!?这是怎么回事,我们明明把动态库文件传过去了,执行程序的时候怎么还能说找不到呢?
这是因为:指令中的 -L 只作用于 “ 编译链接阶段 ” ,不作用于 “ 程序运行阶段 ” 。
在编译链接时,-L ./mylib/lib/ 只是临时告诉编译器:当前去这个目录找动态库,完成链接后,可执行文件内部只会记录动态库的名字 libmystdio.so,不会记录它的路径 ./mylib/lib/。
而我们说了,动态库是在程序运行过程中,如果遇到要使用动态库中文件或者函数的时候,就顺着路径去找,但是因为没有记录指定动态库的路径,所以默认只在系统固定路径 /lib、/lib64、/usr/lib 中查找,不会去我们自定义的 ./mylib/lib/ 目录查找,因此就会报出找不到 libmystdio.so 的错误。
解决办法可以像静态库一样,直接把库的路径放到系统默认查找的路径下,也可以使用软链接的方式。除此之外,还可以使用环境变量。
![]()
这个环境变量是告诉系统:除了默认的 /lib、/lib64 这些系统库目录以外,运行程序时,也去我指定的目录里找动态库(.so 文件)。

至此,我们的程序就可以运行了。
还有另外一种方式,是要使用 /etc/ld.so.conf.d/ 目录,它是 Linux 系统中专门用来存放动态链接器配置文件的系统目录,作用是永久告诉系统动态链接器,去哪里查找动态库(.so 文件)。
我们前面提到过的 LD_LIBRARY_PATH 是临时生效的环境变量,终端关闭或重启后就会失效;而放在 /etc/ld.so.conf.d/ 里的配置,是全局永久生效,所有用户、所有程序运行时都会识别。系统的动态链接器(ld-linux.so)在启动时,会自动读取 /etc/ld.so.conf 主配置文件,而该文件会递归包含 /etc/ld.so.conf.d/ 目录下所有以 .conf 结尾的文件。每个 .conf 文件里只需要写入自定义的动态库路径,系统就能永久识别。
配置完成后,必须执行 sudo ldconfig 命令,这条命令会扫描所有配置的库路径,生成并更新系统动态库缓存 /etc/ld.so.cache。之后系统运行任何程序时,都会优先从缓存中快速查找动态库,无需反复遍历所有目录。
4. 一些小问题
知道了静动态库的生成和使用之后,我们还需要解决几个小问题。
首先,如果说现在同时有动态库和静态库,那么链接的时候会是什么样的场景呢?就像这样:

我们前面提到了,想要程序可以链接上库,要使用这个命令: gcc -o target main.o -L./mylib/lib/ -I./mylib/include/ -lmystdio ,最后一个项目是表明我们要依赖的是 mystdio 这个库文件,但是我们的动静态库的库名都是一样的,那操作系统会怎么做呢?

我们会发现使用命令之后,正常生成了可执行程序文件,并且该程序也能正常运行。然后使用 ldd 命令查看到我们的可执行程序,发现依赖的是动态库。
这也就印证了,如果我们给用户同时提供动静态两种库,操作系统默认会优先使用动态库。
如果想要指定就使用静态库的话,就要在gcc -o target main.o -L./mylib/lib/ -I./mylib/include/ -lmystdio这个命令后加上 -static 。但是如果提供的只有动态库没有静态库,此时还想要强制使用 -static表明必须用静态库的话,就会发生报错。
不过,如果提供的只有静态库没有动态库,在链接的时候只能采用静态链接的方法:

但是在使用 ldd 命令查看的时候,发现链接的还是 libc.so.6 这个C语言标准动态库,是因为: libmystdio.a 仅包含我们自己实现的自定义函数,在链接阶段已被完整复制到可执行文件中,因此运行时无需依赖; 而我们自定义库的代码中调用了printf等系统标准 C 库函数,gcc 默认动态链接系统 C 库libc.so.6,该库为动态库,运行时需要动态链接器加载,因此ldd会检测到该依赖。
本文到此结束,感谢各位读者的阅读,如果有讲解错误或者不对的地方,欢迎各位读者批评或指正。
更多推荐




所有评论(0)