本篇要谈论的内容是关于动静态库的问题,具体的逻辑框架是建立在库的制作,库的使用,和库的原理来展开,基于上述的三个模块来对动静态库有一个较为清楚的认知
在前面的学习中知道,在用户写完代码后,想要将写完的代码转换成可以执行的可执行程序过程是一个相当复杂的过程,那么在这段过程中要处理的过程基本有,例如预处理,编译,汇编,链接,可执行程序,而在学习编译工具gcc的时候又提到过,在代码进行链接的过程中,系统必须要提供对应的动静态库,因此就引入了动静态库的概念
所谓动态链接,就是让程序和库产生这种地址性的关联,而静态库,就是把目标的库文件直接拷贝到对应的可执行程序当中
对于静态库,第一步要先引入的是静态库的原理,静态库就是在进行编译链接的过程中,把静态库中所包含的代码都拷贝到可执行程序中,之后在可执行程序的运转就不需要静态库了
对于动态库,原理是在程序运行的时候才会链接动态库的代码,多个程序会共享使用库的代码,而一个与动态库链接的可执行文件只有一个函数入口地址的表,而不是整个文件的内容,上述是关于动态库的基本原理,关于这些内容后面就进行一个一个的解析
首先,创建出对应的文件,这里假设要实现一个计算器,那么实现对应的函数声明和实现过程:
#include "Add.h"
#include "Sub.h"
#include "Mul.h"
#include "Div.h"
int main()
{
int a = 10;
int b = 20;
printf("%d + %d = %d\n", a, b, Add(a, b));
printf("%d - %d = %d\n", a, b, Sub(a, b));
printf("%d * %d = %d\n", a, b, Mul(a, b));
return 0;
}
那么现在准备工作就完成了,下面的问题是,我想要编译生成一个可执行程序,应该输入什么指令呢?
gcc -o test.exe test.c Add.c Sub.c Mul.c Div.c
这样就完成了编译,生成了一个可以被运行的可执行文件,那么下面对于上面的现象提出一些问题
1. 为什么在编译的时候不带头文件?
因为头文件所属的位置就在当前路径下,编译器是直接可以找到的,如果头文件对应的查找路径是在当前目录或者是指定的目录,是不需要写在编译选项中的
2. 上面的步骤是否每一步都需要?
这个问题问法很奇怪,怎么说是每一步都需要,假设现在要生成的是一个可执行程序,其实根本不太需要把源文件全部编译生成一个可执行文件,因为这样的过程需要经过预处理编译汇编链接等等,而实际上在编译这样的多文件项目的过程中,只需要把源文件都编译为.o后缀的文件,再将这些文件进行链接形成一个可执行文件,这是被倡议的一种链接方式
这也就是在任何项目中,都会存在这样的文件的原因,在进行编译生成可执行文件的过程中,如果是直接将已经生成的这些.o后缀的文件进行一定的链接组合,就能生成可执行程序
基于上面的原因,我们重新进行一次编译,这次按照上述的过程生成对应的.o文件即可,为了方便后续进行其他的使用,写一个Makefile来自动进行编译比较好:
%.o:%.c
gcc -c $<
Test:Add.o Sub.o Mul.o Div.o test.o
gcc -o $@ $^
.PHONY:clean
clean:
rm -rf *.o Test
那么上面就是实现了Makefile,但是和前面写的不太一样,这个Makefile是直接将这些.o后缀的文件生成了一个可执行程序,所以现在就要先生成这样的.o后缀的文件,借助gcc编译工具就可以生成,之后就可以运行出结果了
这里补充一点上面写的这个语句,%.c的意义就是类似于一种通配符,因为后续生成test可执行文件是依赖于.o文件的,但是它们都不存在,那么此时就需要根据依赖关系来进行推导,而在推导的过程中,当Makefile在进行被编译的时候,就会把%.c全部展开,之后就会进行不断的推导,展开成四个gcc的编译方式语句,而这里的$<表示的是把文件依赖列表中的内容一个一个的传递到下面的命令中,最后连起来,就能解析成对应的内容了
那么现在的问题是,如果源文件已经不需要了,而是只需要这些.o为后缀的文件,也就是说,把这些文件进行打包,作为一个库,而把这个包交给使用者后,使用者就只需要写出自己的.c文件,再编译成.o文件,就能和我刚才打包好的包直接进行链接,就省去了前面的很多步骤,像这样的过程就是前面所述的核心观点,基于这样的原因,现在当前的主要任务就是生成一个库
那么就对Makefile进行改造,改造的核心思路就是基于上述的这一系列原理,将文件编译成.o文件,再将文件整合到一个固定的地方,这就是库的概念
上面的思路原理存在,下面的一个问题是,直接把.o文件存到一个固定的位置,显然是不太合适的方式,如果此时有几百个文件呢?如果也是一个一个的进行转移,那么可能会有所遗漏,这都是不被建议和允许的,基于这样的原因,有了打包的概念
ar指令
ar -rc $@ $^
上述就是在Linux中打包的指令,ar命令就是把所有的源文件进行打包形成对应库文件的过程,其中这个rc表示的是replace和create的意思,表示的是如果不存在就创建,存在就替换,总之这样就可以形成一个完整的.a文件
下面要做的就是生成一个库文件,库文件的生成也能放到Makefile中来写,具体的书写过程如下
# 库的名字是mymath,是个静态库
static-lib=libmymath.a
# 生成库需要Add.o Sub.o Mul.o Div.o,实现方式是ar指令
$(static-lib):Add.o Sub.o Mul.o Div.o
ar -rc $@ $^
# 生成.o文件需要把.c文件按照下面的gcc编译选项一个一个生成($<)
%.o:%.c
gcc -c $<
# 建库
.PHONY:output
output:
mkdir -p mymath_lib/include
mkdir -p mymath_lib/lib
cp -f *.h mymath_lib/include
cp -f *.a mymath_lib/lib
# 清空内容
.PHONY:clean
clean:
rm -rf *.o *.a mymath_lib
上述的完整Makefile进行推导解析:第一行表示这是一个静态库,静态库的名字叫做mymath,前面的lib和后面的.a都是前缀和后缀,静态库真正的名字叫做mymath,而后面对于这个静态库的生成方式有了一个定义,静态库的生成依赖的是后面的四个.o文件,而具体的生成方式是ar指令,那么此时Makefile就会进行推导,Makefile现在需要.o文件,但是现在没有,所以在后面就提到了生成.o文件的过程,是利用.c文件来生成的,这样就完成了Makefile的过程,后面的两个操作就是建库和清除的过程
执行结果如下:
这样就完成了一个静态库,那么接下来要进入的问题是,库的使用问题
进入这个话题,就意味着现在我们已经有了静态库,但是这个库怎么用呢?
朴素做法
现在已经有了库,别人给我提供了这些方法,我该如何使用?所以就用到了这些头文件提供的函数,但是现在如果直接编译会发现根本编译不过去,说明就现在而言,还是不可行的
原因在于什么呢?从报错信息来看,找不到这里对应的头文件,原因在于这些库文件都是被保护起来的,现在在本地编写之后的代码是无法找到对应的内容,说明现在还得想办法把这些库文件都让编译工具能够找见才可以,那么就把头文件都放到代码所在的目录中,再进行编译:
此时报错信息是,没有定义,说明现在已经找到对应的头文件了,但是没有找到定义头文件的地方,这个就叫做链接报错,那我写的这个库为什么用不了呢?
对于我们自己写的库函数,都叫做第三方库,而gcc不认识第三方库,哪怕是就在当前路径下,也依旧不认识这个第三方库,因此就引出了要链接库的概念,所以就要引出一个选项,大I
-I 选项表示的意义是link,也就是链接指定的一个库,也就是说告诉编译器,你在进行编译的时候要使用这个库,所以执行下面的指令
gcc test.c -I mymath_lib/include/ -l mymath -L mymath_lib/lib
上面这一串是很长的指令,但是不急,一点一点的分析
首先是,要进行编译的对象是test.c,后面的这个选项表示的是新增头文件的搜索路径,后面紧跟着的就是头文件的搜索路径,而后面的小l表示的是指明链接的库名称,而大L表示的是新增库文件的搜索路径,基于上面这么一长串的选项,就能最终编译出来我们想要的结果,事实上也确实生成了,这说明我们的静态库已经使用成功了
之前我们使用的C标准库从来不需要指定,而此时为什么这里就需要指定了呢?因为这里我们自己实现的叫做第三方库,而gcc是专门用来处理C语言的编译工具,所以在进行编译的时候会直接到指定的路径下去寻找,gcc已经认识了C语言提供的官方库,而我们自己实现的第三方库它并不认识,即使看见了也不认识,需要我们主动的为gcc和自己写的库建立起合适的联系,才能让他们之间认识,编译器才能进行工作编译链接等等的后续操作,最终生成一个可执行程序
因此得出的结论是,未来我们把我们写的静态库提供给别人去使用,只需要把对应的.h头文件和对应的.a文件交给别人就够了,其中这个.a文件就是我们前面所说的.o文件的集合
那么下一个问题是,当使用ldd指令去查看依赖关系的时候,却发现一个问题
问题是,我们生成的这个a.out并不依赖我们写的库文件,这是因为在默认的情况下,可执行程序都是动态链接,因此ldd指令只能查询动态库,而静态库在编译期间就已经被拷贝到可执行程序当中了,因此也就查不到对应的信息,静态库是无法检查的
这里引出一个结论:gcc默认采取的是动态链接,但是对于个别库来说,如果你只提供.a的方式,那编译器也无能为力,只会把内容局部性的作为静态链接,而其他库则采取的是正常的的动态链接,如果带有 -static选项,那就必须要采取静态链接的方式了
所以说,在使用gcc进行编译的时候,如果这个程序依赖10个库,那么gcc就会尽量的把这10个库对应的.so文件都拿到,但是如果没有动态库也没关系,还可以去拿静态库
加入现在需要某个库,我们从网上去下载,得到了库,下一步应该安装库,那如何安装库?实际上就是把对应的头文件和库文件都安装到系统当中,怎么安装到系统?本质上就是把对应的文件安装到usr路径下的include路径和lib路径下,所以说,安装的本质,就是把头文件和库文件分别拷贝到系统的指定路径下,只要拷贝到gcc的默认路径下,那么gcc在进行搜索的过程就不是问题
关于动态库如何制作呢?其实也和静态库类似,从原理上将和静态库都相同,都是在源文件编译成.o文件后,给这些个文件进行打包,就形成了动态库,区别是,在形成对应的.o文件时,需要带上一个fPIC的选项,这个选项的意思是与位置无关码,至于这个是什么意思在后续会有讲解,这里只需要知道是这样的原理即可,具体原因主要是因为,动态库本身没有把内容拷贝到可执行程序当中去,因此动态库和可执行程序之间只是地址方面的关联,因此使用了动态链接后,只是告诉了可执行程序,你所需要的内容在哪里,在哪一个文件的什么位置,你需要的时候自己去找就可以,那么这个过程就叫做动态链接,所以在使用的时候只需要带上一个fPIC就可以了,之后再对生成的.o文件进行打包,使用的命令还是gcc命令,生成一个.so的文件,但是要带上-shared选项,表示的这个文件我想要生成的是一个共享库,也叫做是动态库
不管是在Linux中还是Windows中也好,动态库是比较重要的,形成动态库不需要额外的工具,只需要gcc就可以帮助我们完成这个过程,从这个角度也能看出,形成动态库的方法直接内置到了编辑中,但是静态库没有做出对应的内置,这也就说明动态库的重要性,那么下面对于Makefile进行对应的改造,生成我们所需要的动态库:
# 库的名字是mymath,并且是个动态库
dy-lib=libmymath.so
# 动态库的生成方式是用gcc,带上编译选项,直接编译就可以
$(dy-lib):Add.o Div.o Mul.o Sub.o
gcc -shared -o $@ $^
# 生成动态库所需要的.o文件需要依赖于.c文件生成,并且也需要带上特殊选项,表示的是与位置无关码
%.o:%.c
gcc -fPIC -c $<
# 整体将生成的内容进行打包
.PHONY:output
output:
mkdir -p mymath_lib_so/include
mkdir -p mymath_lib_so/lib
cp -f *.h mymath_lib_so/include
cp -f *.so mymath_lib_so/lib
# 对部分内容做出清理
.PHONY:clean
clean:
rm -rf *.o *.so mymath_lib_so
如果想对于这个动态库把它安装到系统中,那么就需要放到指定的路径下,那么现在我们先不对于它做出任何操作,只是和静态库一样来尝试编译它,结果是:
事实上,用静态库的编译方式来对动态库进行编译,也成功了,说明到现在为止和静态库比起来没有任何区别,那么接下来接续:
在运行程序的过程中失败了,不过这也是可以预见的,因为静态库相当于直接把内容拷贝进去了,而动态库只是告诉你该去哪找,而在运行的时候,程序并不知道去哪找,所以找不见,这样的结果也是意料之内的
动态库和你的可执行程序是分离的,是两个文件,当执行程序的时候,程序需要被加载到内存,而库中的文件也要能够被系统找到,也是要加载到内存中,所以说,动态链接的程序,程序和库文件是分开的,在进程进行加载的过程中,程序库也要被找到并且加载,只有程序库被加载了,才能跑的起来,动态链接非常依赖动态库
那么如何能找到对应的内容呢?下面讨论的就是这个问题
1. 直接安装到系统中
这个是简单粗暴的方法,当然也是简单可行的,看下面的操作
此时运行程序,就可以运行起来了,因为进程在运行的过程中可以在默认路径下找到我们所需要的内容,就能把这些内容加载到内存中供进程使用,调度等等
如果把对应的文件从对应的库中删除,那么就不能再使用了:
2. 软链接的方式
说明这也是可行的,同时查看ldd情况,发现确实是存在链接情况
这个方式也很好理解,默认的寻找方式是到lib64下去寻找,然而在这里可以通过修改环境变量,使得环境变量中新增一个配置变量,这样就能继续寻找了,具体操作过程如下:
但是环境变量的修改是临时的,这样的修改只在当次生效,当下次重新登陆就不存在了,这是因为环境变量在每次登陆时,都会由配置文件对其进行修饰,所以最根本的方法,其实是修改配置文件,这样就能每次都修改成功环境变量
4. 修改配置文件
在Linux中,动态库的配置文件的位置在 /etc/ld.so.conf.d/
那么修改的原理就是在这里新增一个文件,在文件中写入我们需要的地址就可以了!
具体操作展示如下:
所以往后你自己需要使用动态库,不管是用别人的还是你自己写的库文件,如果运行是找不到,那么这里就有四种做法
如果同时同一组方法动静态库同时被提供,那么默认使用的是动态库,并且同一组库会提供动静态两种方式,gcc默认使用的是动态库,如果想使用静态库,就带上-static选项