1 软硬链接
在学习动静态库添加之前需要有一个预备知识,那就是软硬链接,不然对于大家来说能学会的也就只有操作而已,我们需要从原理当中理解这整个过程。
1.1 硬链接
硬链接是Linux和Unix文件系统中创建的链接方式,它可以将一个文件关联到多个文件名,也就是一个文件能够拥有多个硬链接,每一硬链接都指向了相同的数据块。
如何理解这句话呢?
我们可以假象理解这个硬链接它是一个原文件的拷贝,当源文件被删除之后,这个硬链接文件可以作为新的原文件然后对这个文件进行读写操作,里面的数据不会有任何的问题,仿佛这个文件就是它创建的一样。
当然这只是理解方式,实际上并不是这样,它是依靠文件的inode这个结构体内部的引用计数,当有一个硬链接这个引用变量就会加一,删除一个硬链接这个变量就会减一,知道为0,那么这个数据块就会在对应磁盘释放,有注意到我说的是什么吗?所有硬链接使用的数据块都是同一部分,那么是不是说当其中一个更改了文件的内容,其它的文件也会跟着改变呢?
硬链接方式:ln file newfile
当你看到这两个文件的inode号相同,并且引用个数从1变为了2,就表示建立成功了。
那么通过查看内容是否两个文件都是一样的呢?
可以看到两个文件当中的内容是一模一样的,那么此时我们更改源文件的内容呢?
当我们只更改了源文件当中的内容,对应的硬链接文件也对应被更改了,所以**本质上这两个文件用的数据块都是同一片数据块。**
** **那么我手欠将其中一个文件删除之后会发生什么事情呢?既然都是指向同一片数据块,那么删除之后其余的硬链接也应该看不到才对是不?可实际呢?
哦豁,看来不是这样呢,看来删除源文件,硬链接的数据仍然还在勒,我们在看一下此时硬链接的引用变量个数呢?
从之前的2变为了1呢,这也就进一步的证明了我之前所说的内容是正确的**,硬链接就是通过在文件的目录当中添加一个映射关系,也就是一个inode对应多个文件名,指向相同的数据块,至始至终,inode只有一份,对应的数据块也当然只有一份**。
1.2 软连接
那么对应软连接是否与我们的硬链接保持相同的特性呢?
软链接(Symbolic Link),也称为符号链接,是一种在 Linux/Unix 文件系统中创建的特殊文件类型。软链接文件本身不包含实际的数据,而是指向另一个文件或目录的路径名字符串。 与硬链接(Hard Link)不同,软链接文件是一个指向原始文件的指针,并且可以跨越文件系统边界。另外,如果原始文件被删除或移动,软链接文件仍然存在,但它将成为一个无效的链接,无法再次访问目标文件。
** 软链接方式:ln -s file newfile**
当我们看到了有一个特殊颜色的文件通过->指向了另外一个文件,就表示软连接建立成功。
通过查看软连接的inode和引用变量,并不是硬链接的链接方式,它是一个新的文件,有自己独立的inode号,引用变量也是从1开始计数,那么它就应该有自己新的数据块,这是一定的。
那么首先我们来看一个两个文件当中的内容是否都是一样的:
看到是一模一样的,确实符合预期,当我们对文件内容进行修改呢?
嗯,看来也是没有任何问题,我们再将原文件删除了看一下呢?
欸?删除源文件之后的结果与我们的硬链接不相同啊,它竟然找不到了,这是为什么?
其实原因很简单,因为软连接是生成了一个新的文件,也就有了新的inode,也有了新的数据块,并且这个数据块并没有存我们源文件当中的数据,那么它存什么呢?它存源文件所处的位置,之后使用这个文件他就会通过这个位置找到真正存放文件数据的地方。
1.3 目录结构
我们试着将软连接和硬链接都放到名为tt的目录下,看一下现在的这个软硬链接是否还生效?
答案是肯定生效,因为对于文件来说,只要我有了你的inode,不管你跑到那里去,你对应的数据块都在我的手里,所以我想要访问这些数据很明显是轻而易举的。
那么对应的,你们知道为什么我们在linux下可以通过**cd . **或者** cd ..** 这样的操作吗?其实我刚刚以及说出答案了,目录是什么?目录也是一个文件,那么文件就有对应的数据,对于目录来说它的数据是什么?数据就是文件名以及inode的对应关系等等,那么只要我们链接了目录,不就可以随意的走到上级目录,访问当前目录,还有走到下级目录了?
看到了这两个目录文件了吗?其中的 **. **的inode与tt的inode相同,引用变量为2,表示** . **tt的硬链接,那么看到 **.. **这个目录呢?为什么它有3个引用?
我们分析一下 **.. **表示上级目录,我们回到上级目录查看呢?
其中它的inode与我们的上级目录的 **. **相同,而上级目录外也有一个外面也有一个文件指向这个地方,也就是嵌套操作,当然就会有3个硬链接咯。
通过这种方式,我们就能够实现对于文件的各种访问都能回到最开始的位置,始终保持了一个有返回路径的方式。
2 动静态库
2.1 为什么要有库
为什么要有库文件这个东西呢?其实我想我都不用回答这个问题大家心里都是有一个准确的概念,那就是如果没有库,全部靠手搓代码,这怕不是太恼火了吧,假象一下,我们写一个加法函数,然后将这个函数的返回值通过printf输出,但是呢,我们没有库文件,所以只能自己写一个printf函数,那么这个程序有155行,其中150行都是在写printf,最重要的不是这一点,重要的是咱们不会写啊,你想一下,你现在会写printf函数吗?反正博主是不会,博主最多就是通过文件重定向,通过系统提供的函数输出数据。
这还只是一个printf函数,而我们平时写代码可不止这些函数,还有更多更复杂的函数呢,所以对于我们来说,库的产生极大的简化了我们写代码的成本以及学习效率。
当然对于我们平时学习的过程当中也并不是全部都用库文件,像是一些数据结构啊,容器,适配器等等都需要我们自己实现,因为对于我们来说直接实现printf等函数的实现在开始学习时会太难了,但是对于容器这些库,我们是有能力实现的,所以通过自己实现的方式可以加深自己的代码能力和对库的理解程度。
2.2 静态库
2.2.1 方法一:
想要让别人得到并使用你发送的库文件其实有一种很简单的方式,那就是将你的所有文件包括.h和.c、.o文件等等都发送给另外一个人,只要他将这库文件都放在本地文件,那么他就能使用。如下:
我这里发送的是.o文件,因为我在模仿发送者不希望别人知道它的源码是多少,只希望别人怎么使用这个库的功能,然后配合main.o一起生成一个可执行文件。
对于一个可执行程序的生成需要通过预编译,编译,汇编,链接这四个过程,对于前三个步骤来说,里面的代码都是独立的,所以可以运行到.o,但是到了链接阶段就必须要让代码的中的变量函数找到真实的地址,否则就会出现错误。
运行这个可执行文件,我们就能看到库文件成功的打包过来了。
但是有没有感觉这个方式好傻啊,我去下载一个包下来我还需要全部搞到本地文件,然后再将所有的.o与主程序一起运行,简直是恶心人,所以有了另外的方式:
2.2.2 方法二:
首先,我们从网上找到了一个库文件压缩包,打开之后多了两个目录分别是include和lib,其里面就是头文件和库文件。如下:
其中的include里面就是头文件,这我们很清楚,但是这个lib里面的libmymath.a是个什么,不应该是.o或者是.c文件吗?搞呢?
其实这才是真正的静态库,**静态库的命名方式就是lib前缀+(库名)+ .a后缀,后缀可能不止是.a还有其它版本信息等**,但是大概就是这样,如系统库文件:
ok,那命名方式是这样我理解了,那这个文件是怎么生成的呢?
ar -rc 静态库文件 所有的.o文件
好,既然我们已经有了静态库那我们能用了吧,来看看:
欸?错了,他说他找不到这个头文件在哪里,那我就告诉他:
我通过添加一个 -I./include告诉他我的头文件在哪里,-I就是include的意思,后面是跟随的路径,这个 **. **就是当前路径下的include。但是好像还是不行啊?但是问题变了,他说他找不到函数了,那么表示这个头文件它是找到了,但是库文件找不到,好,那我就告诉他库文件在哪里:
嗯?为什么它还是找不到呢?我不是已经告诉它了吗?难道libmymath.a这样的方式是错误的?并不是,gcc编译器确实是找到了这个文件,但是他不敢动啊,他不知道你是不是要这个文件,虽然这个目录里只有一个文件。所以这个时候需要我们在添加一个参数,告诉他我们的库文件叫什么:
欸?不对,但是错误好像变了欸,他说他找不到libmymath.a这个文件,确实它不应该找到,还记得我库文件的命名规则吗?去除前缀和后缀才是真正的库文件名字,那么再来:
哇,终于成功了,可以看到一个黄色的mytest,运行也没有出错就表示我们正确了。
真是感动,走了这么多步才成功,真让人想摸一把眼泪。
但是这样的运行方式可以说是肉眼可见的难受,虽然看起来牛逼了一点,所以再优化:
2.2.3 方法三:
系统当中用来存系统默认头文件的位置在/usr/include/当中:
库文件的位置在 /lib64 或者/usr/lib64当中:(博主喜欢放到/lib64当中)
接下来,我们将本地文件拷贝到这两个默认路径当中:
这两个位置都是被权限约束的,所以需要通过sudo提高权限。
通过查看可以看到我们的文件已经被让进去了,此时我们再来生成可执行文件试试。
这一次就不需要告知gcc文件在哪里,它自己就能找到,但是-lmyamth是必须告知的,没有任何避免的方式,毕竟我们的这个是第三方库,并不在gcc的提供的标准库当中。所以gcc是不认识它的,还是需要我们手动去告知。
当然我们一般还是不要把自己的库文件添加在系统路径当中了,毕竟我们太菜了,复杂的库写出来可能有问题,简单的库没有必要添加。所以刚刚添加的还是删了为好。
2.3 动态库
对于动态库,编译器是非常支持的,所以对于它.o文件的生成还有对于库文件的生成编译器都是有提供方式,而不是像静态库的生成需要额外依靠ar命令。
首先看到.o文件的生成:
gcc -fPIC -c myadd.c mysub.c
与之前的静态库二进制文件生成不同,这里增加了一个-fPIC参数,这是干嘛的呢?我之后单独讲解,现在只需要知道生成动态库要依靠这个方式,它的作用是生成与位置无关代码。
到此我们就得到了两个.o文件,然后再将这两个.o文件打包组成一个动态库文件:
*gcc -shared -o libmymath.so .o
与gcc通过生成一个可执行程序一样的指令,但是中间多了一个参数shared,名为共享的,其中动态库的文件名也是去掉前缀和后缀两个部分,剩下的才是。
然后通过创建目录,将头文件和库文件放到对应目录当中,如下:
但是这都不是重点,或者是现在的重点不是这里,基于此,我们是能够通过这些文件,学习上面静态库文件生成可执行文件的方式,来生成我们的动态库,那么看一下是否能行:
嗯,很不错,很不错,看来我们已经能够熟练操作利用第三方库生成可执行文件了,可是事实是这样吗?我们运行一下这个可执行程序:
哦不,他竟然说不能打开一个共享的文件,我太难受了,那么是为什么呢?通过查看可执行程序的链接文件可以得知动态库到底有没有链接上。
好吧,看来我们的动态库确实没有链接上,不能怪他,但是我有一些好奇,为什么我已经告知了编译器我们的库文件在哪里,但是他链接时还是找不到呢?
实际上并不是编译器找不到,编译器找到了,否则他就会报出某个函数找不到了,可事实并不是,他说共享库不能被不能被打开,也就不是编译器的原因了,那么出问题的地方在哪里呢?
我们可以好好想一想为什么对于静态库它没有出现这个错误,而动态库会出现这个错误呢?其实原因也是可以理解的。那就是静态库的文件会在可执行程序当中展开,但是对于动态库他不行,他需要运行时找到这个库文件,把相应的函数找到并运行完成之后,再返回回来继续执行。而这一过程需要依靠操作系统帮助我们进行这跳转的指令执行。(这部分相信大家还有些疑惑,下一节我会单独讲解)。所以这锅错误不是因为编译器gcc有什么问题,而是我们的操作系统找不到对应的库文件位置。
方法一:
让操作系统知道我们的动态库文件也有三种方式,方法一**那就是添加一个环境变量,操作系统在执行一个可执行文件的时候会在环境变量当中找一个名为LD_LIBRARY_PATH的环境变量**,当发现了这个可执行文件的动态库文件就在这个地方就会为我们链接上,如下:
这是我动态库文件放的位置,所以实际情况需要你们查看自己的位置,还有就是咱们不能更改原来的环境变量,所以需要用到export命令,而不是通过echo去.bashrc更改,否则有可能你的shell就运行不起来了。
此时,我们已经配置好了环境变量,再来看一下这个可执行程序的动态库是否链接成功了?
欸!不仅有了,可执行程序也能够执行了。可是添加环境变量的方式终归只是一个临时方式,当下一次启动shell的时候他又不能实现了,如下:
重启shell之后还是不能实现我们希望的功能,所以这个时候怎么办呢?别急我们还有其它的方式可以使用。
方法二:
反正不是操作系统找不到吗?那么我放一个动态库文件的软链接到系统的查找默认路径不就好了?我们试试能不能行,如下:
可以看到我们已经在/lib64这个目录当中添加了一个库文件的软链接,也成功了,那么看看现在是否能够运行这个可执行程序呢?
看来这种方式是能够让操作系统找到的,并且退出之后下一次还是能够找到该库文件的位置,是一种永久方式,还是很不错的。
方法三:
对于操作系统来说,它在运行一个可执行程序的时候,对于动态库他还会到一个文件当中去查找,那就是/etc/ld.so.conf.d/这个目录当中的某一个文件,此时这个文件需要我们自己添加。
首先在etc/ld.so.conf.d/目录下添加一个文件,文件名随便取,但是后缀需要时.conf,作为一个配置文件,然后通过vim、nano等编辑文件的方式库文件位置写在这个配置文件当中。
也就是上图这种方式,注意这个地方都是系统级别的操作,所以所有的操作都是需要sudo提权限的,否则他会给你报出permiss denied的错误。
在完成上述操作之后还需要执行下方代码:(作用是刷新一下操作系统的文件配置)
此时我们运行这个代码看看是否能够执行呢?
很明显是能够执行的,同样这种方式也是与我们的添加软链接一样是能够永久存在的。不过博主还是建议在使用完成之后将这些不属于操作系统本身的文件删除。
如上三种能够让操作系统找到对应动态库的原因都是因为操作系统在执行这个可执行程序之后就会为我们在这三个位置进行查找。
3 库文件的理解
对于动静态库的操作在文章当中讲了这么多,但是具体这个动静态库有什么区别呢?就我看来好像都是一样的啊?确实,他执行之后确实是一模一样的,他也应该一模一样,否则这库怕不是有点问题。
对于动态库我们可以设想一下,假设我们是一个正在读高中的高中生,然后呢,我又特别喜欢玩电脑,可是家里又没有电脑,那我又想玩,这个时候应该怎么办呢?那就是跑到网吧去上网呗,上完网我又跑回家里做作业之类的事情,反正就是忙自己的事情。**对应我们的可执行程序就是,当我们执行到了一个动态库的函数,那么我们就会跑过去执行这一段代码,执行完成之后又跑回来运行后续的代码**。
那么静态库呢?我们还是一个高中生,但是呢我特别有钱,性格还特别怪,也喜欢玩电脑,当我想要玩电脑时,我不需要到网吧去上,我家里就有电脑,所以想玩游戏时直接在家里玩就行了,但是我有个怪脾气,我每次玩电脑都要买一台新的电脑我才玩,否则我就不玩了,就是有钱任性。对**应代码就是当程序执行到了一个静态库的函数,这个函数已经在我们的程序当中被展开了,所以根本不需要跑到别的文件去运行。并且一旦遇到了这个函数他就展开一次,根本不嫌累**。
所以上面讲的动静态库的区别很明显有一点,那就是动态库的大小会比静态库大不少。
通过查看中间的文件大小也能够很明显的看出来他们的大小区别,静态库一般会比动态库大大约60~80倍左右,而且我们的程序还是一个小程序,一旦是一个大项目那这个差距可不是一点两点咯,也难怪编译器都更喜欢动态库一点。
3.1 动态库的链接过程
对于这一部分呢其实是有些难以理解的,但是大家能理解就理解吧:
首先看到这一张图片,我已经简化了很多内容了,其中还有**进程对于文件的管理,操作系统对于进程的管理,以及虚拟地址空间,页表,操作系统对于打开文件的管理,对于磁盘文件的管理**等等一系列的知识基础才能有机会搞懂这一张图。不过既然看到了这里,我就默认大家是知道这些内容的,因为我之前的博客也是有写过这些知识的讲解的。
首先,我们讲程序运行起来之前,操作系统会先将进程加载到内存当中,并且创建对应的PCB结构体,还有这个进程依靠的库文件也会一起被加载到内存当中,然后操作系统会通过页表建立进程虚拟地址函数的地址与内存中的真实地址的联系,而库文件的内存是在库文件当中的,所以首先建立的联系是通过建立进程地址和库文件在内存的地址的联系,然后通过偏移量找到对应的函数,也就是myadd函数,再次通过页表将地址映射到虚拟地址当中一个叫做共享区的地址当中,所以在我们程序执行的时候,真实并不会再去找一次这个库文件了,他会直接通过共享区的地址找到内存当中myadd函数的地址,这个时候就能够实现动态链接了。
其中通过共享区中的地址找到内存中的地址用到了偏移量这一个方式,为什么会用到偏移量呢?其实原因很简单,那就是我们能够保证我们的库文件就在这一个地方使用吗?很明显不是的,我们在其他的地方也会去找他,如果用绝对地址的方式,那么下其他位置调用库文件那就是不能实现,这也是之前生成.o文件我们需要同过-fPIC(与位置无关码)的原因。所以对于库文件的使用其实操作系统是通过重定位基地址加上一个偏移量的方式找到对应的函数地址。
还有就是因为一个进程的虚拟地址其实在加载它之前就已经被绑定好了,但是由于不同的位置加载运行,实际上它的进程地址空间的地址会有一些变化,也就是不能够保证每一次的地址都是一模一样的,这个时候才引入了一个偏移量的概念,这个偏移量在代码段中就有了,而我们又将库文件的基地址存到了共享区当中,那么通过偏移量,那么就能够找到对应的函数了。并且不管我们的库文件被加载到了进程地址的任何一个位置,我们都能准确的找到。
这就是整个动态链接的全部过程,如果有特别想要了解的朋友,建议了解一个GOT(全局偏移量表)、还有PLT(Procedure Linkage Table)这个数据结构、以及RT(Relocation Table)这些相关的知识,不过博主认为如果不是真正的高手,了解到我讲解的地步已经足够了。别钻牛角,钻牛角尖会导致不幸。
以上就是博主对于动静态库的全部理解了,希望能够帮助到大家。
版权归原作者 波奇~ 所有, 如有侵权,请联系我们删除。