[Linux]:动静态库
✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:Linux学习
贝蒂的主页:Betty’s blog
1. 动静态库的介绍
一般而言,库分为动态库和静态库。
- 在
Linux
当中,以.so
为后缀的是动态库,以.a
为后缀的是静态库。- 在
Windows
当中,以.dll
为后缀的是动态库,以.lib
为后缀的是静态库。
静态库在程序编译时被链接到目标代码中。一旦链接完成,静态库的代码就成为目标程序的一部分。这意味着如果多个程序都使用了同一个静态库,那么每个程序都会包含一份该库的副本,从而导致程序体积较大。
优点:
- 独立性强,不依赖外部环境,因为库代码已经被包含在程序中。
- 运行时加载速度相对较快,因为不需要在运行时进行库的加载操作。
缺点:
- 生成的程序体积较大。
- 如果静态库有更新,需要重新编译链接所有使用该库的程序。
动态库在程序运行时被加载。多个程序可以共享同一个动态库,只有当程序运行时才会将动态库加载到内存中。这大大减小了程序的体积,同时也方便了库的更新和维护。
优点:
- 生成的程序体积较小,因为库代码没有被包含在程序中。
- 库的更新不影响已编译的程序,只需要更新动态库文件即可。
缺点:
- 依赖外部环境,运行时需要确保动态库存在且路径正确。
- 加载动态库可能会带来一定的时间开销。
2. 动静态库的原理
我们知道,一个源文件变为一个可执行文件将经历四个步骤:
- 预处理: 完成头文件展开、去注释、宏替换、条件编译等,最终形成
xxx.i
文件。- 编译: 完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成
xxx.s
文件。- 汇编: 将汇编指令转换成二进制指令,最终形成
xxx.o
文件。- 链接: 将生成的各个
xxx.o
文件进行链接,最终形成可执行程序。
比如我们现在有test1.c
,test2.c
,test3.c
,以及main1.c
这四个.c
文件,经过预处理,编译,汇编之后分别生成test1.o
,test2.o
,test3.o
,以及main1.o
这四个.o
文件。最后经过生成a.out
的可执行文件。
但是此时我们的main2.c
文件的生成同时也需要依赖test1.c
,test2.c
,test3.c
这三个文件,生成可执行程序的步骤都是一样的。此时我们就可以选择将test1.c
,test2.c
,test3.c
这三个文件生成的test1.o
,test2.o
,test3.o
进行打包,之后再使用时,只需要链接这个"包"即可,这个"包"其实就是我们常说的库。
所以动静态库的本质其实是一堆xxx.o
文件的集合。对于库的使用,只需要提供头文件让使用者了解具体功能的作用。在编译程序时,通过链接指定的库来实现对库中功能的调用。
3. 动静态库的使用
在Linux
下,我们可以通过ldd 文件名
来查看一个可执行程序所依赖的库文件。这其中的libc.so.6
就是该可执行程序所依赖的库文件,我们通过ls命令可以发现libc.so.6
实际上只是一个软链接。
实际上该软链接的源文件libc-2.17.so
和libc.so.6
在同一个目录下,为了进一步了解,我们可以通过file 文件名
命令来查看libc-2.17.so
的文件类型。
通过上图观察,我们知道gcc/g++
编译器默认都是动态链接的,如果想使用静态链接,需要在后面加一个-static
。如果你并没有安装对应的静态库的话,可以使用以下指令安装。
sudo yum install glibc-static
sudo yum install libstdc++-static
其中需要注意的是:动静态库真实文件名需要去掉前缀lib
,再去掉后缀.so
或者.a
及其后面的版本号,比如说libc-2.17.so
就是C语言的标准库,其名为:c-2.17
。
4 动静态库的打包
为了方便更加深入理解动静态库,接下来我们以下文件为例,讲解一下我们如何将我们的文件打包成动静态库。
其中add.h
的内容如下:
#pragma once
extern int add(int x, int y);
其中add.c
的内容如下:
#include "add.h"
int my_add(int x, int y)
{
return x + y;
}
其中sub.h
的内容如下:
#pragma once
extern int sub(int x, int y);
其中sub.c
的内容如下:
#include "sub.h"
int sub(int x, int y)
{
return x - y;
}
4.1 静态库的打包
然后我们需要将add.h
,add.c
,sub.h
,sub.c
这个文件打包成静态库。
- 首先第一步将源文件生成对应.o文件。
- 第二步使用ar指令打包成对应的静态库。
其中ar
指令用法为ar 选项 库名 打包文件名
,其中又两个关键选项:
-r
(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件-c
(create):建立静态库文件
- 将头文件和生成的静态库组织起来。
当把自己的库提供给他人使用时,通常需要给予两个文件夹:
- 一个文件夹用于存放头文件集合。比如,可以将
add.h
和sub.h
这两个头文件放置在名为include
的目录下。- 另一个文件夹用于存放所有的库文件。例如,把生成的静态库文件
libmath.a
放到名为lib
的目录下。
最后,将这两个目录(include
和lib
)都放置在mathlib
目录下,此时就可以把mathlib
提供给别人使用了。
为了方便我们处理,我们可以写一个Makefile
。
libmath.a:add.o sub.o
ar -rc libmath.a $^
%.o:%.c #展开所以.c文件生成对应的.o文件
gcc -c $^
.PHONY:clean
clean:
rm -rf ./*.o mathlib
.PHONY:output #发表库
output:
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./*.h mathlib/include
mv ./*.a mathlib/lib
4.2 静态库的使用
我们如果使用我们打包的静态库,在使用gcc
编译时需要带有以下三个选项:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。
由于在程序执行时,编译器并不知晓我们所声明的头文件以及链接库的具体位置,而且链接库中可能存在不同的库文件。因此,我们需要在命令行中指定头文件的搜索路径,库文件的搜索路径,以及具体使用哪个库。
比如我们需要执行main.c
,其中main.c
中使用静态库中的add
函数。
#include<stdio.h>
#include"add.h"
int main()
{
int a=1;
int b=2;
int ret=add(a,b);
printf("%d\n",ret);
return 0;
}
其中需要注意的是,-I
,-L
,-l
这三个选项后面可以加空格,也可以不加空格。
那么我们就有个疑问,那就是我们使用gcc
编译文件时为什么没有带-I
,-L
,-l
这三个选项呢?
其实很简单,因为我们之前使用的库都默认在系统的路径下: 编译器能准确识别这些存在于配置文件中的路径。其实如果为了方便我们也可以将头文件和库文件拷贝到系统路径/usr/include
,/lib.64
下:
- sudo cp mathlib/include/* /usr/include/
- sudo cp mathlib/lib/* /lib.64/
这时再使用gcc
编译时就只需要带-l
选项,指明链接库文件下具体哪个库。
但是实际上,我们并不推荐将自己写的头文件和库文件拷贝到系统路径下,因为这样做可能会对系统文件造成污染。
4.3 动态库的打包
动态库的打包相对于静态库较为复杂,但大致相同,我们还是利用add.h
,add.c
,sub.h
,sub.c
这四个文件进行打包演示:
- 首先第一步将源文件生成对应.o文件。
但是与静态库不同的是,需要带-fPIC
选项,因为动态库运行时才会被加载。
<font style="color:rgb(28, 31, 35);">-fPIC(position independent code)</font>
即产生位置无关码,作用于编译阶段,其目的是告诉编译器生成与位置无关的代码。在这种情况下,所产生的代码中不存在绝对地址,全部采用相对地址(起始位置加上偏移量)。这使得动态库被加载器加载到内存的任意位置时都能够正确执行。倘若不添加该选项,代码中使用的库函数在执行时会尝试调到对应位置执行,但此时可能会因该位置被其他动态库所占用而找不到该函数。
- 第二步:使用-shared选项将所有目标文件打包为动态库。
生成对应的动态库并不需要使用ar
指令,还是使用gcc
编译,只不过需要带-shared
选项。
- 将头文件和生成的动态态库组织起来。
与静态库类似,当把自己的库提供给他人使用时,通常需要给予两个文件夹:
- 一个文件夹用于存放头文件集合。比如,可以将
add.h
和sub.h
这两个头文件放置在名为include
的目录下。- 另一个文件夹用于存放所有的库文件。例如,把生成的静态库文件
libmath.so
放到名为lib
的目录下。
最后,将这两个目录(include
和lib
)都放置在mathlib
目录下,此时就可以把mathlib
提供给别人使用了。
同样为了方便管理,我们也可以定义一个Makefile
文件。
libmath.so:add.o sub.o
gcc -shared -o $@ $^
%.o:%.c
gcc -fPIC -c $<
.PHONY:clean
clean:
rm -rf ./*.o mathlib
.PHONY:output
output:
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./*.h mathlib/include
mv ./*.so mathlib/lib
4.4 动态库的使用
我们如果使用我们打包的动态库,使用gcc
编译时同样需要带有以下三个选项:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。
因为在程序执行时,编译器同样并不知晓我们所声明的头文件以及链接库的具体位置,而且链接库中可能存在不同的库文件。因此,我们需要在命令行中指定头文件的搜索路径,库文件的搜索路径,以及具体使用哪个库。
比如我们需要执行main.c
,其中main.c
中使用动态库中的add
函数。
#include<stdio.h>
#include"add.h"
int main()
{
int a=1;
int b=2;
int ret=add(a,b);
printf("%d\n",ret);
return 0;
}
但是与静态库不同的是,我们并不能直接执行a.out
这个可执行文件。
为什么使用了-I
,-L
,-l
这三个选项,还是没有找到对应的动态库呢?
这是由于我们使用
-I
、-L
、-l
这三个选项仅仅是在编译期间向编译器告知我们所使用的头文件和库文件的具体位置以及具体的库名。然而,当可执行程序生成后,它便与编译器不再有直接关系。所以,该可执行程序运行起来时,操作系统仍找不到该可执行程序所依赖的动态库。
为了解决这个问题,我们有三种方法:
- 第一种就是将库文件拷贝到系统共享的库路径下。
sudo cp mathlib/lib/libmath.so /lib64
但是这种方法可能会对系统文件造成污染,所以我们一般不采取该方法。
- 第二种就是更改环境变量LD_LIBRARY_PATH。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/beidi_113/tmp/mathlib/lib(对应动态库所在路径)
LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中,程序运行起来时就能找到对应的路径下的动态库。
但是我们知道环境变量在重启时会自动恢复,所以这种方法只在当前状态下有效,具有临时性。
- 配置.conf/文件
在系统中,/etc/ld.so.conf.d/
是用于搜索动态库的路径。此路径下存放的全是后缀为.conf
的配置文件,这些配置文件中所存放的内容都是动态库的路径。
因此,若将自己库文件的路径也放置在该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件。并且这种行为是永久的,并不会随重启而改变。
首先我们将对应的库文件所在地址写入一个.conf
文件中,然后将其导入/etc/ld.so.conf.d/
路径,最后使用指令ldconfig
更新一下配置文件,最后我们就能执行我们的可执行文件了。