目录
一、什么是库
我们先来回顾一下之前的内容:
我们在编译我们写的C语言文件的时候分为以下4个步骤:
- 预编译:完成头文件的展开,去掉注释,宏替换等,形成的是xxx.i文件。
- 编译:进行词法、语法和语义的分析,将代码翻译为汇编指令,形成的是xxx.s文件。
- 汇编:将汇编指令转换成二进制指令,形成的是xxx.o文件。
- 链接:将生成的各个xxx.o文件链接,形成的就是可执行程序了。
这里有个简单记忆的方法iso(对应的是相机的感光度)。
我引入这个概念就是为了指出我们生成的xxx.o文件实际上是可进行打包的,这样别人在用的时候就只要xxx.o一链接就可以执行你自己写的代码了,这样做不仅方便而且不会直接暴露出你的源代码。
实际上我们的库是有两种的:
静态库:.a[Linux]、.lib[windows]
动态库:.so[Linux]、.dll[windows]
敲黑板:
动静态文件例如:libmgc.a
去掉前缀lib和后缀.a才是库文件的名字。
二、认识动静态库
我们可以写个代码来见一见:
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
接下来我们来看看这个代码生成的可执行文件所依赖的库文件:
命令:
ldd test
这个libc.so.6的文件就是我们的程序所依赖的库文件了,我们可以看看这个文件的属性:
这里我们可以看到,我们的库文件实际上是一个对libc-2.17.so的软链接文件,我们可以看看它的类型:
命令:
file /lib64/libc-2.17.so
我们可以看到实际上这个文件是一个共享的库文件,也就是动态库。
那么我们应该怎样拿到一个是依赖静态库的可执行程序呢?
这个时候我们就要在编译的时候在文件的末尾加上-static的选项了:
这个时候我们再用ldd命令来查看的时候就会显示不再是动态可执行:
我们也是可以用file命令来确定文件类型的:
这里有个细节就是我们可以明显的看到依赖静态库的可执行文件明显比依赖动态库的可执行文件大:
三、静态库
📝静态库的生成
我们为了方便理解,我们设置两个函数来验证,声明和实现分离,分别是:
printhello.h
1 #pragma once
2
3 extern void printhello();
printhello.c
1 #include "printhello.h"
2
3 void printhello()
4 {
5 printf("hello\n");
6 }
printworld.h
1 #pragma once
2
3 extern void printworld();
4
printworld.c
1 #include "printworld.h"
2
3 void printworld()
4 {
5 printf("world\n");
6 }
打包
第一步:
让所有源文件汇编生成.o文件:
第二步:
使用ar命令将所有目标文件打包形成静态库
ar命令实际上是gnu的归档工具, 是为了将目标文件打包成静态库:
选项:
- -r(replace):如果静态库的文件中有更新,就替换掉原来的旧文件
- -c(create):建立静态库
代码:
ar -rc mylibc.a *.o
我们还可以通过ar命令来查看静态库中的文件:
选项:
- -t:列出静态库中的文件
- -v:显示详细信息
ar -tv mylibc.a
组织
我们要将我们打包好的静态库给别人用就要组织好,我们可以将两个头文件放到include的目录下,将静态库放到mylib目录下:
makefile的实现
这里我们其实是可以用我们之前学的makefile脚本来实现的,具体代码如下:
我们写好脚本并保存后就可以执行make,就可以生成对应的源文件,然后进一步生成对应的库文件了。
这里我们还可以执行make construct来组织好对应的文件,方便我们来使用。
📝静态库的使用
我们实现好我们自己写的库之后,我们就可以来使用它了,我们可以创建一个test.c文件来作为我们测试的源文件:
#include <stdio.h>
#include <printhello.h>
int main(){
printhello();
return 0;
}
如图,我们现在在目录下面只有有我们的所在库的目录和源文件:
使用方法一:
我们先来介绍几个gcc编译时的几个选项:
- -I(大写的i):指定头文件搜索路径。
- -L:指定库文件搜索路径。
- -l(小写的L):指明需要链接库文件路径下的哪一个库。
于是我们就可以这样来编译我们的源程序:
敲黑板:
- 1.因为编译器不知道我们的头文件在哪,所以要指定好我们的头文件。
- 2.因为头文件只有我们的声明,并没有真正实现,所以我们还要有库文件的路径。
使用方法二:
我们不仅可以自己组织,也可以直接交给操作系统组织,也就是直接拷贝到对应的目录里面:
我们拷贝到对应的文件目录下之后,就可以不用指定我们的头文件路径和库文件路径了,但是我们还是要指定是哪个库。
我们执行之后就和之前写的一样了:
四、动态库
📝动态库的生成
我们这里还是使用上面的这四个文件来做实验:
第一步:我们首先要生成对应的目标文件
这个时候我们就要引入一个新的选项——fPIC:
作用:产生位置无关代码
解释一下:
- 我们先从字面上来理解,与位置无关的代码也就是说编译器在生成对应的代码时不会依赖被加载到内存中的特定位置,这里的不依赖的地址也就是绝对地址,真正使用的时相对地址,也就是说代码可以被加载到内存的任意位置都可以执行。
- 如果我们在构建.so文件的时候不加上这个选项的话,我们的系统时候报错的:relocation R_X86_64_PC32 against symbol `printhello' can not be used when making a shared object; recompile with -fPIC
- 这里不加这个选项的话,代码可能会尝试访问绝对路径,这样是不安全的,而且代码也不可复用。
第二步:使用选项-shared将所有目标文件打包成一个动态库
gcc -shared -o libmyc.so printhello.o printworld.o
第三步:将我们的头文件和我们的库文件组织起来
和我们上面实现静态库一样,为了方便其他人使用,我们这里将头文件放在include的目录下面,将生成的libmyc.so文件放在mylib目录下面,然后将两个目录放在lib目录下面:
查看目录结构如下:
makefile的实现
我们make一下就可生成对应的目标文件和动态库了:
我们make constract就可以组织好我们的文件了:
📝动态库的使用
我们这里可以创建一个之前就写好的main.c问价来举例子:
#include <stdio.h>
#include <printhello.h>
int main(){
printhello();
return 0;
}
我们当前的目录里面就有之前创建的动态库文件和刚刚创建的main.c文件
这里我们写好之后和之前在使用静态库时是一样的,我们也是用-I(大写的i)来指定头文件的搜索路径,用-L选项来指定文件的搜索路径,最后用-l(小写的L)来指明需要链接的库文件路径下的那个库。
gcc main.c -I ./bin/include -L ./bin/mylib -lmyc
但是我们会发现这个时候的生成的可执行文件,实际上是不可执行的:
这里我们要注意的是,我们在使用指定文件的命令的时候,实际上这个告知编译器文件路径的编译阶段,而在执行可执行文件的时候是不会有效果的,操作系统也就不知道可执行文件所依赖的动态库,我们也可以使用ldd命令来进行查看。
那么我们在这个情况下如何去执行我们的可执行文件呢?
我们这里其实是有三个解决办法的:
第一种方法:拷贝.so文件到系统的共享库路径下面
我们这里相当于借用了系统之手来完成我们的操作。
sudo cp bin/mylib/libmyc.so /lib64
这样操作之后我们就可以运行我们的程序了:
第二种方法:设置环境变量LD_LIBRARY_PATH
LD_LIBRARY_PATH是程序运行状态查找库时候所要搜索的路径,我们将这个动态库的目录路径添加到这个环境变量中去就可以了。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xywl/Blog2/lib/mylib
这个时候我们再用ldd命令来查看这个可执行程序可以发现,系统现在就可以找到所依赖的动态库了。
我们现在有可以正常执行我们的可执行程序了。
第三种方法:配置/etc/ld.so.conf.d/
我们可以配置这个路径来解决问题,这个路径下面存放的都是以.conf后缀的配置文件,而这些配置文件存放的都是对应的路径,系统会自动在在这个路径下找出所有配置文件的路径,之后就会在每个路径下查找你所需要的库。我们只需要将自己需要的路径放到该路径下就可以找到了。
我们首先要将我们的库文件所在目录的路径放在以.conf为后缀的文件当中。
然后我们可以将这个文件拷贝到/etc/ld.so.conf.d/的目录下面。
但是我们用ldd命令查看可执行程序,我们发现我们的可执行程序并没有找到可以依赖的动态库。
这个时候我们就需要使用ldconfig命令来将我们的配置文件更新一下,更新后就又可以找到我们所依赖的动态库了。
sudo ldconfig
此时我们就可以正常执行我们的可执行程序了。