Linux下动态库链接的详细过程

发布于:2025-08-08 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

1.温故知新

2.动态库


1.温故知新

        上篇博客我们介绍了我们ELF文件,我们知道我们.o文件还有可执行文件都是我们的ELF文件,这个文件在形成的时候在磁盘中就已经被编址了,而且ELF文件有4个部分,分别是ELF header,还有程序头表,节以及节头表,还知道我们ELF文件合并在链接和加载到内存中都会进行合并,链接的时候会将不同文件的节合并,生成程序头表,定义如何将节合并成段,加载到内存的时候会合并,例如我们的代码段,数据段,就是根据我们的程序头表进行合并的。我们还知道当我们的程序形成.o文件,它是无法找到库函数的地址的,还需要对.o文件进行地址重定位才能被执行,所以我们的地址重定位就是在链接时候做的,而我们的ELF在没有加载到内存的时候就有虚拟地址了,所以当我们把磁盘加载到内存的时候,我们本身就有虚拟地址,然后再和物理内存进行映射,填充页表就可以了。而它的虚拟地址就是从我们的段中得到的。

总而言之,我们的ELF文件在我们将文件编译成.o文件,然后链接器对他们的节进行合并,生成程序头表,然后加载到内存的时候,根据程序头表提供的节合并方法,将我们的节合并,就有了代码段,数据段,等不同的部分。ELF文件在我们文件编译链接加载的全过程起到关键的作用。

我们的静态库是不需要动态链接,只需要形成.o文件合并,对他们进行链接,对库函数进行地址重定向即可,加载的时候无需修正,但是动态库不行,

那么我们的动态库是怎么做的呢?

2.动态库

        如果我们的进程想要对动态库进行链接,首先我的进程需要在内存中看到我们的动态库,那我的问题是,我静态库不需要被看到吗?当然不需要,它在链接的时候就被合并进可执行文件里去了。换句话说,当它被加载到内存的时候,就可以被执行,无需进行动态链接了。

我们依赖动态库的文件要跑起来还需要把我们的动态库加载到内存里,加载到内存的哪个区域,加载到栈和堆之间的共享区。我的进程页表还要映射库的虚拟地址和物理内存。

我们的共享区可以映射很多的库

我们的进程A和进程B的共享区可以指向多个相同的库,这样一个库只用在内存中加载一次就可以。

我们的.so也是ELF文件,它在磁盘中也已经编址好了,它是平坦模式编址的,从0开始编址,这样做的好处是我把它从磁盘加载到物理内存,我知道了我的动态库在进程共享区的起始地址,根据偏它在磁盘中的虚拟地址就可以找到我们库的具体的某一个函数的地址了。我们库加载到不同进程的共享区的虚拟地址可能变化,但是它在磁盘中就已经从0开始编址编址好的虚拟地址不会变化。我们把库映射到我们自己的虚拟地址我们肯定知道库的起始地址。我们也知道它的虚拟地址,起始地址+虚拟地址定位库每一个位置。

不知道我们是否还记得我们的ELF header里面有这个程序的入口,我们程序执行的时候是从这个程序入口开始执行的,但是与我们想象的不一样,它并不是从main函数开始执行的,它是从我们的_start开始执行的,它会执行为我们创建一个初始的堆栈环境,初始化数据段, 还有最重要的是它会调用动态链接器来解析和加载程序所依赖的动态库动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确映射到动态库的实际地址。

然后才开始执行我们的main()。一切顺其自然,我们动态库被编译好放到磁盘里去,我们的可执行文件在执行的找到ELF文件,再把动态库加载到内存和页表的共享区建立映射关系,找到我们的动态库建立映射关系,但是有没有发现我们的代码是在代码区的,是不可以被修改的?

为了解决我们这个问题,我们又在数据区放了一个表,叫做全局偏移变量表,简称GOT,里面存放着我们程序要运行所有需要的库函数,我们的数据区是可以被修改的,所以调用函数的时候我们会查表,根据表中的地址进行函数调用,而我们的GOT表会在我们动态库被加载的时候进行修改。

SO我们可以说我们链接动态库的程序,它的库函数地址根本没有被修改,而是去查GOT表进行调用的。

我们的程序在磁盘中就是直接这么写的,它加载到内存不能被修改也不用修改!!!

下面为我们的进程间通信简单介绍一下:

进程间通信,不管怎么通信,我们都是为了让不同的进程看到同一份资源,我们知道我们的进程之间具有独立性,所以我们让不同的进程看到同一份资源是比较困难的,为了克服这个困难,我们发明了多种进程间通信间方法。

匿名管道在有血缘关系的进程间进行进程间通信。

命名管道在不具有血缘关系的进程间进行进程间通信。

我们的共享内存也是在不同的进程间进行通信的方法!!!

我们下期再见!!!

遨游在知识的海洋脑袋疼!!!

3.注

这篇博客是以前写的,现在准备往出发,现在突然学到了我们系统的线程,突然发现,好多知识动手相同的,从开始的进程我们直接贯穿到学习的始终,我真切的意识到我们操作系统为了帮助我们执行任务所做的操作,我们人为了完成我们的任务,我们需要创建大量的进程,我们进程创建出来就是具有独立性的,所以它们进行进程间通信就需要费劲,而我们进程为了完成任务,我们需要创建虚拟地址,创建页表,与物理内存构成映射关系,这里创建页表,又要思考,为啥要创建页表,我直接访问物理内存不就好了,答案是我们直接访问物理内存,不加权限的检查我这个进程就可以随便修改另一个进程的代码了,还有权限越界等等问题,我们需要页表虚拟地址来帮我们维护进程的独立性,做权限检查,并且提高性能,减少内存碎片。而物理内存的东西都是从磁盘中加载来的,磁盘中有许多东西,比如库文件,等等文件,我们为了管理磁盘的文件创立了文件管理系统,采用了分治思想,不同分区的INode可以相同,那为了区分我是哪一个分区,我需要把我的分区挂载到目录下才能区分我是哪个分区的indoe,又为了提高效率,和磁盘IO是缓慢的,我们访问文件又是频繁的,为了帮助我们快速访问的一些文件,我们又在我们的内存创建了目录树,帮助我们缓存,加快我们的访问速率,现在我需要执行一个任务,我首先需要创建一个进程,然后我的进程又需要依赖外部的库,外部的动态库是在我们把进程跑起来就加载到内存里去了,但是只加载属性,只有当它要调具体的库函数,我们触发缺页中断,我们OS进行检查,然后把从磁盘加载具体的函数地址,根据虚拟地址+偏移量的方式访问我们的库函数,我们的代码区又不能修改,为了完成地址重定向,我们又需要去搞一个GOT表,进行动态库地址重定向的修改,而动态库不需要是因为它形成文件的时候就已经完成了所有的工作,直接调就可以。我们不谈我们动态库和静态库的优缺点,我只想说动态库有动态库的特点,静态库有静态库的特点,特定的场景去利用他们的特点,我们就可以比较好的完成我们的任务,然后我们进程搞完了库,我们有的时候需要进程间通信,又搞一个系统调用,创建管道,匿名管道用于兄弟父子通信,因为创建子进程是不会重新搞一份映射关系的,修改的时候才发生中断进行修改,无关进程间通信就需要命名管道,还有共享内存了,让不同的进程可以去访问物理内存的同一块空间,因为进程的独立性,我们可以让不同的任务之间不相互干预,又因为进程的独立性我们进行进程间通信,只有2个字,费劲!!!而我们Linux为了让一个进程可以完成多个任务,我们又搞一个线程的概念出来,我们Linux下没有真正的线程,是通过进程代码的复用来完成对线程的设计的,绝妙的设计,简单造就高效,简单造就理解,简单造就稳定,化繁为简!!!进入线程的学习我们知道了我们线程其实就是在一个进程下创建诸多PCB就是了,它们共享页表共享同一份虚拟地址,所以这些设计特性造就了我们线程进行资源共享就非常容易,但是与此同时,我们是否记得,我们以前学过我们进程出现异常会直接被OS干掉,这里,我们如果一个进程又10000个线程,但是只要一个线程出现异常,OS就会发信号给进程干掉整个进程!!!我们又知道线程是CPU调度的基本单位,Linux下没有真正的线程,我们都叫做轻量级进程,为什么轻量,因为每个都拿自己的一部分代码去执行了,相对于一个进程拿整个代码来说不轻量也怪...我们知道我们的CPU调度轻量级进程是会进行切换的,那我们同一个进程间进行切换就会好不少,因为我们CPU会有缓存信息,代码上下文的缓存都会有,CR3页表里面的内容也不用被切换,高效高效,但是最重要的原因还是大量的cache里缓存的代码不用被切掉。cache的单位都是MB了,是较大的!!!但是我们用户不管,我们学习就是学的线程吗,给我们的指导思想就是线程,这里就不得不说教科书的文绉绉了,因为它给你提供的是设计思想,只带你去概念上学习,不带你从实际进行学习,好比学车一样,它只教你理论,却不教你实践,就要求我们去路上开车!!!博主想说:无理的要求,无理,无理,无理!!!言归正传,我们为了让别的用户快速了解,我们的Linux开发者在它的库里提供了一个pthread的库,来帮助我们使用线程,我们只需要学习pthread的使用,拓宽了用户。还有就是操作系统是躺在中断上的这句话,我们好多东西都是基于中断的,比如缺页中断,我们加载库是只加载它的属性,不全部加载,不然tmd太占内存空间了,但是虚拟地址通过页表的映射关系却已经建立好了,不好,空城计!!!我们进程要调这个库函数,操作系统通过虚拟地址页表去查发现物理地址上毛也没有,发生异常,陷入内核(解释一下我们每次中断或者系统调用会从用户态陷入到内核态,这个时候从内核态返回的时候也可以处理我们的信号,就是那个进程异常就会发出的信号,大部分信号都是直接干掉进程,core类型的信号会帮我们创立一个错误,便于我们调试,用户态就是你用户操作的,内核态是操作系统才能操作的,比如你可以创建一个变量,但是你无法直接创建一个进程,你需要掉fork让操作系统帮你去创建,明白了吧???),查中断处理表处理异常,帮助我们从磁盘中的函数拷贝到我们的内存,进行调用,然后我们好了之后又返回用户态,而且我们说过轻量级进程调度是有时间片的,这个时间片是怎么搞的,我们CPU里面每隔一段很短的时间会发出时钟中断,没触发一次,他回去减少我们的时间片,说白了时间片不就是个数字吗?每一次时间中断我们把这个数字减,到0切掉它,无情,还有我们的键盘写数据,我们怎么知道键盘的数据有没有好,也是中断,又是它,OS不会去主动问键盘好了没?那不太费劲了,我们化被动为主动,我不去问你硬件的资源有没有准备好,而是好了你触发中断,然后我查表去处理你,从进程的调度,到内存到磁盘到虚拟地址到线程我们发现整个操作系统就像一个精密的机器,环环相扣!!!贯穿始终,还没学完以后再说!!!

知识可能难记,但是逻辑是帮助知识的无形大手


网站公告

今日签到

点亮在社区的每一天
去签到