目录标题
- 为什么会有动态库和静态库
- 静态库的实现
- 动态库的实现
- 动静态库的加载
为什么会有动态库和静态库
我们来模拟一个场景,首先创建两个头文件
根据文件名便可以得知add.h头文件中存放的是加法函数的声明,sub.h头文件中存放的是减法函数的声明,既然有头文件那么也应该存在对应的源文件,所以这里再创建两个源文件:
然后就在头文件中添加函数的声明,在源文件中添加函数的实现即可,那么这里的代码如下:
//add.h
#pragma once
#include<stdio.h>
extern int add(int x ,int y); //add.c
#include"add.h"
int add(int x ,int y)
{ printf("enter Add func, %d + %d = ?\n", x, y); return x+y;
}//sub.h
#pragma once
#include<stdio.h>
extern int sub(int x ,int y); //sub.c
#include"sub.h"
int sub(int x ,int y)
{ printf("enter Sub func, %d - %d = ?\n", x, y); return x-y;
}
有了这四个文件之后我们就可以再创建一个文件,并且使用这两个源文件的内容来实现一些功能,那么这里我们就创建一个main.c文件:
main.c中的代码如下:
#include"add.h"
#include"sub.h"
int main()
{ int x =20; int y=10; printf("result: %d\n",add(x,y)); printf("result: %d\n",sub(x,y)); return 0;
}
然后我们就可以使用gcc指令来生成一个名为test的可执行程序,比如说下面的图片:
运行一下这个可执行程序便会出现下面这样的场景:
那么这就说明代码的实现是正确的。在之前的学习中我们知道可以用-c选项来生成二进制文件,并且我们还可以使用二进制文件来生成可执行程序,比如说下面的操作:
并且可执行程序的运行结果也是一摸一样的:
因为二进制文件的内容正常人是看不懂的,所以我们可以利用这个特性来传播一些保密的资源,比如说有个东西我想让你使用但是不想让你知道这个东西的底层实现逻辑时就可以使用二进制文件来实现:
是不是看起来很难受对吧,所以这就可以起到一个保密的功能
假设folder1是功能的发明者folder2是功能的使用者,那么folder2要想使用这个功能就得知道这个功能是如何实现的,所以得将add.o和sub.o文件复制到folder2文件夹里面,然后再将main.c文件赋值到folder2文件夹里面,比如说下面的图片:
然后我们在folder2文件夹里面想要再生成一个可执行程序时就会爆出这样的错误:
可以看到这里说没有找到add.h文件,说明给别人方法时不仅得给别人.o文件还得给别人.h文件:
再来到folder2文件夹里面生成可执行文件时就可以看到成功了:
并且运行一下可以看到结果跟之前的是一模一样的:
也就是说未来别人要用我们实现的函数的话我们就可以提供给别人.o文件(方法的实现)和.h文件(都用什么方法),但是这里存在一个问题如果存在很多个.o文件的话这里在传递和使用的时候都非常的麻烦,所以我们就尝试着将所有的.o文件都打一个包形成一个库,这样在传递一个功能和方法的时候给对方一个库文件即可,库文件就是多个.o文件形成的一个文件,而采用不同的工具和方法生成的库就称为静态库和动态库,库的本质就是.o文件的集合,那么接下来我们就来看看如何生成静态库和动态库,并且这两个库会存在什么样的区别。
静态库的实现
使用ar指令来创建静态库,ar的全程是archive,使用方法ar -rc 生成的库文件名 源文件名
,这样就可以生成一个静态库,比如说当前路径下的文件如下:
接下来我们要完成makefile文件里面的内容,静态库的命名规则是lib开头以.a结尾所以库的名称为libmymath.a,库文件依靠add.o文件和sub.o文件,实现的方法是ar -rc libmymath.a add.o sub.o
,那么makefile的第一条指令的内容如下:
libmymath.a:add.o sub.o ar -rc $@ $^
但是当前路径下没有add.o文件和sub.o文件,所以我们还得添加这两个文件的指令和对应的实现方法,那么这里的代码如下:
libmymath.a:add.o sub.o ar -rc $@ $^
add.o:add.c gcc -c add.c
sub.o:sub.c gcc -c sub.c
通过前面的例子我们知道要想传递一个功能不仅得传递.o文件还得传递对应的.h文件,所以这里为了传递方便我们就创建一个文件夹,文件夹中存在两个小文件夹一个名为lib用来存放所有的库文件,一个名为include用来存放所有的头文件,所以这里再添加一个output指令该指令的实现方法就是创建一系列的文件夹,将所有的.o文件放到一个文件夹里面将所有的.h文件放到另外一个文件夹里面,那么这里的代码如下:
libmymath.a:add.o sub.o ar -rc $@ $^
add.o:add.c gcc -c add.c
sub.o:sub.c gcc -c sub.c
.PHONY:output
output:mkdir -p mylib/includemkdir -p mylib/lib cp -f *.h mylib/includecp -f *.a mylib/lib
最后就是clean指令,这个指令就删除当前路径下的所有.o文件和库文件就可以了,那么makefile的完整代码如下:
libmymath.a:add.o sub.o ar -rc $@ $^
add.o:add.c gcc -c add.c
sub.o:sub.c gcc -c sub.c
.PHONY:output
output:mkdir -p mylib/includemkdir -p mylib/lib cp -f *.h mylib/includecp -f *.a mylib/lib
.PHONY:clean
clean:rm -f *.o libmymath.a
输入make指令变可以看到当前路径下出现了几个文件:
file一下libmymath.a文件便可以看到他说这个文件是一个归档文件,
然后为了方便讲库文件和头文件的传递,我们还要输入一个make output指令来生成文件夹集合:
然后我们就可以使用tar指令将生成的文件夹进行打包,然后就可以把打包文件放到yum源上,这样别人就可以使用yum来进行下载,或者将这个软件放到某个网站上供别人下载:
假设下载完成就是将这个打包文件放到上级目录的folder3目录里面:
那么下载完做的第一件事情就是将打包文件进行解压得到内部的文件:
然后将这个文件夹里面的头文件都安装到系统的头文件目录里面也就是/usr/include/
,将所有的库文件也拷贝到系统的库文件里面也就是/lib64/
,那么上面拷贝的过程就是安装,所谓的安装就是将目标文件拷贝到系统指定的路径下,通过这个路径系统可以找到这些文件,那么这时我们有了库文件和头文件按道理来说在folder3文件夹里面就可以使用这些函数来执行一些功能,所以我们就再把main.c文件复制到folder3文件里面然后再使用该文件生成一个可执行程序比如说下面的操作:
但是生成可执行程序的时候又会出现问题:编译器找不到头文件,我们知道编译器找查找头文件的时候默认在两个地方进行搜索一个当前路径下搜索,一个是在系统指定的路径下进行搜索,我们没有将当头文件和库文件下载到系统路径了里面,所以当前的搜索方式是在当前路径下进行查找,虽然头文件在folder3文件夹里面可是头文件太深了并没有和main.c文件位于同一水平上,所以找不到目标文件那么要想解决这里的问题就得告诉编译器在什么位置下搜索头文件,所以得添加-I选项和指定查找的路径,比如说下面的操作:
这里依然会报错但是这里报错的原因是链接错误,说明头文件找到了但是库文件没有找到,但是之前敲代码的时候我们并没有告诉编译器库文件在哪里?那为什么也能编译通过呢?因为c和c++的库也是在系统的默认路径下,也就是lib64和/usr/lib下就存放着库文件,所以还得添加-L选项并加上库文件的路径,比如说下面的操作:
但是运行之后还会报错,因为链接第三方库的时候还得告诉库的名称,因为系统不知道第三方库叫什么,但是之前写代码的时候从来没有指明过库的名称为什么还能正常运行呢?原因很简单并不是你不指明就也能正常通过而是编译器自动帮你填写了,为什么c++的编译器叫做g++c语言的编译器叫做gcc,因为这些编译器知道你写的程序缺少什么库,但是对于第三方的库他就无法得知了,所以添加-l加上库的名字,这里的名字得去掉前缀和后缀,比如说下面的操作:
生成可执行程序的时候我们还可以发现这里的连接是动态链接并且查看链接库的时候并没有接我们创建的库,因为gcc默认是动态链接,生成一个可执行程序的时候会链接多个库所以这里就会出现问题,gcc默认是动态链接但是这是一个建议过程,对于一个特定的库究竟是动态还是静态库还是取决于你提供的是动态库还是静态链接,如果动静态库都给你了这里的选着权就来到了gcc上面,如果有100个库70个是动态库还有30个是静态库,那么库在链接的时候还是一个一个链接,但是只要有一个库是动态链接的这个可执行程序就是动态链接的。这里在生成可执行程序的时候还是太麻烦了如果想要减少指令的话这里采用的方法就是将头文件都拷贝到/usr/include/里面,再将库文件拷贝到/lib64/里面,那么这种行为就是安装的过程将目标文件拷贝到指定路径下面,但是运行起来还是会报错因为不知道要使用哪个库,所以即便拷贝好了也得告诉程序你使用的是哪个库。
动态库的实现
动态库也是和之前一样先用gcc生成.o结尾的二进制文件,但是这里生成二进制文件的时候得添加一个-fPIC选项,这个选项就是在生成.o文件的时候产生与位置无关码,比如说下面的操作:
然后就对所有的.o文件进行归档,但是这里的归档和静态库的指令不一样,动态库是用gcc指令加 -shared选项进行归档,比如说下面的操作:
这样就生成了一个动态库,同样的道理这里再创建一个文件夹,文件夹里面还有两个小文件夹一个用来存放头文件一个用来存放库文件,然后将这个大文件拷贝到folder3文件夹里面,比如说下面的操作:
然后也是同样的道理,生成可执行程序的时候会告诉我们找不到头文件:
所以得添加-I选项来指明具体的路径:
然后还得告诉编译器库文件在哪里,所以还得添加-L选项:
光找到库的位置不行还得告诉编译器库的名字叫什么所以这里还得添加-l选项指名库的名称:
可以看到这里确实生成了可执行程序但是我们运行一下这个程序时便会发现这里依然是有问题的,并且使用ldd指令查看该文件连接库的情况时会发现libmymath.so动态库根本没有连接上去:
这里的错误表示库没有链接上去没有找到库,但是我已经告诉了库的名称库的位置和头文件了为什么还找不到了,原因很简单你这里的告诉是跟gcc说的,程序在编译链接的时候还和gcc有关吗?没关系了程序在运行的时候需要依靠操作系统和shell,所以操作系统和shell也需要知道库在哪里,操作系统和shell只会去系统路径下进行查找,而我们刚刚写的文件并不在那些路径下所以就找不到,所以就会出现上面这样的问题,那么要想解决这里的问题就得让操作系统和shell找到动态库,那么这里就存在多个方法来解决这里的问题:
方法一:修改环境变量
环境变量LD_LIBRARY_PATH中记录了操作系统默认查找的路径,
只要我们把库所在的路径导入到该环境变量里面操作系统就会在该路径下搜索库,比如说下面的操作:
这样库所在的路径就会导入到环境变量里面,并且再运行一下上面的程序便可以发现没有问题了:
那么这就是方法一修改环境变量,这种方法存在一个问题就是不持久,因为每次登录进入系统都会更新一次环境变量所以该方法是不持久的。
第二种方法:安装
这种方法就是将动态库拷贝到/lib64里面,这样操作系统就可以从系统库中找到我们写的库,但是我们写的库不一定是安全的,所以这里就不展示该方法的实现了,大家也不要尝试这样的方法。
第三种:配置文件法
在系统中存在这么一个路径:/etc/ld.so.conf.d/
,这个路径下存在很多的配置文件:
因为操作系统在查找库的时候会查询一下配置文件也就是.conf结尾的文件,所以我们可以通过创建配置文件的方式来让操作系统找到对应的动态库,那么这里我们就先创建一个.conf结尾的文件,然后将库所在的路径填入到新创建的文件里面:
然后就强行退出并保存文件,然后使用ldconfig指令更新一下所有的conf文件,这样我们就可以永久的正常的执行刚刚写的程序:
那么这就是方法三。
第四种方法:软链接
程序在搜索库文件的时候会默认在当前路径下进行搜索,所以我们可以在程序所在的路径下创建一个软连接让其指向动态库文件,比如说下面的操作:
然后再运行一下该程序便可以发现可以正常的运行:
动静态库的加载
静态库不考虑加载的过程,在生成可执行程序的时候静态库会把有关代码拷贝直接拷贝进程序里面(用了printf的代码就拷贝库中有关printf的代码),然后程序再加载进内存所以当有多个程序都采用静态链接的时候就会出现冗余的现象,这里存在一个问题将库的代码拷贝到我们的程序里面,那是拷贝到程序中的哪里地方呢?磁盘中的程序含有虚拟地质空间,库中的代码在磁盘上的代码区,所以生成可执行程序的时候是将库中的代码拷贝到程序的代码区,所以程序在查找函数的时候也是去代码区找查找。动态链接是将动态库中的指定函数的地址写入到可执行程序里面,因为printf函数在文件里面也有对应的地址,将该地址写入到可执行程序里面然后程序里面就可以根据该地址找到对应的方法,那这个地址是什么地址呢?我们之前说过一个东西叫做位置无关码那这个又是什么意思呢?我们根据一个生活例子来理解,假设一个马路上只有一个红绿灯,在红绿灯东边100米有一家餐馆,那么我们要想找到这个餐馆是不是只用找到这个马路上的红绿灯就可以了,只要找到了红绿灯就可以往东边走100米从而找到餐馆,那么我们把这样的地址称为相对地址,动态库加载进程序中的地址就是这样的相对地址,动态库的地址由两个地址组成一个start起始地址,一个是该函数在库中相对于库起始地址的偏移地址,start具体是多少在生成程序的时候我们还不知道,当操作系统执行程序发现该程序需要使用库函数的时候就会将该函数对应的动态库加载进内存里面,然后再通过页表将该库映射到虚拟内存中的共享区里面,最后将该库在地址空间上的起始地址填入到start里面,这样程序就可以根据start地址加上每个函数独有的偏移量来找到共享区上的库中的每个函数的内容,那么这就是动态库的加载,而静态库则是相对确定的,当库中的代码被加载进内存的时候库中的代码就已经确定了,可以直接根据地址进行查找,那么这就是动态库的加载。