目录
1 软硬链接
2 动静态库
1 软硬链接
不知道大家也没有仔细看过我们的 windows 中的快捷方式的内容,我们右键点开一个快捷方式然后查看其属性,我们发现有一个 目标 的内容
这个目标是一串路径,这也就是我们的程序的安装路径中的一个.exe文件,我们将这个路径复制打开就能看到在这个目录下有一个一摸一样的图标,也就是我们的应用程序。
而点击快捷方式其实就是在点击安装目录下的这个.exe程序。 那么这是如何做到的呢?为什么我们点击一个文件却能够执行另外一个文件呢?为什么我们称这个快捷方式也是文件呢?简单来说就是我们可以在C盘的桌面路径下可以查看这个文件的属性等,所以这个快捷方式肯定是一个文件。
这种快捷方式在Linux中的实现其实就是软链接。
我们可以使用下面的命令来创建一个软链接文件
ln -s 目标文件名 软链接文件名
这样就完成了一个简单的软链接了,这时候我们再看就能看见目录下的这个软链接文件了,同时我们观察他们的属性,原来的目标文件的属性是没有发生变化的, 同时新创建的软链接文件的inode也是与目标文件不同的,这就说明了软链接文件是一个独立的文件。
而创建一个硬链接文件则是使用 ln 命令不加选项。
创建完一个硬链接之后,我们就发现了一个与软链接的不同之处,也就是创建硬链接之后,原来的目标文件的属性发生了变化,中间的数字由1变成了2,同时硬链接文件的 inode 编号与目标文件的inode 编号是一样的,硬链接文件没有独立的 inode 。
那么从上面的软硬链接文件,我们也就能看出他们的最大区别 : 是否有独立的inode
软链接是有独立的 inode 的,所以软链接文件是一个独立的文件。
硬链接没有独立的 inode ,那么建立硬链接的过程是干了什么呢? 我们可以回想一下目录的数据块存了什么内容,存储的是该目录下的文件名与 inode 之间的映射关系。我们可以试着对硬链接文件进行操作,看一下目标文件是否跟着发生变化
我们发现,当向硬链接文件中写入数据的时候,目标文件也发生了变化,同时,硬链接文件的属性和目标文件是一模一样的,我们可以操作一下文件的权限来看一下他们的属性是否也会同步变化
结合我们上面看到的,硬链接文件和目标文件的 inode 编号都是一样的,我们可以得出一个结论,那么就是 硬链接文件 和目标文件用的是同一个 inode ,由此,他们自然也是同一些数据块,所有的数据都是一样的,因为他们本身就是同一个文件。我们说过,文件名其实就是用户看的,而真正对文件的操作其实是拿着文件名去目录的映射关系中找到对应的 inode 来对文件操作,那么一个inode 能不能与两个文件名构建映射关系呢?
所以其实硬链接文件就是在目录中多创建了一条映射关系,它和目标文件本质上就是同一个文件,只是文件名不一样而已,那么这样一来,我们其实也可以理解硬链接为一种特殊的目标问价的重命名或者说是一种备份。
我们也可以将文件内容清空,然后对原目标文件进行操作,然后看一下硬链接文件是否发生变化
改变原文件,硬链接文件确实会同步更新。而我们创建硬链接时文件属性发生变化了,变化的是一个数字 由 1 变为 2 ,这个数字叫做问价的呢硬链接数,也叫引用计数。在inode中有一个计数器,当有一个文件名指向这个inode 的时候,引用计数就加1,同样,当一个文件名与这个inode的映射关系删除时,引用计数则会减 1 ,inode中引用计数就是我们的硬链接数 。
那么当我们将原文件删除之后,硬链接是否会跟着被删除呢?
我们发现,删除原文件之后,硬链接文件还在,而软链接文件却已经失效了。删除文件我们讲过其实是一种惰性删除,但是inode位图和数据块位图置 0 的操作其实不是由我们删除文件的操作来做的,而是操作系统来做的,当一个 inode 引用计数变为 0 的时候,操作系统会自动完成文件的惰性删除,同时,如果inode的引用计数还不为0 ,则不会去删除文件,只会将你要删除的文件名所对应的映射关系删除,同时inode 的引用计数会减1. 这也就是我们上面原文件被删除了硬链接文件还能够指向原来的inode 和数据块的原因。
而在上图中,我们删除原文件,软链接失效了,这说明软链接的链接并不是靠目标文件的 inode编号来维持的,而只是目标文件的文件名。
我们可以再来测试一下,再创建一个同名文件,我们就能够发现软链接又建立起来了
这是不是很奇怪?为什么都不是同一个文件了,只是文件名相同,软链接又重新建立起来了呢?其实说软链接是用文件名来标记这个说法还不够严谨,更严格地说,软链接其实是靠目标文件的路径加文件名来标记的,因为文件名旨在一个目录下具有唯一性,除了该目录就不一定唯一了。
软链接文件一定保存了目标文件的路径也就是位置。而软链接通过路径来标定文件的方式相比于硬链接而言有一个很大的优势,那就是软链接是可以跨文件系统的,也就是跨分区使用,而硬链接由于是通过 inode 绑定,他无法跨分区进行硬链接。
既然软链接中保存了目标文件的路径,那么是不是可以操作软链接文件来改变原文件呢?当然可以。
既然能够找到原文件,那么其实他也能和硬链接一样,改变软链接文件也就能够改变原文件,同时改变了原文件,由于软链接文件能够知道原文件的路径也就能够知道原文件的一切属性,那么他也能根据原文件的修改做出相应的更新。
同时,我们也发现,软链接文件就算我们没有添加任何数据,它的内容大小也不是 0,这说明他在被创建时就已经写入了一些数据,这些数据就是与目标文件相关的一些信息。
回到最初的图片我们也能够发现软链接的文件类型是 l ,也就是链接文件。而硬链接文件的文件类型是 - ,也就是普通文件,因为目标文件就是一个普通文件。
删除软硬链接文件我们可以直接使用 rm 来删除,我们也可以使用 unlink 来删除一个链接文件。
那么上面讲了这么多软硬链接的相关概念,软硬链接到底有什么用呢?或者说该如何使用呢?
软链接既然我们说是快捷方式类似的功能,那么在Linux一切皆文件的前提下,我们就可以使用软链接来定位我们的目录或者文件,具体的使用场景就是如果我们有一些常用的文件,但是他的路径很深,如果我们的单纯使用cd命令的话,进入这个目录会很麻烦,这时候我们就可以在顶层或者上层目录下创建一个软链接来链接到文件所在目录,我们就可以通过软连接来进入较深的目录了。
软链接是可以对目录进行连接的。
而硬链接的目的就是备份的作用,或者我们可以使用硬链接来制作一个简单的回收站之类的功能,让我们能够简单的恢复被删除的文件(实际上是因为没有被真正删除)。
软链接是能够对目录进行链接的,那么硬链接呢?
我们以前使用的 . 和 .. ,能够快捷的表示当前目录和上级目录,这两个就是硬链接形式存在的,怎么证明呢?我们可以创建一个目录来看一下该目录和他的上级目录的硬链接数。
我们可以发现,我们创建一个空目录出来的时候,他的硬链接数就是 2 ,其中一个是他在上级目录的数据块中存储的目录名,而另一个则是他在自己目录下的 . ,任何一个目录都有 . 和 .. ,如果创建的目录是终端目录(它的内部没有目录了),那么它的硬链接数初始就是 2 ,如果不是终端目录则硬链接数至少为 3 ,取决于他的下面有多少个二级目录,每一个目录下都有一个 .. 硬链接到他的父目录。 所以文件系统的树状结构不仅有从上往下的连接还有从下往上的连接。
同时我们发现,我们无法自己对目录创建硬链接
就算我们使用sudo提权,也是没有权限为目录创建硬链接的
这是为什么呢?
我们要知道,在有些场景下我们是需要遍历整个文件系统的所有目录和文件的,也就是遍历文件树,比如我们要搜索某个文件时,这时候,如果我们有目录的硬链接存在的话,就有可能会陷入循环甚至是死循环的遍历中,比如我们在当前目录下创建一个当前目录的硬链接,这样就会在这个目录死循环下去,所以我们用户是无法创建目录的硬链接的,因为这对于操作系统而言是不安全的,但是操作系统自己内置的 . 和 .. 为什么就存在呢? 这主要还是方便用户操作,同时由于这是操作系统自己内置的硬链接,那么他在操作系统眼里是安全的,在遍历的时候是不会进去这个路径的。
2 动静态库
在前面的文章中我们已经讲过动静态链接的一些基本概念以及区别,我们可以先复习一下
静态链接:程序在编译链接的时候就把程序所需要的库的代码拷贝到了我们的程序中,运行的时候不需要静态库(.a)
动态链接:把所需要的函数在库中的地址拷贝到程序的相应位置,在运行的时候根据函数的地址去动态库调用对应的方法
一个与动态库链接的可执行文件仅仅包含他用到的函数入口地址的一个表,而不是外部函数所在文件的整个机器码。
在可执行程序运行之前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存,这个过程称为动态链接。
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制(虚拟地址空间+页表映射)允许物理内存中的一份动态库被要用到该库的所有进程共享。
我们在之前已经使用过动静态链接的使用,而这里我们要学习的如何制作动静态库以及如何使用第三方动静态库。
库理解起来其实很简单,就是头文件加函数实现,头文件是为了让使用库的人知道库中有什么接口,这是要给用户看的,而库中的函数的接口具体实现则一般都不会直接将源码公布出来,而是以动静态库的形式做一些处理,让别人无法得知我们的具体实现。
那么我们就可以写一个简单的 .h 和 .c 文件来制作我们的库
这里的头文件和源文件就是库的基本组成,而如果我们正常要使用这 这个源文件中的函数时,我们需要包含该头文件,以及在编译的时候将该源文件编译完之后进行链接。
而对于使用库的用户来说,他们是拿不到 .c 源文件的原码的,那么这要如何完成链接呢?其实也很简单,我们知道每个源文件在链接都是独立进行编译的,而当他们要进行链接时,都是目标二进制文件 .o 了,那么威威反过来,是不是我们只需要将 .c 源文件编译形成的二进制目标文件交给用户,用户就能使用我们的函数的实现了呢?
当我们将生成的二进制文件交给用户,那么用户就能够直接使用了吗? 当然不能。因为我们还没有将头文件交给用户,用户的代码中是包含了我们的头文件的,而我们的头文件并没有交给用户,那么用户的源文件在预处理展开头文件的时候就已经报错了,所以我们不仅要将 .o文件拷贝过去,还需要将.h文件也拷贝到该目录下
这就是一个简单的让别人能够使用我们自己写的函数的方法。 但是对于库来说,一般可不止一个源文件,也就是不止一个 .o 文件,一般一个功能比较完善的库都是有许多个源文件来进行头文件中方法的实现的,如果按照我们上面的方法,那么用户在使用我们的函数的时候,gcc后面则要接一大堆的 .o 文件,这是很麻烦的。同时由于 .o 文件众多,在将这些文件发送给用户的时候,也有可能会在传输过程中丢失某个文件或者某些文件,如果是丢失了头文件还很好发现,如果是丢失了某一个小的 .o 文件,.o 文件可是二进制文件,我们是不好找哪些东西丢失了的。于是库的设计者就尝试将所有的 .o 文件打包成一个文件,这个文件就叫库,而打包的时候采用的打包工具和方式的不同,就有了静态库与动态库的。所以库的本质就是 .o 文件的集合。 打包的时候是不需要将头文件打包进去的,因为库文件和头文件的系统默认搜索路径可能会有所区别,打包在一起没有意义,而是采用分开的形式,这样也能够保证 .o 文件不会丢失某一个,要丢失就全部丢失了,也很容易检查出来,这就是的打包的优势。
2.1 静态库和静态链接
制作静态库我们使用的归档工具是 ar ,具体用法:
ar -rc 形成的文件名 被打包的文件列表
ar是archive,归档的意思, 选项的 -rc ,r是replace,替换,c则是creat,意思就是如果文件存在就替换,如果文件不存在就创建。
我们可以将原来的一个源文件变成两个源文件编译形成.o文件来更好的演示多个 .o 文件的场景
为了方便变易形成.o文件,我们可以修改一下makefile文件
而制作静态库我们就按照上面给的ar工具来制作就行了
要注意一下库的命名,一般是以lib为前缀,后缀的话如果是静态库就是.a,动态库后缀就是.so
这样一来我们就形成了一个库文件,而要想让别人能够使用库,不仅需要将库文件交给别人,也需要将头文件给用户,我们之所以能够使用C语言库的函数,就是因为她给我们提供了库文件和头文件,交付库就是将 .a/.so 问价以及匹配的头文件交给别人,当然一般我们是将其打包成压缩一个安装包,放在网页上或者服务器上交由别人下载。所以我们需要将该.a文件以及头文件放在一个目录下,然后将该目录打包,然后拷贝到用户的路径下,来模拟一个用户下载的过程。
这时候用户目录下就有了一个.tgz压缩包了,首先第一步就是要解压,解压之后就形成了一个包含头文件和静态库的目录
如果这时候我们将头文件和库文件都开被盗系统的头文件和库目录下,就是一个安装的过程。我们先不管这个过程,就按现在的进度来往下走。
当我们继续使用 make 来编译源文件的时候,我们会发现它有报错
首先就是编译器找不到我们的头文件,因为我们在源文件中时使用 #incldue" " 来包含头文件的,他会在当前目录下和系统的头文件目录下来搜索,所以在这里编译器找不到对应的头文件。这时候我们在gcc编译的时候可以指定头文件搜索路径,也就是使用 -I(大写的i)选项来指定,我们修改一下makefile文件
再次使用make指令编译的时候还是会报错,但是这一次报错则不是头文件找不到了,而是函数未定义,也就是库找不到
这也是类似的道理,就是编译器在搜索库文件的时候,也是会在当前目录或者系统的库目录下去搜索,而我们并没有将库文件安装到这些目录下。我们可以使用 -L 选项来指明库的搜索,但是我们发现还是编译不过,刚才的报错还是没有变化
还是找不到库文件,这是为什么呢?这是因为如果我们需要链接第三方库,只指定库的搜索路径是不够的,因为一个目录下可能有多个库,这时候如果只是指明了库路径,编译器是不知道你需要链接的具体是哪一个库的,所以我们还需要指明库的名字。指定头文件搜索路径的时候,我们不需要指定头文件名,这是因为在我们的源文件中有一行代码其实已经指明了头文件名了,也就是我们的#include" "这一行名单时指定库文件的时候,编译器是无法在指定的路径下智能识别或者说做不到判断我们使用了哪些库的,所以我们必须指定库的名字。
但是我们以前写的代码编译的时候并不需要去指定头文件搜索路径和库搜索路径和饥饿库名字,这是为什么呢?因为我们以前写的代码并没有用到第三方库,使用的都是C语言或者C++的标准库,而我们使用的编译器是 gcc/g++ ,顾名思义就是专门用来编译C和C++程序的,那么gcc和g++是能够识别出你需要链接的标准库的,编译器是知道标准库的名字以及安装路径的,但如果是第三方库,gcc/g++则无法识别出来。
指定第三方库的用到的选项是 -l (小写的L),后面跟库的名字。中间一般不加空格,当然也可以有空格。库的名字是指 库的文件名 去掉前缀和后缀才是库的名字
这时候我们使用 file 命令看一下形成的可执行二进制文件的链接属性,
我们发现一个很奇怪的事,就是我们明明使用的是静态库,但是显示这个形成的可执行程序是动态链接的,这是为什么呢?
同时我们使用 ldd 来罗列来程序使用的库的时候,也没有显示我们的第三方库,这又是为什么呢?
这是因为,gcc默认是动态链接,而我们要形成一个可执行程序,可能不仅仅依赖一个库,会用到很多的库,比如一个程序用到了 100 个库,其中有70个是动态库,30个是静态库,而gcc默认是动态连接的,那么这三十个静态库怎么办呢?gcc默认是动态链接,这只是一种建议行为,但是如果没有对应的动态库,只有静态库,那么这时候编译器也只能静态链接。所以说,对于一个特定的库,究竟是动态链接还是静态链接,取决于你提供的是动态库还是静态库,如果有动态库,那么它默认就会进行动态链接,如果只有静态库,那么也没办法,会进行静态链接,将程序中使用到的代码拷贝到可执行程序中,编译器在链接多个库的时候也是按照这样的规则一个一个进行链接的。
当链接完所有依赖库形成可执行程序之后,只要有一个库是动态链接的,我们就认为这个可执行程序时动态链接的。比如我们上面的程序使用了stdio.h 这个头文件,会链接对应的库,由于我们没有指定要静态链接,它默认会以动态库的形式链接,所以我们的可执行程序就是动态链接的。
而我们上面的编译链接的时候 gcc 后面带了一堆的选项,用起来十分复杂,有没有更简单的方法呢?当然有,比如我们可以将库文件和头文件安装到编译器的系统默认路径下,这个过程叫做安装。
这时候我们在命令行直接使用gcc来进行编译
我们发现他还是会找不到库,这是因为,即便拷贝到了系统路径下,因为他是第三方库,所以编译器还是无法自动识别,需要我们指定库的名字
但是不建议这么做,因为系统路径下是有很多的标准库的,标准库是经过了很多检验才被列为标准库的,而我们自己写的库可能有一些bug,这样就会污染系统。
我们将自己的头文件和库文件从系统默认路径下删除的过程就叫卸载
所以第三方静态库的制作其实是很简单的,只需要使用ar归档工具就行了,麻烦的是第三方库的使用。
同时在这里我们也要说明一点,这里的系统默认搜索路径并不是PATH中的路径,PATH中的路径是系统执行可执行程序是的默认搜索路径,而不是库的搜索路径。上面讲的默认搜索路径指的是gcc编译器的搜索路径,它的内部有自己配置文件,知道自己的标准库和头文件所在的路径。
2.2 动态库和动态链接
动态库的制作我们就不需要ar工具了,我们说过 gcc/g++默认是动态链接的,所以gcc给我们提供了制作动态库的功能。同时,动态库的形成还有一点与静态库不同,就是在编译形成.o文件的时候我们需要加 -fPIC选项,表示形成与位置无关码
形成.o文件之后。打包形成动态库的时候我们使用 -shared 选项来制作动态库
gcc -shared -o 形成的库名称(需要前后缀) 被打包的文件列表
这样我们就形成了一个动态库了。同样要给别人使用的话还是需要将头文件和库文件放在一个目录下并打包再供别人下载。这个过程就不再重复示范了,我们直接将头文件和库文件拷贝到用户代码的目录下的子目录下。我们发现了一个很神奇的事,就是我们使用之前静态库的编译的方法能够编译成功,但是形成的可执行程序却运行不了,显示的报错信息是无法打开动态库,因为系统找不到该文件
这就涉及到了动静态链接的区别了,我们前面使用的静态库静态链接的方案,在链接的时候,程序所使用的的第三方库中的代码以及被拷贝到了程序中,这时候我们运行这个程序的时候,是不需要静态库的。而如果我们采用动态库动态链接的话,因为我们的程序所用到的第三方库中的函数的代码并没有拷贝到程序中,在我们运行该程序的过程中的某一个时间点,我们是必须要打开或者说找到所链接的动态库然后将其加载到内存的,执行代码的操作是由cpu完成的,而加载动态库到内存则是由操作系统来完成的,但是操作系统是找不到我们的动态库的。我们前面使用gcc编译的时候加了一堆选项,这些工作都只是让编译器能够找到我们的动态库和头文件来完成他的编译链接操作,但是我们并没有给操作系统指定我们的动态库和头文件的搜索路径,操作系统或者shell就只会在他的默认路径下去搜索用到的动态库加载到内存,而我们刚刚所制作的库并没有在他的默认路径下,所以操作系统找不到库文件。
编译器只负责形成可执行程序,形成可执行程序之后,对可执行程序的其他操作则跟编译器无关了
所以使用动态库是有两个问题要解决的 :
1 让编译器能够找到头文件和动态库文件
2 让操作系统能够找到头文件和动态库文件
这其中第一个问题的解决方案就是静态库的使用方法,第二个问题的解决方案我们在下面来讲解
shell在执行命令搜索库的时候,除了会在系统的默认路径下搜索,还会在一些其他的路径下去搜索,比如我们的环境变量 LD_LIBRARY_PATH 环境变量中保存的路径,
这个环境变量我们初次查看的话可能内容很少,因为我们目前用到第三方动态库的时候还不多,但是在之后我们逐渐使用第三方库来写我们的代码的时候,就会逐渐向该变量中增加新的路径,要注意的是,我们添加到该环境变量的路径必须是绝对路径。修改环境变量我们使用 export来修改,同时要注意不要将原来的内容也删除了,而是要在原内容的基础上增加新的路径。但是这只是一种临时的解决方案,因为每次重新链接我们的Linux机器他都会重置,除非我们修改他的配置文件。直接修改环境变量的这种临时方式我们就不讲了,我们讲一下如何永久配置。
我们可以在系统中找到 /etc/ld.so.conf.d/ 这个路径,在这个路径中回归存在很多的配置文件
我们只需要将我们的动态库的搜索路径写道这个目录下的任意一个配置文件(不推荐)或者是重新创建一个新的文件进行写入(需要sudo提权),创建的文件后缀需要是.conf。
这样就OK了。可能有的人这样还是显示找不到库文件,是因为还没有刷新缓存,这时候只需要在配置文件的路径下使用 ldconfig 命令(需要sudo提权)来刷新一下动态库路径缓存就行了,这就永久有效了。
在试验完之后我们最好是把我们自己创建的配置文件删除。
第二种方法就是使用软链接的方式,具体怎么用呢?在程序所在目录下创建一个软连接文件,这个软连接的文件名就是我们的库文件名,他链接到我们的库文件。
sudo ln -s 库文件所在位置 库文件名
这里的软链接文件名一定要和库文件名一样,这样系统在执行程序的时候,需要加载动态库的时候会在当前路径下搜索库文件,于是就能够找到与库文件同名的软链接,而我们的软链接其实就是链接到了真正的库文件。
这一种方法就相当于将库文件拷贝到了程序所在路径,只是使用软链接的方式节省了空间。
3 动态库加载问题
静态库需要加载吗?不需要,我们一般不考虑静态库的加载问题,因为使用静态库的话程序中用到的方法在链接的时候就已经拷贝到了程序中,毫无疑问是靠内到了代码段,我们的程序在编译链接的时候就已经按照虚拟地址空间的规则以及划分好了代码段数据区等区域了,都是一些绝对的虚拟地址也就是确定的地址,在加载到物理内存的时候就是直接映射到虚拟地址对应的物理地址上,运行程序的时候就不再依赖静态库了,静态链接拷贝进来的这部分函数的代码其实和我们自己写的代码没什么区别。但是如果多个进程的代码都用到了相同的库中的相同的函数,那么在内存中就相当于存在多份同样的二进制代码,这样就造成了空间上的浪费。
那么动态库是如何加载的呢?
首先我们知道动态链接并没有把代码拷贝进我们的程序中,而是将动态库中指定函数的地址写入到我们的可执行程序的相应位置。那么这个地址是什么地址呢?我们再使用 gcc 制作动态库的时候使用了一个 -fPIC 选项,也就是产生与位置无关码。我们知道路径有相对路径和绝对路径的区别,而函数的地址,也是有相对地址和绝对地址的区别的。 比如我们自己在程序中写的函数以及静态链接拷贝到程序中的函数,他们是直接被放在代码区的,在编译链接的时候就已经为这些函数分配好具体的虚拟地址了,而在加载进内存的时候就是按照对应的虚拟地址然后通过页表映射到物理内存上,这些虚拟地址是已经固定好了的。 但是对于动态链接的函数,每一次加载动态库到物理内存中的物理地址是不一样的,同时我们又无法在编译链接的时候直接为函数分配具体的虚拟地址,因为在程序中并没有函数的实现,那么我们写进程序中的地址是什么地址呢?我们可以称为偏移地址或者说相对地址,我们为每一个动态库加载到内存之后都有一个起始地址,我们称之为start或者是库的名称,而偏移地址就是我们需要的函数相对于起始地址的偏移量,动态链接采用的就是起始地址加偏移量的方案,填进可执行程序里的是偏移量地址(也是在代码区),也就是该函数在库中的偏移量。 当cpu执行到函数调用的代码时,拿到偏移地址通过页表读取的时候会发现这个地址不存在,而程序在编译的时候也会保存一些动态库相关的信息以供操作系统加载我们的动态库,首先操作系统会判断我们的动态库是否已经加载到内存了,如果没有加载进来,那么操作系统就会先去将动态库加载进内存,动态库的加载并不是只加载我们所用到的代码,而是会将整个库文件都加载进来,有与库文件可能会很大,操作系统也可能会采取分批次加载的策略。总之,操作系统在将动态库加载到内存之后,就可以将库中的内容通过页表映射到虚拟地址空间的共享区中,映射的时候就决定了这个库在这个进程的起始虚拟地址,而有了起始地址和偏移地址,我们就能够在共享区中找到对应的函数进行调用。
所以与位置无关码选项形成的库里面的函数的地址不是绝对地址,而是相对地址,形成了动态库特定格式。我们的库加载到内存的哪个地方,都能通过库的起始地址和偏移地址来找到函数的地址,同时也使得库不会被局限在代码区中,而是可以映射到共享区。
上面讲的还是一个进程使用一个动态库的情况,如果系统中的很多个进程都是用到了该动态库,而我们只需要加载一份动态库的代码到内存中,他在物理内存中由操作系统维护,大大节省了空间。