在进行开发过程中,我们不可避免地会使用到人家的库,那么库到底是什
么?而库又分为动态库和静态库,那么这两个又是什么?这篇博客由我来
简单介绍动静态库。
文章目录
- 1. 库
- 2. 静态库
- a. 静态库的制作
- b. 使用静态库
- 3. 动态库
- a. 动态库的制作
- b. 动态库使用
- c. 使用动态库的前置准备
- 4. 动态库的加载
- 5. 再次认识虚拟地址空间
1. 库
我们进行大型开发的时候,会有许多个头文件和原文件,并且也会使用C语言的标准库,还有或多或少的第三方库。当我们写出这样的程序:
我们知道它编译是使用了C语言的标准库,而我们包了stdio.h这个头文件,按常识来说,我们也因该会有他的原文件,而我们看到他源文件存放的地方找到他时,我们找不到.c为后缀的。但是可能会找到以lib开头.so或者.a.和某些版本号这样的形式的文件。而这样的文件就是库,而库分为动态库(Linux中.so后缀)和静态库(Linux中.a后缀)。库大致是我们所使用的其他原文件的集合目的,而使用动静态库的开发也提高了开发效率和安全性。这里的说明只是大致的说明,接下来我会详细的介绍。
2. 静态库
我们在开发过程中会建立多个头文件和原文件,比如制作一个简陋般的计算器:
我们使用gcc编译它们:
这里在编译时不需要加上头文件的原因是,gcc会自动在系统默认路径和当前路径下寻找需要的头文件。这样编译我们感觉还行,但是如果我们的原文件有100个呢?当我们把我们的头原文件交给他人使用的时候,其他人使用也是添加这么多原文件编译吗?会不会太费劲了?而且如果我们使用这么多的原文件需要交叉使用生成不同的可执行呢?每次都把他们重新编译一边吗?
a. 静态库的制作
所以这时候我们其实可以把所有的原文件处理成可重定向二进制文件,也就是将这些文件都处理成.o文件,这样最起码我们形成多个可执行的时候,不需要再重新编译浪费时间了,而只需要链接需要的文件就可以了:
而现在假如把我们写的头文件和.o文件交付给他人使用比交付给.c文件安全性高且效率高:
当用户使用时,只需要将他的原文件和发过来的.o文件编译链接就可以了:
我们这样使用是传过来的原文件较少,如果有许多个呢?这时候库就出现了。我们可以将这些.o文件进行打包这样使用的时候就没有那么麻烦而只用包相应的头文件就可以了,将.o文件打包的命令是ar命令:
其中的-rc选项意思是当生成文件已经存在时替换它,不存在则生成。
这样就做好了我们的静态库,现在我们将我们的静态库把User文件中的.o文件替换掉:
但是这样的格式是不太标准的,我们知道C标准库将头文件放在include目录下,库文件放在lib64目录下,我们也可以这样做:
b. 使用静态库
使用静态库需要使用到gcc的三个选项:
-I(大写i):让gcc找头文件的时候也在这个路径下找
-l(小写L):后面跟库的真实名字(去掉lib前缀和.a/.so后缀)
-L:后跟链接库的路径
这样就可以使用我们的静态库了,有人就会问了,编译链接动态库的时候gcc还的加个选项啊-static,这个选项表明的意思其实是:使用的库必须全部以静态库链接,如果链接的库只有动态库则直接报错,而我们的gcc默认编译链接是动静态混合的。
静态库链接加载的方式是直接将内容拷贝到我们的可执行中
3. 动态库
在实际开发中我们的动态库是使用的较多的。
a. 动态库的制作
动态库的制作也是将.o文件打包,但是它是使用gcc来打包,并且打包时候的.o文件是经过处理的,我们会用到一个选项:-shared。处理.o文件时的选项-fPIC。
我们也像制作静态库标准那样来:
交付给用户:
这样一个动态库就制作好了。
b. 动态库使用
动态库的使用和静态库差不多:
但是当我们运行后;
它说找不到我们的动态库。原因就是动态库的加载是当我们的可执行程序成为进程后,动态库也会加载到内存中,而我们的可执行中只存储着动态库的位置,并把它加载到内存中。所以当我们的可执行使用动态库的时候就需要找到他,这里说它找不到的原因是,可执行程序找动态库只会在它的默认路径下找:
这里有个ldd命令,会显示可执行链接的静态库的情况。
所以我们要使用动态库还需要一些前置准备:
c. 使用动态库的前置准备
1. 将获取到的第三方库下载到我们的系统中
也就是将我们的头文件放到include中,将动态库放到lib64中:
而当这么做之后,我们以后有关链接这个动态库的编译,就不需要说明其他,而只需要说明库的真实名字即可。 这也是使用第三方库最推荐的做法。
2. 当前目录/lib64下建立动态库的软链接
建立软链接:
3.环境变量
在Linux中有一个环境变量LD_LIBRARY_PATH。
它存储着让可执行找静态库的默认路径。所以只要添加我们的路径到它这里面也可以:
更改环境变量;
4. 更改配置文件
在Linux的一个路径下存着这么一些文件:
这些以.conf结尾的文件存储着可执行文件查找动态库时的路径:
所以我们也可以把我们的动态库所在路径添加到这里:
如果还是找不到的话,可以使用sudo ldconfig命令刷新一下配置文件。
以上就是我们动静态库的制作和使用,如果你写了一个比较好的库供人使用的话,那么就可以用以上格式,压缩之后传输给别人,别人解压缩安装好你的库就可以直接使用了。
4. 动态库的加载
我们要知道,我们写好代码之后经过编译器编译它就不存在什么变量名和函数名了,统统都变成了二进制,而这种二进制也不是杂乱无章的,它是有规则的,在Linux下,它遵循ELF格式。进行着有规则的数据分布,而他的数据分布大致如下:
可以看到它跟虚拟地址空间的的分布有些相似。这不是偶然现象,而是,ELF文件的格式就是虚拟地址空间的格式,因为栈和堆都需要动态分配,所以没有这两个分布。由此可以看出虚拟地址空间不只是一种技术,也是一套标准。
其中符号表就是记录了代码中所使用的函数与地址的映射关系。汇编代码中对函数的调用我们可以发现是通过call一个地址来跳转的,也就是说,程序没被运行的时候,可执行文件中就已经有了地址。而既然在未加载进内存中的时候就已经有了地址,那这个地址是什么呢?其实它就是虚拟地址,因为由于上面ELF文件存储数据的格式,导致它对文件编址的时候也基本遵循虚拟地址空间的方式。而这种地址在磁盘中就已经有了,所以它也叫逻辑地址,我们一般叫这个名字。
由于ELF可执行文件的编址符合虚拟地址,那么在32位机器下,它的可编址范围是从0到FFFFFFFF,那我们就可以将我们的代码进行编址了,它的编址大致如下:
在这其中补充个小知识点:我们上面使用到的地址其实都可以看成是从0开始然后加上一个偏移量(例如0+11223344)的方式,而这种以0为基地址,然后+偏移量寻址的方式叫做平坦模式。而ELF文件的编码都是以0为基地址,不会改变,所以我们可执行文件的编址方式也叫做绝对编址。
我将代码简略表示了一下,在我们的main函数中只调用了printf函数和用户自己定义的Add函数,由于Add是我们属于我们自己代码的函数,所以当编译的时候直接就把它硬编址,给编好了地址,而我们的printf函数是库函数,而这个库是动态库,动态库又不会加载到我们的程序中,所以我们对它的编址先持保留。这个时候我们的可执行文件已经编译好了。当我们运行这个文件的时候,系统知道我们会使用到C标准库中的函数,当我们运行我们的可执行文件时,这个动态库当进程需要的时候这个动态库就已经在内存中了(因为函数的定义还是在动态库中)。所以我们的C标准库需要加载进内存中,但是我们的进程在执行代码的时候使用的并不是物理内存的地址,而是虚拟内存的地址啊。所以我们动态库要跟我们进程的虚拟地址空间产生联系,而虚拟地址空间中,共享区正好是建立这个“联系”的地方,而这种联系也是通过页表建立(会详细说明):
这里就要注意了,我们的可执行只会使用一个动态库吗?肯定不是的,它可能会调用许多个库。而这些库在某些时候会使用它,有些时候又不使用了,我说明的意思是这个共享区中动态库的“联系”是时刻变化的,那既然它在变化,说明某个动态库建立好“联系”后,又断开了然后又建立了,那么这次的建立和第一次建立“联系”的位置在共享区中是一个位置吗?结果肯定是不一定了,有可能被其他动态库在原来那个位置建立了。那么我们就需要动态库在进程地址空间中共享区建立联系的的位置是想在哪里建立就在哪里建立。
这个“联系”中有一个重要的参数其中就是动态库在虚拟地址空间的地址。
而动态库也是需要对其中的内容编址的呀,它这个时候还能使用平坦模式,还能以绝对编址的方式对代码编址吗?要知道动态库在虚拟地址空间的地址不是固定的,它的开头可不是0。动态库它就是为了进程而服务的。这个时候动态库就采用了相对编址的方式来对代码进行编制:
这样不管我们的动态库在地址空间中哪个地址中建立联系,只要库目前已经加载到地址空间中,那么它的地址也就确定了。我们只需要那个地址+所需函数的偏移量就能找到所需函数了。这就是相对编址。我们也发现了这种相对所选择的参考系是虚拟地址空间,而如果是库本身的话,那他也是绝对编址。
那么现在调用printf函数的时候我们的过程就如下:
这也就是为什么我们需要动态库的可执行程序运行时需要告诉动态库的信息了,因为它也要加载进内存中供进程使用使用的方式还是自身加载到共享区中的地址+目标函数的偏移量,来调用函数。这也是gcc 编译形成
.o文件时,需要使用位置无关选项了。而只使用静态库的程序不需要告诉进程静态库的信息原因就是你可以理解为,静态库在链接时,就相当于把自己的代码拷贝到了我们的程序代码中。
以上是我们运行一个程序,假如我们运行一百个程序呢?这一百个程序都用到了printf函数。那么这个时候如果程序使用动态库的话,还需要重复加载C标准库到内存中吗?显然是不用了,,因为只要各自进程中,建立与内存中已经加载好的那一个C标准库建立联系就可以了。所以这也是动态库为什么更受欢迎的原因,一方面是它可以用编译器gcc直接打包生成,另一方面,也节省了内存。而这也是为什么虚拟地址空间中存动态库的区域也叫做共享区了,动态库也叫做共享库。一个进程需要用到多个库,多个进程用到更多的库,那么库也需要被管理,那管理的方式自然还是先描述后组织了。
5. 再次认识虚拟地址空间
经过上面动态库的加载原理,我们现在再来看看我们的程序到底是怎么运行的。
我们知道我们的程序中存在地址,那么当我们的程序加载到内存中创建PCB,建立虚拟地址空间,成为一个进程时,我们的代码在物理内存中,物理内存需不需要对代码形成的指令的进行编址?答案肯定也是需要的。我们虚拟地址空间的分布那自然就把可执行文件的逻辑地址搬过来就成了我们的虚拟地址了。CPU执行我们的指令时,他肯定会从main函数开始,那main函数是怎么被找到呢?遍历吗?其实ELF文件格式中会记录main函数的地址,叫做entry,供操作系统读取。
CPU中会有一个指令寄存器来处理指令,要执行指令,首先要找到指令,那么这时候找指令使用的是虚拟地址还是物理地址呢?其实是虚拟地址。这样当CPU找指令的时候使用虚拟地址然后通过页表,找到指令的物理地址(我们说了物理内存也是需要对指令进行编址的)然后开始执行指令,这时候如果是调用函数的话,那么它的指令是call 一个地址,而这个指令又是在文件中的地址,所以这个地址肯定也是虚拟地址,CPU又会使用这个虚拟地址,来通过页表找到函数然后继续执行。这样我们的CPU好像在虚拟地址空间、页表、物理内存上转起来了。