Linux《库的制作与原理》

发布于:2025-07-10 ⋅ 阅读:(16) ⋅ 点赞:(0)

在之前的篇章当中已经了解了文件是如何存储在磁盘当中的,了解了存储的格式以及进程是如何到磁盘当中查询对应的文件,理解了磁盘文件系统的基本运行原理,那么接下来在本篇当中就将学习库是如何制作的;在此包括动态库和静态库,在了解库基本的制作方式之后将了解对应的库是如何加载到可执行程序内的,通过本篇的学习将让我们对C/C++当中提供的各种库有更深的理解,在此我们将理性的认识到动态库二号静态库具体的区别。



1.什么是库以及如何生成库

1.库的概念

在了解库的制作之前我们先要来了解库的定义是什么

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。

本质上来说库是⼀种可执行代码的⼆进制形式,可以被操作系统载如内存执行。库有两种:

• 静态库 .a[Linux]、.lib[windows]

• 动态库 .so[Linux]、.dll[windows]

在centos当中使用ls查询以下路径存在对应的静态库和动态库

//查看C库
//查看静态库
ls /lib64/libc-2.17.so  -l
//查看动态库
ls /lib64/libc.a  -l

//查看C++库
//查看静态库
ls /lib64/libstdc++.so.6 -l
//查看动态库
ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l

 

1.2 静态库制作 

在了解了库的概念之后,接下来就试着实现一个静态库,在此制作的库就是基于之前实现的mylib.c,源码在之前的基础IO当中有实现

Linux《基础IO》

在之前实现的的项目当中再加入以下的文件

mystring.h

#pragma once    
                                                                                                                                       
    
int my_strlen(const char*);  

mystring.c

#include"mystring.h"                                                          
                                                                   
                                                                    
int my_strlen(const char* str)                                                                 
{                                                                  
    const char* begin=str;                                         
    while(*str)                                                    
    {                                                              
        str++;                                                     
                                                                   
    }                                                              
                                                                   
    return str-begin;                                                                                                                  
}         

那么在当前的目录下就存在了以下的文件,在此就可以将该目录当中的所有.c文件编译之后形成.o文件再将其形成静态库。


 

在此形成.o之后就需要使用到ar来将这些.o文件形成静态库

//ar 是 gnu 归档⼯具, rc 表⽰ (replace and create)
ar -rc 形成静态库名称 形成静态库所需的文件

 

那么再形成的以上的静态库之后如果要使用以上创建的库又该如何操作呢?

接下来创建一个Crafting_Library的目录,将原来创建的libmyc.ak拷贝到该目录当中,此时再写一个调用方法的文件user.c,此时要将静态库和目标文件进行链接欸就需要使用到gcc工具,但是这时问题又来了,那就是为什么以下使用gcc进行链接形成可执行程序的时候会报错呢?

 

以上进行使用gcc进行链接的时候就会发现会出现main函数当中找不到对应的自定义的函数,那么这是为什么呢?

由于在此连接的是我们自己创建的静态库,和之前链接C标准库不同,gcc默认是找不到库的位置的,即使要使用的库就在当前操作的目录当中。

在此要解决该问题就需要在使用gcc使用带上-l 选项,在-l之后带上的就是要进行链接库的名称

注意库的名称是lib和.a之间的内容,例如以上的静态库的名称就是myc

但是为什么在以上带上了-l之后还是无法实现链接了,这其实是因gcc在链接的时候默认是不会在源文件的目录下进行查询的。在此要解决该问题就需要在使用gcc再带上-L 选项,这样gcc就会除了再系统当中默认的路径下查询外还会在-L之后的路径进行搜索。 

运行代码输出的结果和用来在之前的目录当中是一样的,这就说明了user.c链接我们创建的静态库成功。

以上的方式其实能实现我们的要求,但是以上的方式还是较为冗余,在此还可以整理一下

在此先在当前的目录下创建一个lib目录,之后再在lib目录当中创建include和mylib目录,在include目录当中存储程序运行需要的头文件,在mylib当中存储链接需要的库文件。

但是这时使用gcc就会出现以下的报错:

 出现以上报错的原因其实是对于头文件默认是在系统或者当前目录的路径下查找的,这时我们将头文件存储在当前目录的二级目录当中这时gcc就会找不到对于头文件,要解决这个问题就需要在使用gcc的时候再带上-I 选项

那么在以上在链接对应的库时如果要实现不带对应的选项就可以实现应该能如何实现呢?

此时其实就只需要将对应的头文件添加到/user/include当中,将对应的静态库添加到/lib64当中

只不过之前不同的是在使用gcc的时候还是要带上-l选项。那么在此就可以理解了库的下载其实就是将对应的库的头文件和库拷贝到对应的路径下。

注:在此不建议将自己设计的库拷贝到系统库的相关路径下,这样会污染系统默认的库环境。

在此可以创建一个makefile文件来实现一键实现对应库的压缩文件,makefile内容如下所示:

libmyc.a:mystdio.o mystring.o    
    ar -rc $@ $^    
mystdio.o:mystdio.c    
    gcc -c $<    
mystring.o:mystring.c    
    gcc -c $<    
    
    
.PHONY:output    
output:    
    mkdir -p lib/include    
    mkdir -p lib/mylib    
    cp -f *.h lib/include                                                                                                                                                                    
    cp -f *.a lib/mylib    
    tar czf lib.tgz lib    
    
    
    
    
.PHONY:clean    
clean:    
    rm -f *.o libmyc.a lib.tgz    

 有了以上的makefile文件之后就可以先实现make指令先创建出制作静态库需要的.o文件,之后再使用make output就可以创建出对应的lib目录;再将对应的文件拷贝到该目录当中,之后再将lib目录压缩为lib.tgz压缩包。

那么接下来就可以直接将该压缩包拷贝到其他目录下,该过程就模拟了将库传输给其他的用户。 

1.3 动态库的制作

以上在了解了静态库的的制作方式之后接下来就继续来了解动态库的制作,相比以上了解的静态库,其实动态库的使用是更加普遍的。在此还是使用和以上动态库相同也是使用之前实现的mylibc文件代码。

现在需要将原来mylibc当中的文件编译形成对应的动态库,在此形成动态库和以上实现静态库不同不再需要实现其他的工具,只需要使用到gcc就可以实现,只不过需要带上 -shared选项.

首先也是要将对应的.c文件编译为.o文件,不过和之前静态库的实现不同的是在使用gcc产生.o文件的时候使用gcc需要使用到-fPIC选项。

 首先在Crafting_Library目录下使用gcc来产生对应的.o文件

注:在此fPIC表示的是:产生位置无关码(position independent code),当前只需要了解到有这一概念即可要详细的了解其中的原因要等到接下来了解到ELF文件的相关概念之后。

以上在产生了对应的.o文件之后接下来就实现将这些.o文件打包为动态库,在此还是使用gcc来实现,只不过需要带上-shared选项否则创建出来的就是可执行程序。

再使用file指令就可以看到以上创建出来的libmyc.so确实是一个动态库文件。


以上创建动态库的操作也静态库一样也是可以使用makefile来自动化编译,接下来再将原来创建静态库使用的makefile进行修改以下的形式:

libmyc.so:mystdio.o mystring.o    
    gcc -shared -o $@ $^    
mystdio.o:mystdio.c    
    gcc -fPIC -c $<    
mystring.o:mystring.c    
    gcc -fPIC -c $<    
    
    
.PHONY:output    
output:    
    mkdir -p lib/include    
    mkdir -p lib/mylib    
    cp -f *.h lib/include    
    cp -f *.so lib/mylib    
    tar czf lib.tgz lib    
      
    
    
.PHONY:clean    
clean:    
    rm -f *.o libmyc.so lib.tgz           

使用make output指令之后确实会产生出对应动态库的压缩包

接下来就可以将动态库的压缩包拷贝到dynamic_lib目录下

接下来在当中的目录下创建出对应的user.c文件此时使用gcc来进行生成可执行程序就会出现以下的报错

出现以上报错的原因是gcc默认是在系统的默认路径下进行头文件的查询的,但是在user.c当中使用到的是我们自己创建的头文件那么此时就需要指定这些头文件的路径,在此和静态库相同也是加上-I选项。

 

带上-I选项之后出现了以上的报错,这是因为当前链接的库myc不是在系统默认路径下的,那么这时就需要使用到-L选项来指定对应库的路径;使用-l选项来指明对应库的名称。

带上以上的选项之后gcc就可以创建出对应的可执行程序了,但是此时问题又来了,当运行usercode可执行程序的时候会发现会出现报错

 此时的问题是当中的usercode当中链接的myc动态库居然是无法找到的,这又是为什么呢?在之前使用gcc时不是告诉了动态库的位置了吗,怎么现在又找不到了呢?

其实通过ldd指令来查看usercode的属性就可以看到其实当前的可执行程序当中是没有链接对应的动态库的,出现这种情况的原因其实是之前只是将动态库的位置告诉给了gcc,但系统还是不知道库的位置。而之前静态链接不会出现该错误的原因是静态链接时就直接将对应的静态库直接拷贝到可执行程序当中了,这就不会出现系统找不到的问题。

 

那么接下来就要来解决以上的问题了,如何让系统在运行usercode程序的时候能找到我们创建的动态库呢?

方法一:

只需要将我们创建的动态库拷贝到/lib64,拷贝之后再运行usercode就会发现该程序能正常运行了。此时使用ldd查看就可以看到系统能找到myc动态库。

方法二:

除了以上直接将动态库拷贝到系统指定的路径下还可以在该路径下建立对应动态库文件的软链接。

注:以上使用ln时之后带的选项-f表示的是强制 

在进行完软链接之后系统在运行usercode程序的时候默认就会先找到软链接文件之后再找到对应的库文件。

方法三:

除了以上直接将动态库拷贝到系统指定的路径下和在该路径下建立对应动态库的软链接之外,还可以通过修改系统当中的一个环境变量LD_LIBRARY_PATH。在OS‘运行程序时除了会进行动态库的查找还会在该环境变量当中查找动态库。

在此只需要将myc动态库的绝对路径添加到该环境变量当中就能让操作系统找到该动态库

以上写入到环境变量当中存在的问题是写入是内存级的当关闭Xshell之后再打开就会发现系统又无法找到对应的动态库了,要解决该问题就需要将写入到环境变量当中的内容写入到系统相关的配置文件当中。

 

方法四:

除了以上的方法之外还可以在系统当中的/etc/ld.so.conf.d/ 当中添加一个后缀为.conf的文件,该文件名可以任意,创建之后在该文件当中添加对应动态库的路径

在此在对应的new.conf文件当中添加动态 库的路径之后接下来再使用指令sudo ldconfig就能重新的加载配置文件。

总结:

让操作系统找到对应的动态库有四种方法,分别是:拷贝至系统指定路径、在指定路径下建立软链接、在LD_LIBRARY_PATH环境变量当中添加动态库路径、在/etc/ld.so.conf.d/ 当中创建新.conf文件

1.4   动静态库总结

以上我们了解了如何制作动静态库的制作,那么接下来再来总结动静态库当中的一些结论

1.当系统当中同时存在动态库和静态库时gcc/g++默认使用动态库

将lib目录当中同时添加静态库和动态库之后再使用gcc生成对应的可执行程序usercode 

那么此时就可以发现可执行程序默认链接的时动态库

若此时非要进行静态链接就需要再使用gcc的时候带上-static选项

注:使用-static时一定要保证存在对应的静态库否则会出现报错。

2. 在Linux当中系统默认安装的大多数库默认都是优先安装动态库

3.系统当中的 库:程序=1:n
库当中一般存储的都是一些使用频率较高的代码或者数据,就例如在C语言当中就封装了对应数学库等。

2. 目标文件

在编译和链接这两个步骤在Windows下IDE封装的很完美,我们⼀般都是⼀键构建非常方便,但一·旦遇到错误的时候呢,在之前的学习当中我们已经了解了Linux当中是如何通过gcc编译器俩完成这一系列的操作的。

我们知道当中编译之后在Linux当中是会生成对应的.o文件,在Windows是会生成对应的对应的.obj文件,那么接下来就来复习一下编译链接的相关过程来为之后的静态库的原理理解铺垫一下。

例如以下的代码:
 

// hello.c
#include<stdio.h>
void run();
int main() {
printf("hello world!\n");
run();
return 0;
} 
// code.c
#include<stdio.h>
void run() {
printf("running...\n");
}

以上的代码当中分为两个文件,在文件hello.c当中声明的对应的run函数,在code.c文件当中实现了对应的run函数。以上使用gcc将两个文件都编译生成对应的.o文件之后

 

其实编译之后生成的.o文件被称为目标文件。在工程当中当需要将其中的一个文件进行重新的编译只需要单独将该文件进行编译即,而不需要浪费时间将所有的文件都进行编译。其实.o文件就是一种ELF格式的文件,是对二进制代码的一种封装。

3. ELF文件 

以上我们已经初步的了解到了.o文件是ELF格式的文件,那么接下来就来详细的了解ELF格式的文件的格式具体是什么样的。

其实ELF格式的文件是可以分为以下的几个部分。

• ELF头(ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。

• 程序头表(Program header table) :列举了所有有效的段(segments)和他们的属性。表⾥记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在⼆进制文件中,需要段表的描述信息,才能把他们每个段分割开。

• 节头表(Section header table) :包含对节(sections)的描述。

• 节(Section ):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执⾏代码,数据节存储了全局变量和静态数据等。

其实无论是.o文件还是可执行程序、静态库、动态库都是ELF格式的文件。 

4. ELF文件的形成可执行程序

以上我们已经了解到了可执行程序和.o文件都是ELF格式的文件,那么在此问题就来了,.o文件是如何形成对应的可执行程序的呢?接下来就来详细的了解

在每个ELF的文件当中是存在多个Section的,例如以上helloc.c和code.c编译生成a.out之后,在此使用以下的指令能查看对应的ELF文件当中各个Section的信息,显示ELF文件的节头信息。节头描述了ELF文件的各个节的起始地址、大小。

readlf -S [文件名]

在此将多个ELF文件进行合并的时候其实是会将相应的Section合并,例如以上的.o文件在生成对应的可执行程序时就先会将多个的.o文件的Section合并。

注:以上使用readelf可以看到ELF当中是存在一个bss的区域的,其实在程序当中是不会将未初始化的数据也存放到数据区当中的,不会为这些未初始化的数据开辟空间,而是只会在bss当中存储这些数据对应变量的名称以及大小,只有等到这些变量真正的定义之后才会为其开辟空间。

当将多个.o合并形成对应的可执行程序之后接下来在将程序加载到内存当中时还会将Section合并为segment,合并的原则为:相同的属性进行合并,例如:可读、可写、可执行。

所以即使是不同的Section在加载到内存当中时也可能会以segment的格式加载到一起,那么这些合并的原则又是在加载过程当中又是从那里得到的呢?

其实在ELF文件当中在程序表头(Program header table)当中就已经存储了Section合并的原则

使用以下的指令就能查看Section合并的segment

readelf -l [文件名]

那么此时问题就来了为什么要将Section合并为segment呢?

通过之前的学习我们知道在磁盘当中是按照4kb为最小的单位进行读取的,其实在内存当中和磁盘一样为对应的数据开辟空间的时候也是不是不要多少就开多少的,进行空间开辟的最下单位也是4kb的。那么这时就会出现不进行合并时,假设页面的大小为4096字节,如果其中一个部分为4097字节,另外一个部分为512字节,那么这样就会占据3个页面,而将这两个部分合并之后就只需要使用2个页面即可。

除此之外,操作系统在加载程序的时候会将相同属性的section进行合并成为一个大的segment,这样就可以实现不同访问权限,从而优化内存管理和权限访问控制。

综上将Section合并为segment的主要原因是为了减少页面碎片,提高内存的使用效率,以及优化内存管理和权限访问。

其实简单来说程序表头(Program header table)和执行表头(Section header table)的作用分别是在运行加载的时候起作用、在链接的时候起作用。

以上我们了解了ELF当中的两个字段的作用,那么接下来再继续的来了解ELF文件当中的ELF header又有什么作用。

在此使用以下的指令就可以查看对应ELF文件ELF header内的内容

readelf -h [文件名]

其实在ELF header当中起始位置存储的一个名为Magic的值,该值的作用是来标识文件的类型让操作系统在打开的时候能识别出文件的类型而使用相应大方式打开。除此之外在ELF header 当中还存储了各个字段的起始以及结束位置的地址,以及各个字段的大小。

其实在ELF header当中最重要的是Entry point address ,在该属性当中存储了对应ELF文件的起始地址,这在程序加载到内存当中发挥了巨大的作用,具体的过程会在接下来的程序加载当中讲解。

整个ELF文件的关系图如下所示:

 

5.理解静态链接

其实无论是静态库的生成还是可执行程序的生成本质上都是将.o文件进行合并的过程,那么只需要将静态链接理解了,那么可执行程序是如何生成的我们也就不难理解了

接下来就通过.o文件和可执行程序和静态库反汇编之后的内容进行比对分析静态链接具体实现过程是什么样的。

使用下的指令就能查看对应文件反汇编之后的代码

objdump -d [文件名]

在此先生成对应的静态库tmp 

接下来分别查看code.o、hello.o、a.out的反汇编代码。

以下是a.out部分反汇编代码

通过以上查看反汇编的代码就可以看出在.o文件中在call指令当中函数的地址是还未确定的,而在a.out可执行程序当中call指令当中函数地址就被确认下来了,这就说明在可执行程序形成过程当中只有等到.o文件链接完之后合并了才会进行统一的编址;在链接的过程当中会修改.o当中没有确认的函数地址。

其实静态链接和可执行程序形成的过程是类似的,就是将库当中的.o进行合并和上述过程⼀样

所以链接其实就是将编译之后的所有目标文件连同用到的⼀些静态库运行时库组合,拼装成⼀个独立的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在⼀起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程。

6. ELF程序加载到内存

以上已经了解了ELF文件是如何生成的,以及该文件的属性是什么样的,那么接下来就来详细的了解ELF文件是如何加载到内存当中的。

首先要来了解一个问题就是在ELF程序中,当程序还未加载到内存的时候有没有地址呢?以及在之前进程学习的时候就了解到每个进程都是有对应的虚拟地址空间的,那么虚拟地址空间进行初始化的数据又是从哪里来的呢?

其实一个ELF可执行程序在在加载到内存之前就已经有了地址,我们知道ELF程序是可以分为一个个的segment的,那么对应每个segment就有对应区域的起始地址,在初代计算机当中每个函数或者变量就可以根据对应segment的起始地址以及对应函数或者变量的相对偏移量来确定位置

如果每个segment的起始地址都是0呢,所以到了当代的计算机当中认为所有的可执行程序都认为是在一个区域内,对应一个变量或者函数只要一个偏移量就可以确认其的位置。

在磁盘当中就将ELF程序以上的编址为逻辑地址,也成为平坦模式编址,其实磁盘上的可执行程序代码和数据编址就是对虚拟地址的统一编制。

注:在此提到的逻辑地址其实和虚拟地址空间当中提的虚拟地址是同一种东西,只不过是叫法不同,这也就说明虚拟地址不仅仅是进程看待内存的方式。

当ELF程序加载到内存的时候就会根据EL文件F当中的Program header table 来初始化进程虚拟地址空间当中的各个区域,在将虚拟地址空间当中的各个区域范围初始化完之后就需要完成物理地址和虚拟地址之间的映射了,这时就需要使用到ElF程序ELF header当中的Entry指针来得到程序的起始地址,之后就该地址加载到CPU当中的EIP寄存器当中。之后再结合CPU当中的CR3寄存器以及MMU内存管理单元来建立页表;完成程序的虚拟地址和物理地址之间的映射关系。

当程序完全加载到内存当中之后就建立了以下所示的虚拟地址空间

可执行程序加载流程如下图所示:

  1. 通过dentry树解析文件路径,获取文件的inode及磁盘存储位置
  2. 将可执行程序从磁盘加载到内存
  3. 初始化程序的虚拟地址空间
  4. 建立虚拟地址与物理地址的页表映射关系

7. 动态链接和动态库的加载

以上我们已经了解了静态链接以及可执行程序是如何加载到内存当中的,那么接下来就接着来了解动态链接是如何进行的以及动态库是如何加载的。

首先动态库的加载是在可执行程序加载之后进行的,首先也是需要找到对应的动态库,这时操作系统就会通过相关的环境变量以及配置文件来找到对应的动态库的位置从而实现动态库加载到内存的操作。

在找到动态库之后就会接着就会在进程虚拟内存空间当中建立动态库物理地址和虚拟地址的映射,在进程的共享区当中会存储库函数或者变量的虚拟地址。

当有多个进行同时使用一个动态库实际上共用的是内存当中的同一个库 

 

实际上相比静态链接动态链接其实是更为常用的,这是因为静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费大量的硬盘空间。

这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成⼀个独⽴的动态链接库,等到程序运⾏的时候再将它们加载到内存,这样不但可以节省空间,因为同⼀个模块在内存中只需要保留⼀份副本,可以被不同的进程所共享
 

动态链接其实本质上是将链接的整个过程推测到程序加载的时候。

 在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口点是 _start ,这是⼀个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。 在 _start 函数中,会执行⼀系列初始化操作,这些操作包括:

1. 设置堆栈:为程序创建⼀个初始的堆栈环境。

2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。

3. 动态链接:这是关键的⼀步, _start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。

4. 调用 __libc_start_main :⼀旦动态链接完成, _start 函数会调用__libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执行⼀些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使⽤了线程)等。

5. 调用 main 函数:最后, __libc_start_main 函数会调⽤程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。

6. 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回 值,并最终调用 _exit 函数来终止程序

动态链接器:

◦ 动态链接器(如ld-linux.so)负责在程序运行时加载动态库。

◦ 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。

环境变量和配置⽂件:

◦ Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其⼦配置⽂件)来指定动态库的搜索路径。

◦ 这些路径会被动态链接器在加载动态库时搜索。

缓存⽂件:

◦ 为了提⾼动态库的加载效率,Linux系统会维护⼀个名为/etc/ld.so.cache的缓存⽂件。

◦ 该⽂件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会⾸先搜索这个缓存⽂件。

因此总的来说程序和动态库之间建立映射以下图示就可以解释:

实际上实现的流程和程序的加载是十分的类似的。 

 

那么在动态库加载完之后接下来在对动态库内的函数进行调用时,对于任何函数只需要知道库的起始地址+方法的偏移量就可以定位到库当中的方法。

因此实际上在动态库加载之前程序当中调用动态库方法处的地址原来是未被确认的,而是等到动态库加载之后再确认的,但是这是问题就来了以上的操作就需要在加载完动态库之后对于代码区进行修改,但是代码区不是只读的吗?怎么能修改呢?

为了解决该问题在程序当中就预留了一段的空间来存放函数的跳转地址,被称作全局偏移表GOT

 1. 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表

2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利用CPU的相对寻址来找到GOT表。

3. 在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。

4. 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT

但是在使用objdump查看可执行程序的时候发现没有什么GOT啊,只有plt,这个plt又是什么呢?

 

实际上我们要知道不仅仅有可执行程序调用库, 库也会调用其他库!!库之间是有依赖的,如何做到库和库之间互相调用也是与地址无关的呢??库中也有.GOT,和可执行⼀样!这也就是为什么大家为什么都是ELF的格式!

 由于动态链接在程序加载的时候需要对⼤量函数进行重定位,这⼀步显然是非常耗时的。为了进⼀步降低开销,我们的操作系统还做了⼀些其他的优化,比如延迟绑定,或者也叫PLT(过程连接表(Procedure Linkage Table))。与其在程序⼀开始就对所有函数进行重定位,不如将这个过程推迟到函数第⼀次被调⽤的时候,因为绝⼤多数动态库中的函数可能在程序运行期间⼀次都不会被使用到。

总而言之,动态链接实际上将链接的整个过程,比如符号查询、地址的重定位从编译时推迟到了程序的运行时,它虽然牺牲了⼀定的性能和程序加载时间,但绝对是物有所值的。因为动态链接能够更有效的利用磁盘空间和内存资源,以极大方便了代码的更新和维护,更关键的是,它实现了⼆进制级别的代码复用。

 

8. 动静态链接总结

• 静态链接的出现,提高了程序的模块化水平。对于⼀个大的项目,不同的人可以独立地测试和开发自己的模块。通过静态链接,⽣成最终的可执行文件。

• 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成⼀个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。

• 而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行⼀个程序,操作系统会首先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。

以上就是本篇的所有内容了,接下来将开始Linux进程间通信的学习,未完待续……。


网站公告

今日签到

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