软硬链接 && 动静态库(深入地址空间)

前言:

​ 在前面一章,我们介绍了文件系统,下面我们来对关于文件I/O操作的进一步补充,我们来聊一聊软硬链接是个什么,还有我们之前粗略的讲解的动静态库,最后我们想来进一步谈谈地址空间的概念。

软硬链接:

  • 先看现象
    我在这里创建了两个文件,然后我分别输入指令,然后看看当前目录:
    ln -s file_target1.txt file_soft.link
    ln file_target2.txt file_hard.link
    image-20240905145130638

  • 分析现象:

    1. 软连接之后是一个独立的文件,因为它有独立的inode
    2. 硬链接之后不是一个独立的文件,因为它没有独立的inode,用的确是目标文件的inode。
    3. 属性中有一列‘1’‘’1‘’2‘的数字的,这一列是硬链接数。
  • 分析特点:

    1. 软连接的文件内容 目标文件所对应的路径字符串。(类似于windows的快捷方式)
      image-20240905151003977
    2. 硬链接就是一个文件名和inode的映射关系,建立硬链接,就是在指定目录下,添加一个新的文件名和inode number的映射关系。
    3. 这些数字其实文件磁盘的引用计数,用来统计有多少个文件名字符串通过inode number指向当前文件。
      因为当前创建了硬链接,就有一个“别名”指向同一个文件,因此有引用计数为2.

    所以我们定位文件,只有两种方式:

    1、通过路径(软链接)
    2、直接找到目标文件的inode(硬链接)

  • 理解目录:

    1. 任何一个目录,刚开始创建的时候引用计数一定是“2”
      image-20240905151747135
    2. 在目录A下再新建一个目录,会让A目录的引用计数自动+1
      image-20240905153134558
    3. 想要知道一个目录内部有几个目录,要让该目录引用计数 - 2即可。
  • 为什么创建目录会自动变2呢?

    image-20240905155453189

    因为每个目录创建的时候都会生成 . 和 … 文件,这就会产生硬链接。

  • Linux系统中,不允许给目录建立硬链接
    例:假设你当前的路径为/home/XXX/Work/root_hard,你对根目录创建了硬链接,这就会导致当操作系统遍历当前路径时遇到root_hard,就会回到根目录,造成死循环。
    但是这不同于.和..,因为所以操作系统都认识这两个小家伙,所以不会造成死循环!

  • 硬链接的作用:

    1. 构建Linux的路径结构,让我们可以用.和..来进行路径定位
    2. 一般用硬链接做文件备份

动态库和静态库:

​ 我们在前面讲解gcc的使用时,有对动态库和静态库有粗略的介绍,主要还是对动静态链接做过讲解。我们下面来回顾一下:
​ 在linux操作系统中,我们以后缀 .so 作为动态库,. a 作为静态库,而在windows中,我们以后缀 .dll 最为动态库,.lib 作为静态库,当然我们可以在linux查以下该可执行文件运用了哪些库,输入指令:ldd a.out
image-20240910124542371

image-20240910125306473

image-20240910125322740

这里我们默认是使用的动态库,当然你也可以让程序使用静态库,但是默认一般centos是没有静态库这个的,因此你可以尝试安装然后使用静态链接来创建可执行程序,具体的操作可以看看我之前写的blog

如何制作静态库?

​ 假设有一天,你承担了一个项目需要编写一个简易计算器,实现最简单的加法运算和减法运算。现在你已经实现了,通过add.c来实现函数中的算法,add.h来声明函数,同理sub.c和sub.h一个是实现一个是声明。但是你在提交程序时他们希望能借鉴你的算法,让别的程序也能用这个方法,同时会给你相应的报酬。本来你只是以为提交一个可执行程序就好了,然后他们输入数字就好了,但现在需要把你辛辛苦苦写的代码交给他们,但是你并不想让他们看到你的代码里面算法是如何实现的,这你该怎么去解决呢?
image-20240910131933130

​ 聪明的你肯定想到了,只要我把代码加密,他们不就看不懂了吗?同时还有满足算法能正常实现,那你又该如何去解决呢?其实对于加密,我们只需要将所编写的代码转换至二进制语言,这样就可以做到计算机能看懂而人看不懂不就好了吗,所以你就尝试使用gcc -c这个操作,将代码进行汇编成二进制目标文件,即:
gcc -c add.c最后得到的就是add.o
image-20240910132047230

我们可以打开add.o看看内容:
image-20240910132112037

不难发现我们什么都看不懂,同理我们也可以对sub.c也这么做。
那这样子我们还能正常编译gcc main.c add.o sub.o形成可执行程序吗?
image-20240910132402884

不难发现,当然可以成功也可以正常运行。

可是,那边又来刁难你了,现在他们想让你把这些实现算法的.o文件打包成一个静态库,然后发给他们。那你又该怎么打包成静态库呢?
你知道是使用ar -rc来进行打包:
image-20240910145859205

这时我们就有了我们自己搞的一个库,但是我们不能直接gcc main.c libmymath.a这样喔,因为对于C/C++的库,gcc编译器默认是认识,而对于libmymath.a这是我们自己写的,gcc编译器默认是不认识的,所以对于这种第三方库,我们需要向gcc指明位置,这样才能使用。
gcc - I "头文件位置" -L "库的位置" -l"库名"
以上是gcc拓展出的各个选项,这里要额外说一下库名。
libmymath.a来说,这个**库名准确来说是去掉lib和.a,**得到的mymath才是库名,如果你不规范的话,是会报错的!
image-20240910150617291

当然你也可以尝试将你打包好的静态库libmymath.a复制到/lib64目录下,将各个头文件复制到/usr/include中,这样你就可以将自己的库和头文件放在系统当中,下次就可以直接#include <sub.h>和#include <add.h>了。但是不建议这样子做,因为你现在写的这些代码还是有点挫。现在你已经利用静态库生成了可执行程序,所以你就算删除了静态库也不会影响可执行程序的启动。

如何制作动态库?

​ 现在你想要制作一个动态库,首先你需要先将.c文件汇编形成.o文件,但是这里要区别于静态库,我们需要在gcc后面加入-fPIC这个我们后面讲!
输入:gcc -fPIC -c add.c sub.c
image-20240910153347155

这样就有.o文件了,然后我们在输入指令:gcc -shared -o libmymath.so *.o
image-20240910155006608

这个时候我们想要像一个正常的操作系统那样,存在一个include目录用来存放头文件和一个lib目录用来存放动态库,因此我们将这些有关文件拷贝至各个文件中:
image-20240910160515122

所以我们在要编译形成可执行程序,我们可以这么写:
gcc -I ./include -L ./lib -lmyc
image-20240910160656610

然后我们执行a.out
image-20240910160854641
程序报错了,说找不到我们创建的动态库,我们lld查看一下:
image-20240910160947774
确实是找不到,那是因为你只是将这个动态库告诉了gcc编译器没有告诉操作系统而操作系统是要在程序运行的时候查找动态库并加载运行。

静态库就不存在这个问题,因为编译期间,已经将库中的代码拷贝至可执行程序内部了!加载和库就没有关系了!

具体的解决方法:

  1. libmyc.so 拷贝至 /lib64目录下(不建议)
  2. 创建软连接(快捷方式)sudo ln -s 当前路径的 libmymath.so /lib64/libmyc.so
  3. 使用环境变量,LD_LIBRARY_PATH(加载库路径)
    LD_LIBRARY_PATH = $LD_LIBRARY_PATH:/当前路径,但是这种方法退出之后就不存在了。
  4. 将环境变量添加系统中(永久生效)
    vim ~./bashrc
  5. 修改/etc/ld.so.conf.d,新增自己的配置文件,再执行ldconfig加载起来

动态库 VS 静态库

​ 默认链接动态库,如果你没有使用 -static,并且只提供静态库,那么编译器就只能静态链接当前的静态库,其它库正常动态链接。

  • -static的意义是什么呢?
    必须强制的将我们的程序进行静态链接,这就要求我链接的任何库都必须提供对饮的静态库版本。

文本写入 && 二进制写入

​ 二进制将数据在内存中的样子原封不动的搬到文件中,文本格式则是将每一个数据转换成字符写入到文件中,他们在大小上,布局上都有着区别。由此可以看出,2进制文件可以从读出来直接用,但是文本文件还多一个“翻译”的过程,因此2进制文件的可移植性好。

🚀动态库加载与可执行程序与地址空间的关系:

​ 我们在前面讲解地址空间时,我们有了解过关于内存中的物理的地址和与虚拟地址mm_struct各个区域的映射关系。那么动态库在加载的时候会处于哪个区呢?
image-20240911084756307

不难发现我们在加载动态库时,是将动态库放在了虚拟地址中的==“共享区”==

​ 我现在先了解一个轮廓框架,我们先深入了解下地址控制与可执行程序相关的话题,然后我们再回头来看动态库的加载,到那时我们就都能串联起来了!

🧛‍♀️可执行程序&&地址空间

​ 现在我有几个问题,

  • 我们形成可执行程序后,代码当中有地址吗?是什么地址?

    当我们形成可执行程序后,代码肯定是先经过了预处理检查语法,再进行编译形成汇编语言,然后将汇编语言汇编形成二进制代码供计算机读入。这个时候我们编写一个简单的c语言程序再进行反汇编操作,看看各个汇编指令的地址是如何分布的,可以使用objdump -S a.out > test.s来讲代码保存起来。

    image-20240911092745683

    image-20240911092807529

    这里的汇编指令太多了,在这里我们只看一部分。

    所以我们的可执行程序里面到处都是地址

    同时我们进行size可以看到这个可执行程序已经分好了各种区域,类似我们mm_struct中的地址空间划分。
    image-20240911093050444

    并且我们也发现了可执行程序划分出来的区域,那我们在这里推测可执行程序其实与mm_struct中各个区域划分有关。

    一般在Linux形成的可执行程序中,是以ELF格式的可执行程序,二进制食欲自己的固定格式的,包括elf可执行程序的头部,可执行程序的属性

    可执行程序编译之后,会形成很多行汇编语句,每条汇编语句都有对应的地址
    既然每条语句都有属于他的地址,那我们可以理解这些一个一个的地址就是我们所谓的虚拟地址

    在这里要注意的是,对于一个test.c文件,无论你进行多少次gcc进行编译形成汇编语句,你在反汇编后,每条语句对应的虚拟地址都是一样的不会变化的!

  • 对于这些语句的地址又是如何编制的呢?

    我们在编址时一般有两种:其一是相对编址;其二是绝对编址。

    相对编址时,使用起始地址加上对应的偏移量
    image-20240911094703741

    绝对编制就是我们原本的样子:
    image-20240911094731271

    绝对编址就是我们本身的方式,也叫做平坦模式,所以整体是线性增长的。
    同时我们也可以认为这其实就是虚拟地址

  • mm_struct的各个区域的大小是固定的吗?”

    先说结论,当然不固定

    我们就拿这一篇博客举例子,我个人现在是在typora上面写博客的,但我写完一篇博客后这个typora的大小差不多是8KB左右。但是我今天想玩CS:GO那对于这个可执行程序就不只是8KB了喔,你所占空间都不一样,那你的代码长度和内容肯定也不一样,那对于他们的mm_struct中的各个区域的大小也就不一样,这是肯定的!

    那我们又该如何进行分区呢?

    其实我们在打开可执行程序时,默认会打开一个一个动态库:

    image-20240911103633059

    这个库里面存放着,就是我们的加载器

    我们会先把加载器的库加载到内存中,执行库中的函数将可执行程序拷贝到内存,加载器对可执行程序的头部进行解析。可以得到main函数的地址。

    所以 ELF + 加载器 我们就可以解析得知各个区域中的起始位置结束位置 + main函数地址
    就像我们之前说的,mm_struct内部的各个区域都有对应的start 和 end

  • 程序是如何加载的?

    我们在前面的学习中,知道要执行一个可执行程序,是操作系统创建进程,通过进程来实现的。
    进程 = 内核数据结构 + 代码和数据,那我们在执行一个可执行程序时,是先创建PCB还是先将代码和数据加载到内存中呢?
    答案就是六个字:“先描述,再组织”

    所以首先我们会先创建进程的内核数据结构(PCB),再将可执行程序的代码和数据加载到内存中。

    既然先创建PCB,那么在将可执行程序的代码和数据加载到内存之前,ELF格式和加载器就已经对一个可执行程序进行了“预处理”,将这个程序的各个区域划分都会找到并且记录起来,所以在创建PCB之后也会生成对应的mm_struct,然后通过加载器将mm_struct的各个区域划分好
    image-20240911105506252

    然后可执行程序的代码和数据就会加载至内存当中。
    所以虚拟地址空间这个概念不是OS独有的,是由OS+编译器+加载器一起构成的

  • 页表的映射关系是怎么创建的?

    现在我们的mm_struct已经初始化完毕,但是可执行程序加载到了内存当中,所以操作系统需要进行管理,需要建立物理内存地址与虚拟地址之间的映射关系,当然完成这个工作的是“页表”。

    我们都知道程序的入口是main函数,而我们在由加载器对ELF格式下的可执行程序进行解析时,初始化了mm_struct,而mm_struct不仅对每个分区标记了对应的start和end,还找到了main函数的入口地址这个地址最先是由CPU中的pc指针来指向的,pc指针接下来就会保存当前执行指令的下一条虚拟地址,然后再不断执行。

    正是有了main函数地址,而我的PCB也创建好了mm_struct也初始化好了,那么这时,在磁盘中的可执行程序的代码和数据就会进入内存当中。

    既然你进了内存里面了,那你操作系统本身就会管理内存,那你既然在内存当中,所以你在内存当中的物理地址我不就知道到了吗?

    所以对于main函数来说,你的虚拟地址在我的pc指针里,这我能找到,而你的物理地址呢,我操作系统也能找到,那你们之间不就建立起来了映射关系吗?页表不就可以出现来管理了吗?

    image-20240911134758265

🎨动态库的加载

​ 在介绍动态库加载的时候,我们知道了动态库是加载到mm_struct的代码共享区中。库既然是在磁盘当中,那么它也有属于自己的虚拟地址,只不过没有main函数地址罢了。假设现在创建一个库,里面存放着add函数,而我的.c文件中要执行add函数。
image-20240911145154494

当我们执行到Add时,此时我们的动态库还没加载,那么当然就需要将动态库也加载到内存,同时映射到虚拟地址空间。

这里我们发现,动态库对应的libmyc.so存在一个虚拟地址,而libmyc.so库里面也存在一个add函数,这个函数也有一个虚拟地址,那他们之间有什么关系吗?

image-20240911151344997
实际上库中的函数地址就是库函数地址为0的偏移量,当想要访问到库函数时,我们就需要知道他的地址,他的地址就是两者的相加:0x5555+0x1111=0x6666
所以有了地址我们就可以动态库的函数了,也就是跳转到共享区,执行完毕再跳回来。

所以这个地址被映射到虚拟地址空间的那个位置重要吗?
并不重要,所以制作动态库时需要-fPIC,叫做与地址无关码!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无双@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值