背景
在实践中,我们一定会使用别人的库(不限于C、C++的库),在实践中,我们会使用成熟、被广泛使用的第三方库,而不会花费很多时间自己造轮子,为了能更好地使用库,就要在学习阶段了解其本质。那么对于库而言,可以从两方面认识它:
- 如果想自己写一个库呢?(编写者)
- 别人应该如何使用我们写的库?(使用者)
作为C、C++的使用者,应该知道它是一门编译型语言,一堆源文件(.cpp)和一堆头文件(.h)要合在一起才能生成一个可执行程序(.exe):
预处理: 头文件展开、去注释、宏替换、条件编译等,生成.i文件
编译: 词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,生成.s文件
汇编: 将汇编指令转换成二进制指令,生成.o文件
链接: 将生成的各个.o文件进行链接,生成可执行程序(Windows:.exe,Linux:.out)
例如: 用test1.c,test2.c以及main1.c形成可执行文件,需要先得到各个文件的目标文test1.o,test2.o以及main1.o,然后将这些目标文件链接起来,最终形成一个可执行程序。
实际上,对于可能频繁用到的源文件,比如这里的test1.c test2.c 可以将他们的目标文件test1.o,test2.o进行打包,之后需要用到这两个目标文件就可以直接链接这个包当中的目标文件即可,上面的打包就可以称为一个库。库的本质就是一堆.O文件集合,库的文件当中并不包含主函数而只是包含了大量写好的方法以供调用,因此,我们说动静态库是可执行程序的"半成品" -》所以库中没有main函数
认识静态库
站在编写者角度:生成静态库
以下面四个文件和一个main.c文件为例,演示其打包为库的过程
Add.c
1 int Add(int x, int y)
2 {
3 return x + y;
4 }
Add.h
extern int Add(int x, int y);
Sub.c
1 int Sub(int x, int y)
2 {
3 return x - y;
4 }
Sub.h
extern int Sub(int x, int y);
gcc -o Test Add.c Sub.c main.c
//我们使用 gcc -o 选项将所有的源文件进行编译,形成可执行
我们不建议上述方法,建议是将所有的源文件编译成.o文件,再将所有的.o文件链接形成我们的可执行
在 Makefile 中,$< 是一个自动化变量,表示规则中的第一个依赖文件(prerequisite)。
通过使用 $<,你可以方便地引用规则中的第一个依赖文件,使规则更加灵活和通用。
总结上面的操作也就是
gcc Add.o Sub.o main.o -o Test
//将别人的.o文件与自己写的main.c文件形成的.o文件链接起来,形成可执行,这个就是静态库的打包
通过上面的例子我们知道,需要将生成的所有目标文件和main.o文件链接才能生成可执行程序,但是除了main.o之外的.o文件都太分散了,用起来很麻烦(当然可以通过Makefile简化步骤),给别人使用也不太方便,还容易缺失,所以将它们打包。而将目标文件打包的结果就是一个静态库
使用ar指令将所有目标文件打包为静态库
ar 命令是 GNU Binutils 的一员,可以用来创建、修改静态库,也可以从静态库中提取单个模块。它可以将一个或多个指定的文件并入单个写成 ar 压缩文档格式的压缩文档文件。
例如,将Add.o和 Sub.o打包:
ar -rc libtest.a Add.o Sub.o
那么上面这个文件夹中就只有打包的库文件和头文件(因为头文件是公开的,我们通常可以看到stdio.h里面的内容)
但是,我们使用gcc编译的时候为什么报链接错误呢?
因为自己写的库是第三方库,gcc不认识,所以需要指定-l链接我们自己写的第三方库
gcc main.c -ltest
但是链接的时候又出现错误,不能找到这个库,所以需要提供一个选项-L,说明这个库是在哪个路径底下
gcc main.c -ltest -L.
站在使用者角度:打包静态库
我们将头文件和.o文件分别放在一个文件夹下的两个文件夹中
然后将这个文件夹的压缩包给别人,用的时候就是将include里面的内容拷贝到/user/include
目录底下,将lib里面的内容拷贝到/lib64/
目录下。或者指定库的路径
指定库的路径
动态库
依然使用之前的四个文件和一个main.c文件示例。
- 生成所有源文件对应的目标文件
gcc 需要增加-fPIC选项(position independent code):位置无关码
gcc -fPIC -c Add.c
gcc -fPIC -c Print.c
位置无关代码对于 gcc 来说:
- -fPIC作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
- 如果不加-fPIC选项,则加载. so 文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个. so 文件代码段的进程在内核里都会生成这个. so 文件代码段的拷贝,并且每个拷贝都不一样,这样就和动态库一样占用内存了,具体取决于这个. so 文件代码段和数据段内存映射的位置。
- 不加-fPIC编译生成的. so 文件是要在加载时根据加载到的位置再次重定位的,因为它里面的代码 BBS 位置无关代码。如果该. so 文件被多个应用程序共同使用,那么它们必须每个程序维护一份. so 的代码副本 (因为. so 被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)。
我们总是用-fPIC来生成. so,但从来不用-fPIC来生成. a。但是. so 一样可以不用-fPIC选项进行编译,只是这样的. so 必须要在加载到用户程序的地址空间时重定向所有表目。
- 使用 gcc 的 -shared 选项将所有目标文件打包为一个动态库
gcc -shared -o libtest.so Add.o Print.o
动态库的使用
对于动态库,即使显式地提示 gcc main.c 中调用了第三方库中的函数,也会因为找不到库而编译错误。
例如,仍然使用 gcc 的三个选项说明编译 main.c 需要的库文件和头文件,以及应该链接哪个库。注意,此时的工作目录依然是:
gcc main.c -I./mylib/include -L./mylib/lib -ltest
不同于静态库,这里动态库生成的可执行程序并不能运行。
- 拷贝到系统目录
sudo cp mylib/lib/libtest.so /lib64
- 建立动态库的软链接
ln -s mylib/lib/libtest.so libtest.so
- 更改 LD_LIBRARY_PATH环境变量
LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量中,告诉系统程序依赖的动态库所在的路径:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/2024_1_28/dy_test/my_lib/lib
注意要用:隔开,否则会覆盖原来的环境变量。但是这个方法是临时的,因为这个环境变量是内存级别的环境变量,机器会在下次登录时清理。