目录
5.1.1 在gcc/g++指令中使用-I选项声明头文件路径
7.1 修改环境变量LD_LIBRARY_PATH(临时方案)
我们写代码时都要包含头文件,头文件内有方法的声明,但光声明没什么用,我们还要找到这些方法实现的方式,关于具体的实现方法被存在一个叫库的地方
在Linux下有动态库和静态库两种库(其中以.a为文件后缀的是静态库,以.so为文件后缀的是动态库):
所以我们使用一门计算机语言来写程序时,需要下载其对应的头文件和动静态库(即我们常说的环境搭建的一部分)
当然是为了提升开发者的效率
如果我们没有库,我们每想用一个最基本的printf函数都要自己去手动实现一下,这不是太费劲了吗?造轮子只适合我们学习原理,但并不适合用来开发
不管是动态库还是静态库,其命名都有下面的一套规则:
lib+库名+. +文件后缀(so/a+版本号)
下面我们拿两个库名来举个例子:
libc.a libstdc++.so.6
我们把lib和文件后缀去掉看看:
libc.a libstdc++.so.6
所以这两个库名分别为c和stdc++
下面我们写两段简单的代码来实现一下库的原理:



现在我们实现了整数的加减这两个功能,现在得想想办法将自己写的功能交给其他人用
直接将源码交给别人?
是自己信任的人当然可以,不过在公司层面上因为安全和版权问题并不会这样干!
那我们可以先将我们自己写的源文件汇编成二进制文件目标文件(二进制文件不会暴漏源码),再将头文件和二进制文件目标文件交给别人,别人想要使用我们自己写的功能联合编译一下不就行了嘛:


完美运行
但是一下子给别人这么多文件感觉不太方便哎,我们能不能把这些.o文件(目标文件)打包成一份发给别人呢?(别人调用功能时需要查看头文件,因此头文件不适合打包)
ar指令可让我们集合许多文件,成为单一的备存文件。在备存文件中,所有成员文件皆保有原来的属性与权限:
指令参数
| -d | 删除库文件中的成员文件。 |
| -m | 变更成员文件在库文件中的次序。 |
| -p | 显示库文件中的成员文件内容。 |
| -q | 将文件附加在库文件末端。 |
| -r | 将文件插入库文件中。 |
| -t | 显示库文件中所包含的文件。 |
| -x | 自库文件中取出成员文件。 |
选项参数
| a<成员文件> | 将文件插入库文件中指定的成员文件之后。 |
| b<成员文件> | 将文件插入库文件中指定的成员文件之前。 |
| c | 建立库文件。 |
| f | 为避免过长的文件名不兼容于其他系统的ar指令指令,因此可利用此参数,截掉要放入库文件中过长的成员文件名称。 |
| i<成员文件> | 将文件插入库文件中指定的成员文件之前。 |
| o | 保留库文件中文件的日期。 |
| s | 若库文件中包含了对象模式,可利用此参数建立备存文件的符号表。 |
| S | 不产生符号表。 |
| u | 只将日期较新文件插入库文件中。 |
| v | 程序执行时显示详细的信息。 |
| V | 显示版本信息。 |
下面来演示一下:

接下来看看能不能成功编译:
咦?编译器找不到库中的二进制文件,我们自己打包的库文件属于第三方库,编译器并不能识别,所以我们在编译时要声明:
在gcc指令下使用-L选项声明第三方库所在的路径,使用-l选项声明第三方库的库名:

但是我们去一些官网下载库文件, 我们下到的是.tar的压缩文件,该文件内有一个库文件存放的目录和一个头文件存放的目录,以及一些说明性文件
现在我们来实现一个库文件的打包:

这样子一个mymain.tgz的库文件压缩包就被我们打包好了
如果一个使用者拿到了我们打包的库压缩文件,首先要做的就是解压了(对于指令不熟悉的同学可以看到这里:【Linux】常用基本指令详解):

然后就是编译了:

咦,这次编译是找不到头文件了,因为我们要使用的头文件并不在当前目录下,而是存放在include目录下
我们可以在gcc指令中使用-I选项声明要使用的第三方头文件路径:

编译成功,完美运行~
如果静态库没有默认安装到系统gcc、g++默认的搜索路径下,用户必须指明对应的选项,告知编译器: 头文件在哪里、库文件在哪里、库文件具体的名称
上面的方法比较麻烦,我们还可以将我们下载下来的库和头文件,拷贝到系统默认路径下:

这时再去编译:

这里需要注意的是,我们安装的库是第三方的(除语言本身接口、操作系统系统接口之外的库),要想正常使用,即便是已经全部安装到了系统中,gcc/g++必须用-l指明具体库的名称!

所以对任何软件而言,安装/卸载的本质就是到系统特定的路径下添加/删除文件
但是安装到系统中,我们安装大部分指令,库等等都是需要sudo的或者超级用户来操作的
要形成动态库的第一步,首先需要将用到的源文件编译形成与位置无关码(我们在后面解释这是什么)的二进制文件,在gcc/g++指令后面加上-fPIC选项即可:

有了与位置无关码的二进制文件后,我们再用gcc/g++指令加上-shared选项将它们打包形成一个动态库文件:

接下来我们就按照打包静态库的方法来将我们形成的动态库文件和头文件打包成为一个压缩包:

这样一个动态库的压缩包文件我们就打包好了
下面又一个使用者拿到了我们打包的动态库压缩文件,废话不多说,先解压:

再来按先前的方法编译:

成功了,运行一下试试看:

?报错了?我们不是已经告诉gcc动态库的位置了吗?怎么还找不到目标文件呢?
这是因为我们告诉的是gcc,它是个编译器,所以编译并没有报错,但是我们运行程序时是操作系统进行的,OS可不知道库在哪里
那静态库怎么就可以运行呢?
静态库的链接原则是:将用户使用的二进制代码直接拷贝到目标可执行程序中。
但是动态库不会!所以我们最终形成的可执行程序没有库中的二进制代码,导致了运行报错。
那动态库形成的可执行文件要怎么运行呢?
我们Linux有个环境变量LD_LIBRARY_PATH,存储的是系统查找动态库的路径
所以我们将我们动态库所在的路径添加到该环境变量中即可:

运行一下:

成功了
但是这个方法只是一个临时方案,我们修改后的环境变量重新登录就没了
我们可以在系统默认搜索的库路径下添加一个软链接来指向我们写的动态库:

运行一下:

我们Linux系统下有一个文件:/etc/ld.so.conf.d/

这个文件中记录的都是一些路径,系统会通过这些路径查找所需要链接的动态库
所以我们可以向该目录中添加我们动态库所在目录的文件(该文件后缀为.conf)即可:

添加完后使用ldconfig指令使配置文件生效即可:

之前我们讲解过进程地址空间:

现在我们联系以前的知识对动态库是怎么样被进程调用的进行讲解,来看下面这张图:

在磁盘中现存有两个程序(1、2)和应该动态库,这两个程序中的代码都调用了动态库中的某些功能。
接下来程序1被操作系统调用,形成了进程1:

在该进程1被调用的过程中,其在磁盘中的数据会被加载进物理内存中,并且操作系统会创建进程1的PCB、进程地址空间和页表
但由于进程1中会调用动态库中的代码,所以动态库中的部分数据也会被加载到物理内存中,但这被加载到物理内存中的数据,在进程1运行时会被加载到其进程地址空间的共享区,当进程中的代码段运行到要调用动态库中的功能时,会直接从共享区中调用
下面进程2也同时被操作系统调用了:

我们可以看到进程2再被操作系统调用时,会有着和进程1一样的操作,但是有点不同的是,由于动态库中的数据已经被加载到物理内存中,所以只需要调整页表的映射关系即可。由此可以看出不同进程中的共享区的数据可能会来自于同一块物理空间
在前面的动态库的形成原理中我们提到了与位置无关码,那这个码到底是什么呢?
我们都知道代码在编译链接的时候,如果调用了动态库的功能,编译器会动态库中的我们所需要的代码的地址拷贝到自己的可执行程序中相关的位置
但注意:这里拷贝的仅仅是我们所需要的代码在动态库中的地址
这个地址是一个相对地址,表示该段代码在动态库中起始位置的偏移量(因为进程运行时是要将动态库加载到进程地址空间中的,由于每个进程具有差异,共享区的位置是不确定的,所以我们无法确定动态库加载到进程地址空间中的绝对地址,所以用偏移量来记录要调用的代码距动态库起始地址的相对位置),这个偏移量被称为与位置无关码
● 动态库和静态库同时存在,系统默认采用动态链接(如果在动静态库同时存在时,想以静态链接的方式编译文件,可以在gcc/g++指令后面加上-static选项)
● 编译器在链接的时候,如果提供的库只有静态库,只能进行静态链接
● 云服务器一般都只会提供动态库(使用yum来安装静态库:yum install -y glibc-static(C语言静态库)yum install -y glibc-static libstdc++-static(C++静态库))
该期博客到这里就结束了,感谢大家的阅读~