文章目录
- 前言:
- 软硬链接:
- 动态库和静态库:
- 如何制作静态库?
- 如何制作动态库?
- 动态库 VS 静态库
- 文本写入 && 二进制写入
- 🚀动态库加载与可执行程序与地址空间的关系:
- 🧛♀️可执行程序&&地址空间
- 🎨动态库的加载
前言:
在前面一章,我们介绍了文件系统,下面我们来对关于文件I/O操作的进一步补充,我们来聊一聊软硬链接是个什么,还有我们之前粗略的讲解的动静态库,最后我们想来进一步谈谈地址空间的概念。
软硬链接:
-
先看现象
我在这里创建了两个文件,然后我分别输入指令,然后看看当前目录:
ln -s file_target1.txt file_soft.link
ln file_target2.txt file_hard.link
-
分析现象:
- 软连接之后是一个独立的文件,因为它有独立的inode。
- 硬链接之后不是一个独立的文件,因为它没有独立的inode,用的确是目标文件的inode。
- 属性中有一列‘1’‘’1‘’2‘的数字的,这一列是硬链接数。
-
分析特点:
- 软连接的文件内容 目标文件所对应的路径字符串。(类似于windows的快捷方式)
- 硬链接就是一个文件名和inode的映射关系,建立硬链接,就是在指定目录下,添加一个新的文件名和inode number的映射关系。
- 这些数字其实文件磁盘的引用计数,用来统计有多少个文件名字符串通过inode number指向当前文件。
因为当前创建了硬链接,就有一个“别名”指向同一个文件,因此有引用计数为2.
所以我们定位文件,只有两种方式:
1、通过路径(软链接)
2、直接找到目标文件的inode(硬链接) - 软连接的文件内容 目标文件所对应的路径字符串。(类似于windows的快捷方式)
-
理解目录:
- 任何一个目录,刚开始创建的时候引用计数一定是“2”
- 在目录A下再新建一个目录,会让A目录的引用计数自动+1
- 想要知道一个目录内部有几个目录,要让该目录引用计数 - 2即可。
- 任何一个目录,刚开始创建的时候引用计数一定是“2”
-
为什么创建目录会自动变2呢?
因为每个目录创建的时候都会生成 . 和 … 文件,这就会产生硬链接。
-
Linux系统中,不允许给目录建立硬链接
例:假设你当前的路径为/home/XXX/Work/root_hard
,你对根目录创建了硬链接,这就会导致当操作系统遍历当前路径时遇到root_hard,就会回到根目录,造成死循环。
但是这不同于.和..
,因为所以操作系统都认识这两个小家伙,所以不会造成死循环! -
硬链接的作用:
- 构建Linux的路径结构,让我们可以用
.和..
来进行路径定位 - 一般用硬链接做文件备份
- 构建Linux的路径结构,让我们可以用
动态库和静态库:
我们在前面讲解gcc
的使用时,有对动态库和静态库有粗略的介绍,主要还是对动静态链接做过讲解。我们下面来回顾一下:
在linux操作系统中,我们以后缀 .so 作为动态库,. a 作为静态库
,而在windows中,我们以后缀 .dll 最为动态库,.lib 作为静态库
,当然我们可以在linux查以下该可执行文件运用了哪些库,输入指令:ldd a.out
这里我们默认是使用的动态库,当然你也可以让程序使用静态库,但是默认一般centos是没有静态库这个的,因此你可以尝试安装然后使用静态链接来创建可执行程序,具体的操作可以看看我之前写的blog
。
如何制作静态库?
假设有一天,你承担了一个项目需要编写一个简易计算器,实现最简单的加法运算和减法运算。现在你已经实现了,通过add.c
来实现函数中的算法,add.h
来声明函数,同理sub.c和sub.h
一个是实现一个是声明。但是你在提交程序时他们希望能借鉴你的算法,让别的程序也能用这个方法,同时会给你相应的报酬。本来你只是以为提交一个可执行程序就好了,然后他们输入数字就好了,但现在需要把你辛辛苦苦写的代码交给他们,但是你并不想让他们看到你的代码里面算法是如何实现的,这你该怎么去解决呢?
聪明的你肯定想到了,只要我把代码加密,他们不就看不懂了吗?同时还有满足算法能正常实现,那你又该如何去解决呢?其实对于加密,我们只需要将所编写的代码转换至二进制语言,这样就可以做到计算机能看懂而人看不懂不就好了吗,所以你就尝试使用gcc -c
这个操作,将代码进行汇编成二进制目标文件,即:
gcc -c add.c
最后得到的就是add.o
我们可以打开add.o
看看内容:
不难发现我们什么都看不懂,同理我们也可以对sub.c
也这么做。
那这样子我们还能正常编译gcc main.c add.o sub.o
形成可执行程序吗?
不难发现,当然可以成功也可以正常运行。
可是,那边又来刁难你了,现在他们想让你把这些实现算法的.o文件打包成一个静态库,然后发给他们。那你又该怎么打包成静态库呢?
你知道是使用ar -rc
来进行打包:
这时我们就有了我们自己搞的一个库,但是我们不能直接gcc main.c libmymath.a
这样喔,因为对于C/C++的库,gcc编译器默认是认识,而对于libmymath.a
这是我们自己写的,gcc编译器默认是不认识的,所以对于这种第三方库,我们需要向gcc指明位置,这样才能使用。
gcc - I "头文件位置" -L "库的位置" -l"库名"
以上是gcc拓展出的各个选项,这里要额外说一下库名。
以libmymath.a
来说,这个**库名准确来说是去掉lib和.a
,**得到的mymath
才是库名,如果你不规范的话,是会报错的!
当然你也可以尝试将你打包好的静态库libmymath.a
复制到/lib64
目录下,将各个头文件复制到/usr/include
中,这样你就可以将自己的库和头文件放在系统当中,下次就可以直接#include <sub.h>和#include <add.h>
了。但是不建议这样子做,因为你现在写的这些代码还是有点挫。现在你已经利用静态库生成了可执行程序,所以你就算删除了静态库也不会影响可执行程序的启动。
如何制作动态库?
现在你想要制作一个动态库,首先你需要先将.c文件汇编形成.o文件,但是这里要区别于静态库,我们需要在gcc后面加入-fPIC
这个我们后面讲!
输入:gcc -fPIC -c add.c sub.c
这样就有.o文件了,然后我们在输入指令:gcc -shared -o libmymath.so *.o
这个时候我们想要像一个正常的操作系统那样,存在一个include目录用来存放头文件和一个lib目录用来存放动态库,因此我们将这些有关文件拷贝至各个文件中:
所以我们在要编译形成可执行程序,我们可以这么写:
gcc -I ./include -L ./lib -lmyc
然后我们执行a.out
程序报错了,说找不到我们创建的动态库,我们lld
查看一下:
确实是找不到,那是因为你只是将这个动态库告诉了gcc编译器
并没有告诉操作系统,而操作系统是要在程序运行的时候查找动态库并加载运行。
静态库就不存在这个问题,因为编译期间,已经将库中的代码拷贝至可执行程序内部了!加载和库就没有关系了!
具体的解决方法:
- 将
libmyc.so
拷贝至/lib64
目录下(不建议) - 创建软连接(快捷方式)
sudo ln -s 当前路径的 libmymath.so /lib64/libmyc.so
- 使用环境变量,
LD_LIBRARY_PATH(加载库路径)
,
LD_LIBRARY_PATH = $LD_LIBRARY_PATH:/当前路径
,但是这种方法退出之后就不存在了。 - 将环境变量添加系统中(永久生效)
vim ~./bashrc
- 修改
/etc/ld.so.conf.d
,新增自己的配置文件,再执行ldconfig
加载起来
动态库 VS 静态库
默认链接动态库,如果你没有使用 -static,并且只提供静态库,那么编译器就只能静态链接当前的静态库,其它库正常动态链接。
- -static的意义是什么呢?
必须强制的将我们的程序进行静态链接,这就要求我链接的任何库都必须提供对饮的静态库版本。
文本写入 && 二进制写入
二进制将数据在内存中的样子原封不动的搬到文件中,文本格式则是将每一个数据转换成字符写入到文件中,他们在大小上,布局上都有着区别。由此可以看出,2进制文件可以从读出来直接用,但是文本文件还多一个“翻译”的过程,因此2进制文件的可移植性好。
🚀动态库加载与可执行程序与地址空间的关系:
我们在前面讲解地址空间时,我们有了解过关于内存中的物理的地址和与虚拟地址mm_struct
各个区域的映射关系。那么动态库在加载的时候会处于哪个区呢?
不难发现我们在加载动态库时,是将动态库放在了虚拟地址中的==“共享区”==
我现在先了解一个轮廓框架,我们先深入了解下地址控制与可执行程序相关的话题,然后我们再回头来看动态库的加载,到那时我们就都能串联起来了!
🧛♀️可执行程序&&地址空间
现在我有几个问题,
-
我们形成可执行程序后,代码当中有地址吗?是什么地址?
当我们形成可执行程序后,代码肯定是先经过了预处理检查语法,再进行编译形成汇编语言,然后将汇编语言汇编形成二进制代码供计算机读入。这个时候我们编写一个简单的c语言程序再进行反汇编操作,看看各个汇编指令的地址是如何分布的,可以使用
objdump -S a.out > test.s
来讲代码保存起来。这里的汇编指令太多了,在这里我们只看一部分。
所以我们的可执行程序里面到处都是地址。
同时我们进行
size
可以看到这个可执行程序已经分好了各种区域,类似我们mm_struct中的地址空间划分。
并且我们也发现了可执行程序划分出来的区域,那我们在这里推测可执行程序其实与mm_struct中各个区域划分有关。
一般在Linux形成的可执行程序中,是以ELF格式的可执行程序,二进制食欲自己的固定格式的,包括elf可执行程序的头部,可执行程序的属性
可执行程序编译之后,会形成很多行汇编语句,每条汇编语句都有对应的地址。
既然每条语句都有属于他的地址,那我们可以理解这些一个一个的地址就是我们所谓的虚拟地址!在这里要注意的是,对于一个
test.c
文件,无论你进行多少次gcc
进行编译形成汇编语句,你在反汇编后,每条语句对应的虚拟地址都是一样的不会变化的! -
对于这些语句的地址又是如何编制的呢?
我们在编址时一般有两种:其一是相对编址;其二是绝对编址。
相对编址时,使用起始地址加上对应的偏移量
绝对编制就是我们原本的样子:
绝对编址就是我们本身的方式,也叫做平坦模式,所以整体是线性增长的。
同时我们也可以认为这其实就是虚拟地址! -
“
mm_struct
的各个区域的大小是固定的吗?”先说结论,当然不固定!
我们就拿这一篇博客举例子,我个人现在是在typora上面写博客的,但我写完一篇博客后这个typora的大小差不多是8KB左右。但是我今天想玩CS:GO那对于这个可执行程序就不只是8KB了喔,你所占空间都不一样,那你的代码长度和内容肯定也不一样,那对于他们的mm_struct中的各个区域的大小也就不一样,这是肯定的!
那我们又该如何进行分区呢?
其实我们在打开可执行程序时,默认会打开一个一个动态库:
这个库里面存放着,就是我们的加载器。
我们会先把加载器的库加载到内存中,执行库中的函数将可执行程序拷贝到内存,加载器对可执行程序的头部进行解析。可以得到main函数的地址。
所以 ELF + 加载器 我们就可以解析得知各个区域中的起始位置与结束位置 + main函数地址。
就像我们之前说的,mm_struct内部的各个区域都有对应的start 和 end -
程序是如何加载的?
我们在前面的学习中,知道要执行一个可执行程序,是操作系统创建进程,通过进程来实现的。
进程 = 内核数据结构 + 代码和数据,那我们在执行一个可执行程序时,是先创建PCB还是先将代码和数据加载到内存中呢?
答案就是六个字:“先描述,再组织”所以首先我们会先创建进程的内核数据结构(PCB),再将可执行程序的代码和数据加载到内存中。
既然先创建PCB,那么在将可执行程序的代码和数据加载到内存之前,ELF格式和加载器就已经对一个可执行程序进行了“预处理”,将这个程序的各个区域划分都会找到并且记录起来,所以在创建PCB之后也会生成对应的mm_struct,然后通过加载器将mm_struct的各个区域划分好。
然后可执行程序的代码和数据就会加载至内存当中。
所以虚拟地址空间这个概念不是OS独有的,是由OS+编译器+加载器一起构成的 -
页表的映射关系是怎么创建的?
现在我们的
mm_struct
已经初始化完毕,但是可执行程序加载到了内存当中,所以操作系统需要进行管理,需要建立物理内存地址与虚拟地址之间的映射关系,当然完成这个工作的是“页表”。我们都知道程序的入口是main函数,而我们在由加载器对ELF格式下的可执行程序进行解析时,初始化了
mm_struct
,而mm_struct
不仅对每个分区标记了对应的start和end,还找到了main函数的入口地址。这个地址最先是由CPU中的pc指针
来指向的,pc指针
接下来就会保存当前执行指令的下一条虚拟地址,然后再不断执行。正是有了main函数地址,而我的PCB也创建好了
mm_struct
也初始化好了,那么这时,在磁盘中的可执行程序的代码和数据就会进入内存当中。既然你进了内存里面了,那你操作系统本身就会管理内存,那你既然在内存当中,所以你在内存当中的物理地址我不就知道到了吗?
所以对于main函数来说,你的虚拟地址在我的
pc指针
里,这我能找到,而你的物理地址呢,我操作系统也能找到,那你们之间不就建立起来了映射关系吗?页表不就可以出现来管理了吗?
🎨动态库的加载
在介绍动态库加载的时候,我们知道了动态库是加载到mm_struct
的代码共享区中。库既然是在磁盘当中,那么它也有属于自己的虚拟地址,只不过没有main函数地址罢了。假设现在创建一个库,里面存放着add函数,而我的.c文件
中要执行add函数。
当我们执行到Add时,此时我们的动态库还没加载,那么当然就需要将动态库也加载到内存,同时映射到虚拟地址空间。
这里我们发现,动态库对应的libmyc.so
存在一个虚拟地址,而libmyc.so
库里面也存在一个add函数,这个函数也有一个虚拟地址,那他们之间有什么关系吗?
实际上库中的函数地址就是库函数地址为0的偏移量,当想要访问到库函数时,我们就需要知道他的地址,他的地址就是两者的相加:0x5555+0x1111=0x6666
。
所以有了地址我们就可以动态库的函数了,也就是跳转到共享区,执行完毕再跳回来。
所以这个地址被映射到虚拟地址空间的那个位置重要吗?
并不重要,所以制作动态库时需要-fPIC
,叫做与地址无关码!