首页 前端知识 深入浅出动静态库

深入浅出动静态库

2024-05-19 09:05:23 前端知识 前端哥 803 562 我要收藏

🌎深入浅出动静态库


文章目录:

深入浅出动静态库

    软硬链接
      软连接
      硬链接
      软硬链接应用场景

        软链接应用场景
        硬链接应用场景

    动静态库

      初识动静态库
        动态库
        静态库
        动态链接和静态链接

      动静态库的制作和使用
        静态库打包
        动态库打包

    动态库查找问题
      自动化构建动静态库

    动态库加载问题

      动态库加载的理解
      简单认识可执行程序的编址

      动态库链接和加载的问题
        一般程序的加载
        再谈动态库


前言

  当你在Linux系统上编写和运行程序时,动态库和静态库是两个非常重要的概念。它们不仅影响着程序的编译和执行效率,还直接关系到程序的可移植性和灵活性。

在这里插入图片描述


🚀软硬链接

✈️软连接

  我们直接对文件进行一个软链接,我们可以使用如下命令:

ln -s file.txt link.soft#对file.txt进行软连接

在这里插入图片描述

  ln表示link,-s 选项表示 soft。这样就可以对文件进行软连接,软连接后的效果如上图所示。我们查看它们的inode,就可以发现:

在这里插入图片描述

  从软链接 有独立的inode编号 我们可以知道,软连接本质上是一个独立文件,软链接内容里面放的是 目标文件的路径

  软链接的这种方式就像windows下创建的快捷方式。

在这里插入图片描述


✈️硬链接

  首先,创建一个硬链接文件,我们使用如下命令来创建硬链接文件:

ln file.txt link.hard#硬链接

在这里插入图片描述

  同样,我们查看文件的inode号,可以发现:

在这里插入图片描述

  从硬链接的inode号与原文件的inode号相同,所以 硬链接的本质 不是 一个独立的文件。而我们画蓝色框框的部分也增加了。其实这部分 表示的是 文件的硬链接数

我们来看一个有趣的现象:

在这里插入图片描述
  我们明明只对 file.txt 文件进行了写入,但是我们cat 硬链接文件的时候却也发现,硬链接居然也会显示与原文件相同的内容,如果我们把file.txt文件删除会发生什么事情呢?

在这里插入图片描述
  当我们把原文件删除了之后,但是硬链接文件只有硬链接数减少了,其他的东西都是不变的,包括文件内容。这里删除文件并不是将文件内容清空,而是 将目录里的inode映射关系删除

  我们前面说了,硬链接的本质不是一个独立的文件,因为它的inode号与目标文件是相同的。那硬链接究竟是什么?实际上,硬链接并 没有 新建文件,而是把最新的 文件名 和 目标文件inode号的映射关系 写入到指定的数据块中

在这里插入图片描述

  删除一个文件,inode引用计数就会自动减少,当inode中的引用计数减为0时,这个文件才会真正被删除,才会把 block bitmap 和 inode bitmap 清空。

  一句话来总结:硬链接本质就是 在指定目录下,插入新的文件名和目标文件的映射关系,并且让inode 的引用计数自增


✈️软硬链接应用场景
🚩软链接应用场景

  删除一个文件除了使用 rm 命令,还可以使用如下命令删除文件:

unlink 文件名

在这里插入图片描述

  软链接可以干什么呢?如果你做过稍微大一点的项目,通常在目录下有以下内容:

在这里插入图片描述

  通常来说,我不想我的源代码被其他人看到,所以我把myexe可执行程序放到bin目录下,将来把项目给别人时,别人只能看到 bin、conf、log这三个目录。

  这样别人执行起来就很麻烦,虽然这里目录关系就只有两层,但是在项目当中可执行程序的位置可能在比较深的位置,每次运行就会很麻烦,所以我们可以在当前目录创建其软链接方式:

在这里插入图片描述

  软链接给我们创建了一个快捷的运行方式,在大项目中这种做法很常见。

  除此之外,我们还 可以给目录文件创建软链接 方式:

在这里插入图片描述


🚩硬链接应用场景

  我们来看一个现象:

在这里插入图片描述
  我们创建了一个空目录,但是为何这个空目录文件有两个硬链接数?

在这里插入图片描述

  本质原因是,每个目录都有隐藏目录,隐藏目录包括当前路径和上级路径,而 当前路径的inode编号和创建目录的inode编号相同(文件名不同inode相同),所以 每个目录的硬链接数至少是2

  如果我在这个空目录内新建一个目录硬链接数会如何变化?

在这里插入图片描述

  test目录的硬链接数变为了3,同理,在s目录下的隐藏目录存在上级目录,也就是test本身,所以test的硬链接数会 +1。

在这里插入图片描述

  通过以上例子,我们可以总结出:目录数 = 硬链接数 - 2

  目录可以创建软链接方式,但是却不能创建硬链接,这是个很简单的问题:

  假如目录可以建立硬链接,我们在一些比较常用的目录下建立了根目录的硬链接方式,要知道硬链接并不是一个独立的文件!
  对目录进行搜索是一个很常见的事情,但是当搜索的目录通过你的软链接时,会发生什么?没错,会发生无穷递归问题!

在这里插入图片描述

  但是有人可能还会问:你不是说创建一个文件就有2个硬链接数吗,目录里的隐藏目录不也属于当前目录的硬链接吗?

  这么说也没错,其实这就是Linux的规定,因为Linux默认不信任用户创建目录的硬链接方式,只有自己分配的才没问题。


🚀动静态库

✈️初识动静态库

  其实我们在Windows下很可能也见识过动静态库,比如 xxx.dll 或者 xxx.lib 它们分别是Windows下的动态库文件和静态库文件,而在Linux他们的后缀又有所不同:

  • Windows动态库(xxx.dll)、静态库(xxx.lib
  • Linux动态库(xxx.so)、静态库(xxx.a

  让我们来逐步认识什么是动静态库!


🚩动态库

  你使用过库文件吗?其实我们在最开始学C语言的时候就使用过库,只不过当时在你的视角察觉不到而已,我们对可执行程序使用如下两个命令即可查看链接库情况:

ldd proc_exe#用于打印程序或库文件所依赖的共享库列表
file proc_exe#用于查看文件类型

在这里插入图片描述

  就连最简单的hello world都需要链接C语言的动态库,可见在编写程序时库文件是无处不在。

  动态库又叫做 共享库,在Linux下动态库有自己的后缀名:.so,所以 libc.so.6 其实就是一个动态库。并且 动态库是在程序运行时被加载进来 的!

  • 动态库的优点程序的可执行文件相比于静态库小,便于程序的模块化以及更新,同时,有效内存的使用效率更高,程序编译期间默认使用动态链接
  • 动态库缺点动态库在运行时需要额外的加载和链接过程,会导致性能开销增加。因为是在运行时被加载,所以调试起来也很麻烦。动态库与程序是分离的,因此具有版本依赖性。

  在Linux下不论动静态库,去掉前缀 lib 后缀 .a或.so 剩下的就是 库的名称,所以libc.so.6(glic的软链接) 的库名称就是 c,即C的运行库。


🚩静态库

  我们代码编译成可执行程序的时候 默认执行的是动态链接,也就是使用动态库。而如果想要使用静态库,需要再编译阶段带上 -static 选项:

gcc -o xxx xxx.c -static#静态编译

在这里插入图片描述
  在编译时带上此选型,编译链接就变为了静态链接。图中也可以看到静态链接的可执行程序文件大小比动态库大得多。我们也可使用ldd和file命令查看:

在这里插入图片描述

  • 静态库优点静态链接的可执行程序,是将静态库拷贝到自己的可执行程序中,所以其可移植性高、部署简单。不需要动态加载,性能更高。
  • 静态库缺点由于是直接将静态库拷贝下来,所以静态库文件一般相对很大,如果都采用静态链接将是一个不小的空间消耗。更新维护难,当库更新时相关的程序全部需要重新编译链接。

  静态库一般需要下载下来,使用如下指令下载:

sudo yum install glibc-static

🚩动态链接和静态链接

  在动态链接中:程序在运行时通过动态链接器将所需的库加载到内存中,而不是将库的代码和数据复制到可执行文件中。所以,可执行文件只包含了程序的代码和对库的引用,所以动态库也叫做 共享库。

  在静态链接中:编译器将程序所需的所有库的代码和数据都复制到可执行文件中。这说明可执行文件独立于系统上的其他库,并且包含了程序运行所需的所有代码和数据。


✈️动静态库的制作和使用

  在说之前,我们有一件事情需要明白,为什么我们需要库?如果你是制作者,那么站在制作者的角度我们认为为什么需要库?我们来看一个例子:

  阿熊是个学渣,经常旷课迟到,阿熊的C语言成绩很差,但是作业也不得不交,因为这关乎着阿熊期末能不能过的问题。有一天C语言老师布置了一个大作业,听其他同学说不简单,但是阿熊没放在心上。
  “明天就是截止日期了,这次大作业占不少平时分” 阿熊的室友阿智说,阿熊听到以后立马就急了,于是阿熊苦苦哀求,让阿智把C语言给他拷贝一份,在阿熊的软磨硬泡之下,阿智终于还是不耐烦答应了。
在这里插入图片描述
  但是这个大作业阿智写了多个.c的源文件以及头文件,阿智一想,阿熊肯定会直接看也不看把我的东西交上去,不行…那就这样吧!于是阿智对阿熊说:“这个作业很重要,我不能给你源代码,但是我可以给你目标文件和头文件,这些都有了main函数你就自己写!”

  当阿熊拿到这些文件之后,解压放在自己的目录下,头文件包含后就可以使用了,虽然看不到源代码,但是也不用阿熊自己造轮子了。而这样打包的方式,其实就有一点库的影子了。

  实际上, 其实就是 将所有的 .o 文件用特定的方式进行打包,形成一个文件!

  那么打包的这些文件能现main.c的main.o文件吗?肯定是不行的,一个C语言文件里只能出现一个main函数,所以现在看来,我们为什么要有库?

  • 提高开发效率
  • 隐藏源代码

🚩静态库打包

  今天,我创建了一个空目录test,把以前写过的一些源文件和头文件放在目录下:

在这里插入图片描述
  声明和定义是分离的,我们并不需要关心代码里写的到底是什么,我们只需要将其打包形成一个库文件能用即可。

  首先我们创建一个用户的空目录,然后我们可以使用如下命令来 将源文件编译为目标文件

gcc -c xxx.c#形成目标文件

在这里插入图片描述

  我们把.o文件和.h文件全部放在usr目录下,那么以后,这里的usr目录不就是我们前面提到的给阿熊打包的文件吗?

在这里插入图片描述

  那么阿熊就拿到了usr目录,拿到了目标文件和头文件声明,这时阿熊只需要创建main.c的文件,把头文件包含就可以使用了:

mystdio.h

#pragma once 

#include<stdio.h>

#define SIZE 4096
#define NONE_FLUSH (1<<1)
#define LINE_FLUSH (1<<2)
#define FULL_FLUSH (1<<3)

typedef struct _myFILE
{
    char outbuffer[SIZE];
    int pos;
    int cap;
    int fileno;
    int flush_mode;
}myFILE;

myFILE *my_fopen(const char* pathname, const char* mode);

void my_fclose(myFILE* fp);

int my_fwrite(myFILE* fp, const char* s, int size);

mymath.h

#pragma once 

int myAdd(int a, int b);

main.c

#include<stdio.h>
#include "mystdio.h"
#include "mymath.h"

int main()
{
    int ret = myAdd(10, 20);
    printf("%d+%d=%d\n", 10, 20, ret);

    myFILE* fp = my_fopen("log.txt", "w");
    if(fp == NULL) return 1;

    return 0;
}

  可见在main函数里,阿熊并不需要自己造轮子,只需要调用写好的函数接口即可。

在这里插入图片描述

  其实这就是最朴素的库文件,后续,开发者为了更加方便使用库,于是就把.o文件打包,我们可以使用如下命令打包:

ar -rc libxxx.a xxx.o xxx.o ...#将.o文件编译打包为静态库
  • -rc(replace && create) 选项表示 替换或创建 的意思。

  那么阿智再给阿熊发代码就不用那么麻烦了,把所有的 .o文件编译打包为静态库,然后发给阿熊就可以了!

在这里插入图片描述
  可见我们把.o文件全部编译打包成了静态库文件,其中库的名称为去掉前缀和后缀所以这个被打包的静态库叫做 myc

  这个时候阿熊又来要代码了,于是,阿智就可以把当前面目录下的头文件和静态库给阿熊就可以啦:

在这里插入图片描述
  阿熊拿到之后main函数也写了,直接编译:

在这里插入图片描述
  这个时候反而报错了,其实这是因为存在链接错误,这里就要提出另外的一些该概念了。

  在Linux中,我们gcc 默认只认识C语言的库,我们这种自定义的库,也叫做 第三方库

  我们想要使用第三方静态库,需要使用如下命令:

gcc xxx.c -llibname -L path#编译第三方静态库
  • -l选项该选项表示需要链接 库的名称(无前后缀),并且库名紧跟在选项之后
  • -L选项需要链接库的路径,不加默认在C语言库中搜索

所以,我们编译main.c就可以这样使用:

在这里插入图片描述

  当然,如果你觉得麻烦,不想带第二个选项也是有办法的,直接把头文件拷贝到系统中默认头文件目录下、把自己的库拷贝到Linux系统下的库目录中。那么在以后你只需要告诉编译器链接这个库即可编译。

  而这种 把头文件和库文件拷贝到系统的头文件目录和库文件目录下的行为 就叫做 安装!但是我并不建议这么做,因为这么做很可能会污染系统的生态环境!


🚩动态库打包

  动态库打包与静态库打包稍稍有些区别,首先在编译期间,我们需要带上一个选项:

  • fPIC产生位置无关码(position independent code)

  位置无关码不需要现在知道是什么,在本文最后一个话题会有详解,我们只需要知道在 编译时要带上这个选项 才能进行接下来打包动态库的过程。

在这里插入图片描述
  打包动态库和静态库不同,并不需要借助像 ar 命令这样的打包工具,直接使用gcc来编译 .o文件,需要带上额外的选项:

  • -shared选项表示生成共享库的格式

  所以使用如下命令来将上面两个.o文件打包为动态库:

gcc -shared -o libmyc.so mymath.o mystdio.o#库的名称必须要带有前后缀

在这里插入图片描述
  这样,我们就可以打包一个动态库了。

自动化构建动态库打包过程

Makefile:

  • $<将依赖的文件列表依次取出
libmyc.so:mymath.o mystdio.o
	gcc -shared -o $@ $^ 
mymath.o:mymath.c
	gcc -c -fPIC $<
mystdio.o:mystdio.c
	gcc -c -fPIC $<

.PHONY:clean
clean:
		rm -rf *.o libmyc.so

  或者使用makefile的通配符:%,作用与Linux * 号的作用相同,所以我们还可以:

libmyc.so:mymath.o mystdio.o
	gcc -shared -o $@ $^ 
%.o:%.c
	gcc -c -fPIC $<
	
.PHONY:clean
clean:
		rm -rf *.o libmyc.so

在这里插入图片描述

  同样如果阿熊再来问阿智要代码,我们可以把动态库和头文件放在usr目录下:

在这里插入图片描述

  同样,我们对main.c进行编译同样编译不过,因为gcc默认在系统的库/lib4/目录下查询,所以需要在编译的选项中声明到底使用哪个库文件,以及库文件的路径信息。

在这里插入图片描述

  我们使用ldd命令查看是否依赖了我们的第三方库:

在这里插入图片描述
  可以看到这里不仅仅依赖了C语言库里的libc同时也成功的把我们的第三方库加载了进来。

完善自动化构建动态库打包过程:在原来的基础上加上一些目录文件,以便于更好的归类。

libmyc.so:mymath.o mystdio.o
	gcc -shared -o $@ $^ 
mymath.o:mymath.c
	gcc -c -fPIC $<
mystdio.o:mystdio.c
	gcc -c -fPIC $<

.PHONY:clean
clean:
		rm -rf *.o libmyc.so output 

.PHONY:output
output:
		mkdir -p mylib/include
		mkdir -p mylib/lib 
		cp -rf *.h mylib/include
		cp -rf *.so mylib/lib

效果如下:

在这里插入图片描述


🚀动态库查找问题

  我们前面不论是动态库还是静态库,都是在同一目录下链接到对应的动静态库,但是通常,我们需要编译的源文件并不和库文件在同一目录下,这样我们还能编译成功吗?

在这里插入图片描述
  我们尝试来编译main.c文件:

在这里插入图片描述

  其实gcc在编译时,同样默认会在Linux中的头文件目录下查找,如果在其他地方,需要带上以下选项:

  • -I选项指定路径下搜索头文件

  同样,也需要使用-L和-l选项:

在这里插入图片描述
  这样我们就可以把main.c文件编译为可执行程序了。但是为什么我们运行的时候叒报错了?我们编译静态库也不会这样啊?

  从报错信息来看,这是一个运行时错误,而静态链接是直接拷贝到可执行程序中,并不参与运行时的情况,所以这就是与静态库没关系的原因。至于为什么会运行时错误,我们不妨使用ldd对可执行程序查看:

在这里插入图片描述
  可执行程序在系统中找不到这个库!为了解决这个问题,我会提供四种解决问题的方法!


  • 安装自己库的路径到系统中

  把自己的所有库文件全部拷贝到系统库目录(/lib64)下,在拷贝的时候需要sudo权限,因为往库文件拷贝本质是在安装,安装在系统库中,可执行程序就既可以编译又可以运行

在这里插入图片描述

  这样我们的可执行程序就能立马找到我们的库文件了,但是我前面也说过,并不推荐这种做法。


  • 配置环境变量

  我们可以通过配置叫做 LD_LIBRARY_PATH 的环境变量,这个环境变量的作用是,程序运行时,动态库查找的辅助路径。当在lib64目录下找不到时,会从这个环境变量记录的路径下查询。

  没错,我们可以把自己的库文件路径导入到环境变量当中:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xzy/NewDir/Test2/link/test/usr/mylib/lib

在这里插入图片描述
  这样就能链接到动态库了。在运行试试:

在这里插入图片描述
  但是我们都知道,这样导入的环境变量每次重启就会丢失,我们需要把这个导入的操作写到home目录下的 .bashrc 文件里,这样每次重启也不会丢失路径了。

  有些人的机器里可能没有这个环境变量,当然,没有可以创建,可以参考另外三种方法。


  • 使用软链接

  其实还有一种非常简单的方式,就是创建软链接的方式,把第三方库和系统库建立软链接:

在这里插入图片描述

  在这四种方法中,这是最简单的一种,也是我最推荐的一种方法。


  • 更改系统配置文件

  在Linux的/home/etc/目录下有一个叫做 ld.so.conf.d(链接动态库配置文件) 的目录,在该目录下我们可以自建用户级的配置文件 文件格式:xxx.conf后缀必须为==.conf== 才行。

  而这个配置文件里的内容,我们只需要添加路径即可,系统会默认从你给的文件路径下查找。这些完成之后,最后还需要下面这条指令来让你的配置文件生效:

ldconfig

在这里插入图片描述

  上面四种方法任你喜欢的使用,非要推荐的话,如果你使用的比较正式的官方的库,建议直接使用第一种方法。其他情况我建议使用第二种方法。


✈️自动化构建动静态库

  我们构建项目,说不准会用到那个库文件,所以干脆把动静态库都编出来,但是稍微大一点的项目文件很多,所以我们可以使用makefile来自动化构建项目:

libmyc.a:mymath.o mystdio.o
		ar -rc $@ $^
		rm *.o#删除静态库目标文件
%.o:%.c
		gcc -c $<

libmyc.so:mymath.o mystdio.o
		gcc -shared -o $@ $^ 
%.o:%.c
		gcc -c -fPIC $<

.PHONY:clean
clean:
		rm -rf *.o libmyc.so mylib *.a

.PHONY:output
output:
		mkdir -p mylib/include
		mkdir -p mylib/lib 
		cp -rf *.h mylib/include
		cp -rf *.a mylib/lib
		cp -rf *.so mylib/lib

  有个小细节,因为静态库与动态库生成的.o文件不同,所以生成静态库之后就需要把生成的.o文件删除。

在这里插入图片描述
  这样就可以高效且快速的构建项目了。

  
  当两个库都存在的情况下,gcc编译程序时默认链接的是动态库!如果需要链接静态库,就需要带上 -static 选项:

在这里插入图片描述
  当我们把动态库删除了在使用gcc,默认不用-static选项呢?

在这里插入图片描述
  当我们不使用-static选项且 没有动态库时,可执行程序也没有办法,只能对库进行静态链接,但程序不一定整体是静态链接的!但是反过来,我们只提供动态库,非得让程序进行静态链接就会报错


🚀动态库加载问题

✈️动态库加载的理解

  在Linux当中,库默认就是一个磁盘级文件。而我们 库函数 的每一次 调用,都 是在进程地址空间中进行的。动态库 加载后会被映射到共享区中

  我们可执行程序链接的动态库只有一个吗?大部分时候,不论我们的命令还是可执行程序,链接的都不止一个库文件:

在这里插入图片描述

  而我们说过,动态库又叫做共享库,它的本质是 所有系统进程中公共的代码和数据,只需要存在一份。所以,每一个被需要用到的 动态库只需要被加载到内存一次,哪个进程调用动态库只需要自己的进程地址空间和动态库之间由页表建立映射关系即可。

在这里插入图片描述

  到这里,还是有很多东西都没办法完成逻辑自洽,接下来我会对两个问题展开谈谈。

  • Q1:哪些库加载了?哪些库没加载?这些都是由谁来决定?

  库的分配和加载都是 由操作系统决定

  • Q2:系统中可不可以同时存在非常多的已经加载的库呢?如果是如何管理呢?

  当然,系统中是 允许很多库同时存在,这些都是操作系统的工作。操作系统当然也需要管理这些库了,那么OS如何管理这些库?先描述,在组织

struct loadlib
{
	char* libname;
	void *addr;
	unit64_t time;
	struct loadlib* next;
	//...
}

  而这些被管理的库,抽离出主要且相同属性,组织为描述库的结构体,而库与库之间是通过指针相连,于是形成了链式结构,所以从此往后OS 对库的管理就变为了对链表的增删查改


✈️简单认识可执行程序的编址

  在我们的印象中,只要把程序代码写好,使用gcc/g++编译器编译,形成的可执行程序就可以直接使用了,但是却几乎没怎么了解过可执行程序。我们仅仅知道可执行程序运行起来就是进程。

  其实 可执行程序本身是有自己的格式信息的。如果可执行程序在还没有加载到内存的时候,程序中有没有地址呢?我们可以通过下面命令对程序进行汇编查看:

gcc -S xxx.c -o xxx.s -I /path/

在这里插入图片描述

  我们也可以使用如下命令对可执行程序进行反汇编:

objdump -S xxx.exe > xxx.s

在这里插入图片描述

  从这里就可以看出来,在程序被加载到内存之前,可执行程序本来就是有地址的!在可执行程序没 加载之前 也已经基本按照类别(权限、访问属性等)已经将可执行程序划分为各个区域了!

  区域是什么区域?我们可从来没听说过?不急,我们使用如下命令来查看可执行程序的各个区域:

size xxx.exe

在这里插入图片描述

  • text可执行程序的代码区
  • data表示可执行程序的已初始化全局数据区
  • bss表示未初始化的全局数据区(全局变量和静态变量等)

  可执行程序的从代码到数据,每一行都需要进行编址,除此之外,可执行程序还存在一个头部管理信息的区域,比如在头部就存在e_entry的属性,记录着main函数的起始位置。

  而进程能知道我们程序的main函数在哪也是可执行程序头部管理信息区域的功劳,除此之外,其还包含了比如 分区数目、库的使用情况等。

在这里插入图片描述

  为什么我们可执行程序天然就需要编址呢?而你的进程地址空间的数据又是从哪来的呢?以及mm_struct的代码区对不同文件来说可能不同啊?一个5000行代码的文件和5万行代码的文件肯定是不同的啊?等等很多问题。

  我们从编址开始说明,编址方式分为 绝对编址相对编址

  • 绝对编址绝对编址的方式又称为 平坦模式。程序使用物理内存的绝对地址进行访问。这种方式下,程序直接使用物理地址进行访问。
  • 相对编址代码或者数据以某个位置为参考系,相对参考系的偏移量为相对编址。

  对可执行程序进行编址,如果是在32位平台下,那么编址的范围就是 [0, 4GB](2^32),现代编译器采用的是绝对编址,而在这4GB的内存空间中,可执行程序的代码和数据从起始位置到4GB结束位置 线性的进行编址,这其实就是我们的 虚拟地址

  这下就说的通了,如果代码本身在加载到内存之前就带有虚拟地址,那么在使用可执行程序的代码和数据的虚拟地址直接填到 进程地址空间,再来构建页表。所以就可以 通过可执行程序的数据来初始化地址空间和页表 了!

  这里值得注意的是:不仅仅是操作系统要遵守虚拟地址空间,编译器同样也要遵守


✈️动态库链接和加载的问题
🚩一般程序的加载

  程序运行起来就是进程,而task_struct中的mm_struct是一个结构体对象,那么是谁来初始化进程地址空间的呢?

  当程序在初始化地址空间之前,先将可执行程序的表头加载进来,可执行程序的头部的属性字段包括各个区域划分 对地址空间对象进行初始化!所以不同程序有不同的已初始化和未初始化的范围大小。

在这里插入图片描述

  此时,进程地址空间就被我们初始化了。当我们把进程地址空间初始化完毕,那么在磁盘中存着的正文代码部分也会在地址空间中拷贝一份,这个时候程序开始加载到内存中,而在正文代码部分,每行代码在内存中都有了新的物理地址,我们通过页表,将正文代码的虚拟地址和在内存的物理地址之间建立映射关系。

  虽说是建立映射关系,但是建立的前提是需要获得每行代码所对应的物理地址,这个过程我们并不清楚,其实这步操作是CPU在帮助我们完成的,CPU中存在两个寄存器,分别是pc指针和cr3寄存器:

  • pc指针保存正在执行指令的下一条指令的地址
  • cr3寄存器保存着当前任务的页表的起始地址

在这里插入图片描述
  有了这两个寄存器,我们就好办多了,要想执行程序,那就必须要有程序main函数的起始地址,而恰好在可执行程序的头部保存了程序起始位置地址,在初始化地址空间时,其也被pc指针读取,于是代码就可以被pc指针与其他寄存器配合正常执行完毕。而cr3指针通过MMU(内存管理单元)和页表来完成虚拟到物理地址之间的映射。

  pc指针用来读取程序段,而程序段在加载之前都是虚拟地址,也就是说,pc指针负责读取虚拟地址,而cr3指针负责将虚拟地址转化为物理地址。所以 我们可以说,在这里CPU的工作就是 帮一个完整的程序完成虚拟到物理地址之间的转化

  由此,我们最后得出的结论是:地址空间其实是由 操作系统 + 编译器 + 计算机体系结构(CPU)三者共同完成的


🚩再谈动态库

  到现在我们都没有谈论静态库链接的问题,主要还是因为在程序加载到内存之前静态库里的内容已经被拷贝到可执行程序中了,所以并没有什么好说的。

  我们来分析动态库加载过程,首先,动态库在磁盘当中有自己的起始地址,可执行程序在程序段内拥有动态库的起始地址加上需要调用方法位置的偏移量。操作系统检测到当前程序需要加载的动态库个数,接下来就是把库加载到内存中。

  而动态库的加载也是通过CPU把在磁盘当中的 逻辑地址或虚拟地址)转化为内存当中的物理地址。而与静态库不同,地址空间在被初始化时动态库的虚拟地址并不与代码的虚拟地址放在一起,而是放在堆栈中间有一片叫做 共享区 的区域中。

在这里插入图片描述

  在共享区中就会记录下动态库的起始位置和结束位置,所以就可以通过页表完整的将整个动态库的虚拟地址映射到物理地址当中!而当一个程序想要调用动态库时,只需要 从地址空间的共享区找到动态库的起始虚拟地址,再通过该 虚拟地址 + 偏移量 就可以找到动态库中需要调用接口的位置了。


今天的文章就到这里,如果对您有帮助的话,还望点个小小的赞~~

转载请注明出处或者链接地址:https://www.qianduange.cn//article/8811.html
评论
会员中心 联系我 留言建议 回顶部
复制成功!