当前位置: 首页 > news >正文

【Linux】库制作与原理 从生成使用到 ELF 文件与链接原理解析

文章目录

  • 一、什么是库
    • 总结
  • 二、静态库的生成
  • 三、动态库
    • 动态库的生成
    • 动态库的使用
  • 四、对比动静态库
  • 五、ELF文件
    • 查看ELF文件内容
    • ELF形成可执行
    • ELF可执行文件加载到内存
  • 六、静态链接
    • ELF加载与进程地址空间(虚实地址的转换机制)
  • 七、动态链接与动态库加载
    • 进程如何看到动态库
    • 进程间如何共享库的
    • 动态链接特点
    • _start
    • 程序如何和库映射起来
    • 程序如何进行库函数调用
    • 全局偏移量表GOT
    • 库间依赖
    • 总结动静态链接


一、什么是库

(补充:库中不能出现main函数,因为库会和有main函数的程序链接在一起)
库是写好的现有的,成熟的,可以复⽤的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个⼈的代码都从零开始,因此库的存在意义⾮同寻常。
本质上来说库是⼀种可执⾏代码的⼆进制形式,可以被操作系统载⼊内存执⾏。库有两种:
• 静态库 .a[Linux]、.lib[windows]
• 动态库 .so[Linux]、.dll[windows]

我们看下面的库示例可以知道,库本质就是linux指定目录下的普通文件,所以我们就可以像对常规文件读写那样将库文件内容拷贝到我的可执行文件里,或者将库文件内容加载到内存里。

// ubuntu 动静态库
// C
$ ls - l / lib / x86_64 - linux - gnu / libc - 2.31.so
- rwxr - xr - x 1 root root 2029592 May 1 02:20 / lib / x86_64 - linux - gnu / libc - 2.31.so
$ ls - l / lib / x86_64 - linux - gnu / libc.a
- rw - r--r-- 1 root root 5747594 May 1 02 : 20 / lib / x86_64 - linux - gnu / libc.a
//C++
$ ls / usr / lib / gcc / x86_64 - linux - gnu / 9 / libstdc++.so - l
lrwxrwxrwx 1 root root 40 Oct 24 2022 / usr / lib / gcc / x86_64 - linuxgnu / 9 / libstdc++.so -> .. / .. / .. / x86_64 - linux - gnu / libstdc++.so.6
$ ls / usr / lib / gcc / x86_64 - linux - gnu / 9 / libstdc++.a
/ usr / lib / gcc / x86_64 - linux - gnu / 9 / libstdc++.a// Centos 动静态库
// C
$ ls / lib64 / libc - 2.17.so - l
- rwxr - xr - x 1 root root 2156592 Jun 4 23:05 / lib64 / libc - 2.17.so
[whb@bite - alicloud ~]$ ls / lib64 / libc.a - l
- rw - r--r-- 1 root root 5105516 Jun 4 23 : 05 / lib64 / libc.a
// C++
$ ls / lib64 / libstdc++.so.6 - l
lrwxrwxrwx 1 root root 19 Sep 18 20 : 59 / lib64 / libstdc++.so.6 ->
libstdc++.so.6.0.19
$ ls / usr / lib / gcc / x86_64 - redhat - linux / 4.8.2 / libstdc++.a - l
- rw - r--r-- 1 root root 2932366 Sep 30 2020 / usr / lib / gcc / x86_64 - redhatlinux / 4.8.2 / libstdc++.a

1、静态库本质是对静态库的源代码形成的所有.o文件进行打包,静态库就是.o文件的集合。
2、静态库会合并自己的代码到可执行程序,所以一但链接静态库形成可执行程序后,可执行程序就不再依赖静态库了,我们使用ldd指令也查不到该可执行程序对静态库的依赖了。
3、库 = 头文件(方法手册)+ 库文件(.o文件的集合),不论是动态库还是静态库都适用。
4、file 指令可以查看一个可执行程序文件的链接类型。

如果我们运行程序时引入的外部文件是以单个.o文件的形式时,是可以直接链接所有目标文件(包括我自己程序的目标文件和外部引入的目标文件)形成可执行文件的。但是如果外部的文件是以静态库的形式引入当前程序时,我们直接链接程序是会报错的,因为要使用一个静态库必须要先找到,所以gcc编译时需要带 -l(库名) 选项,但是这还不够,因为库文件不同于头文件,系统一般不会去当前工作路径找静态库,而是由gcc/g++编译器中的 /usr/bin/ld(加载器)去指定路径如 /lib64->usr/lib64 这样的链接文件中找,所以需要指明到库在当前路径下。/lib64->usr/lib64文件一般在根目录中:

在这里插入图片描述

下面是编译自己库文件的完整指令:

在这里插入图片描述

而C标准库却不用我们指定特定路径就能链接,因为C标准库本身就在编译器会去找的默认路径 /lib64->usr/lib64 下,当我们把自己的库文件拷贝到默认路径下后,就可以不带-L指定路径,只用带-l指定文件名就可以完成链接了。
所以所谓库的安装,就是把库文件拷贝到系统默认路径下。

这里还有一个问题,那既然我们自己的文件和C标准库的文件都在默认路径下了,为什么gcc链接自己的库还需要 -l 指定文件名,而链接C标准库却不用?这是因为gcc本来就是用来编译C语言的,所以gcc默认认识C标准库,不用手动指定。
所以未来我们使用任何第三方库,至少需要带 -l(小写l) 指定文件名。

以前我们讲过带 " " 的头文件是告诉编译器先到当前路径查,若没找到再到系统默认路径中查。 带 < > 的头文件是告诉编译器直接到系统默认路径中查。如果我们就想用 < > 包含当前文件下自己创建的头文件,可以用两种方法:
1、gcc test.c -I. ,用-I(大写i) 选项指定一个头文件搜索路径。
2、将当前路径的头文件拷贝到系统指定头文件路径 /usr/include 路径下。

总结

  • 库 = 头文件 + 库文件。
  • 库使用需要搜索:1. 头文件 (-I (大 i))——预处理阶段选项 2. 库路径 (-L)——链接阶段选项 3. 库是谁 (-l (小 l)) ——链接阶段选项,这三种选项后面跟不跟空格都有效。
  • 库如果不想过多使用上面的选项,就需要将库安装到系统特定路径。
  • 库安装本质是把头文件和库文件拷贝到系统指定的,默认的,编译器能找到的路径下:头文件(/usr/include)和 库文件 (/lib64) 。

二、静态库的生成

ar 是 gnu 归档⼯具,一种指令,类似zip,它专门用来打包形成静态库。
rc 选项表示 (replace and create) ,已存在则替换,不存在则创建。

在这里插入图片描述

上面这段makefile的含义是:
1、自动查找mymath.c和mystdio.c,通过gcc -c编译生成mymath.o和mystdio.o。
2、用ar工具将这两个目标文件打包成静态库libmyc.a。

makefile中的顺序问题:即使libmyc.a的规则写在前面,%.o:%.c写在后面,Make 也能正确找到依赖的编译规则,顺序不会导致逻辑错误。不过从可读性角度,通常会把 “最终目标”(比如libmyc.a)放在前面,依赖的规则(比如.o的编译)放在后面。这样一眼就能看到最终要生成的结果,更符合阅读习惯。

形成库文件后还需要创建一个目录mylibc,把头文件和库文件分别拷贝进目录的include和lib目录中,在把mylibc打包压缩,就可以把压缩包传给别人使用啦:

在这里插入图片描述

下面是mylibc的目录结构:

在这里插入图片描述

总结:给别人提供库就是以特定目录结构组织好的头文件和库文件

三、动态库

动态库的生成

在这里插入图片描述

shared: 表⽰⽣成共享库格式
fPIC:产⽣位置⽆关码(position independent code)

动态库的使用

动态库使用时和静态库有一些区别,当我们把自己形成的动态库链接形成可执行程序时带的 -L 选项是告诉了链接器库文件的路径在哪里,但当我们运行动态库形成的可执行程序时系统会报找不到库文件,这是因为我们没有告诉系统(加载器)库在哪里,因为动态库不像静态库在程序内部,运行可执行程序时动态库不会随着程序加载进内存,而是需要加载器去磁盘里找到动态库把它单独再加载进内存。
但是为什么在运行时不用指定C标准库的路径却能正常运行程序呢?这和其实和链接时一样,不仅链接时链接器会去默认路径lib64中找库文件,运行加载程序时加载器也会到默认路径lib64中找库文件。

这里解决运行找不到库文件报错有两种方法:
1、把库文件拷贝到系统默认路径lib64下,这样链接、运行时就都能找到了。
2、在系统默认路径 /lib64 下建立与当前路径下自己形成的动态库的软链接。

在这里插入图片描述

3、配置 LD_LIBRARY_PATH(加载库路径)环境变量,我们之前学过在命令行配置的环境变量是内存级的,所以只有该次shell会话中有效,如果我们想让该环境变量配置永久有效,就需要更改环境变量配置文件,下面只演示是临时配置,配置配置文件小编就不演示了。

在这里插入图片描述

4、更改系统配置文件,将动态库查找路径全局有效。

在这里插入图片描述

具体步骤是在 /etc/ld.so.conf.d 系统目录下添加一个以.conf为后缀的动态库查找路径的配置文件,上面是该系统文件的示意图。然后在配置文件中写入自己库文件的绝对路径,然后使用 ldconfig 指令,该指令会将 /etc/ld.so.conf.d 系统目录下的文件全部热加载到内存中,这样我们的可执行程序就能依赖libmyc.so库文件了,运行可执行文件时就不会报错了。

在这里插入图片描述

虽然查找动态库我们介绍了4种方法,但是最佳实践是前两种,小编还是推荐大家优先使用前两种。

四、对比动静态库

1、当我们gcc/g++编译链接时不加-static,动静态库同时存在,默认优先使用动态库,进行动态链接。

在这里插入图片描述

2、当我们gcc/g++编译链接时不加-static,只有静态库,只能使用提供的静态库,进行静态链接。
3、当我们gcc/g++编译链接时加-static,-static要求我们必须采用静态链接的方案,也就是说静态库必须存在,若不存在,则会报错。

我们在测试上面原理时会发生一个问题,只要加-static就会报错,这是因为大部分linux系统只给我们安装了C/C++的动态库,没有安装静态库。所以加-static时就会因为找不到C/C++的静态库而报错。所以需要给系统安装静态库,下面是安装指令:

安装 C 语言静态库(如标准库glibc的静态版本):
# CentOS 7
sudo yum install glibc-static
# CentOS 8及以上
sudo dnf install glibc-static安装 C++ 静态库(如标准库libstdc++的静态版本):
# CentOS 7
sudo yum install libstdc++-static
# CentOS 8及以上
sudo dnf install libstdc++-static
Ubuntu:
安装 C 语言静态库(如标准库libc6的静态版本):
sudo apt-get update
sudo apt-get install libc6-dev  # 包含C标准库的静态文件和头文件安装 C++ 静态库(如标准库libstdc++的静态版本):
sudo apt-get update
sudo apt-get install libstdc++6-dev  # 包含C++标准库的静态文件和头文件

五、ELF文件

我们前面提到过,可执行程序也是文件,它们里面存放着程序的代码和数据,但是感觉这些概念都很虚,我们来落实落实,了解一下二进制可执行文件中的代码和数据在内存和磁盘中的组织形式。首先我们来认识一下ELF文件。

1、以 .a .so .o .exe 这些为后缀的二进制文件都是以ELF格式存在磁盘上。(类比音频是mp3格式)
2、最常⻅的节:

  • 代码节(.text):⽤于保存机器指令,是程序的主要执⾏部分。
  • 数据节(.data):保存已初始化的全局变量和局部静态变量。

在这里插入图片描述

查看ELF文件内容

查看ELF Header:
指令: readelf -h 文件名
(ELF头(ELF header) :描述⽂件的主要特性。其位于⽂件的开始位置,它的主要⽬的是定位⽂件的其他部分。)

操作如下:

在这里插入图片描述

下面是ELF header内部结构:

在这里插入图片描述

ELF header 本质就是结构体,ELF header在32 位系统下大小为 52 字节,64 位系统下为 64 字节。

typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;

编译器和操作系统都要认识这个ELF header:
编译器在编译源文件形成可执行程序时编译器会为我们创建ELF header结构体,所以编译器天然认识ELF header。而操作系统未来要将可执行程序加载到内存里,就需要读取可执行程序的ELF header,所以操作系统也认识ELF header。

查看Section Header Table:
指令: readelf -S 文件名
(节头表(Section header table) :包含对节(sections)的描述,例如节的偏移量和大小,节头表中记录节的偏移量和大小可以说明每个节大小不是4kb,否则就不用记录了。)

操作如下:

在这里插入图片描述

查看Program Header Table:
指令: readelf -l 文件名
(列举了所有有效的段(segments)和他们的属性。表⾥记着每个段的开始的位置和位移(offset)、⻓度,毕竟这些段,都是紧密的放在⼆进制⽂件中,需要段表的描述信息,才能把他们每个段分割开。)

这里小编先介绍一下说明是数据段,它和数据节之间有什么关系:
前面我们介绍过了,在编译器角度,一个数据节不一定是4kb,一般小于4kb,具体大小由代码长度和数据大小决定,并且多个数据节可能会有相同的属性。在OS角度,磁盘和内存进行IO的时候,必须是4kb,所以必然就需要将多个数据节合并对齐为4kb,这个合并多个数据节大小为4kb的内容就是数据段,合并的时机是将磁盘数据加载到内存时。

至此我们明白了未来看待ELF文件有两种视角:
1、编译器:以section看待
2、操作系统:以segment看待

这里的Program Header Table就是一张合并方法表。

查看具体的sections信息:

指令:objdump -S 文件名

查看编译后的.o⽬标⽂件:

指令:objdump -d 文件名
(查看反汇编)

ELF形成可执行

step-1:将多份 C/C++ 源代码,翻译成为⽬标 .o ⽂件。
step-2:将多份 .o ⽂件的section进⾏合并,最后合并为一个可执行程序,该过程就是链接。

在这里插入图片描述

ELF可执行文件加载到内存

1、⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进⾏Section合并,形成segment。
2、合并原则:相同属性,⽐如:可读,可写,可执⾏,需要加载时申请空间等。
3、这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到⼀起。
4、很显然,这个合并⼯作在形成ELF的时候,合并⽅式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中。

为什么要将section合并成为segment:
1、Section合并的主要原因是为了减少⻚⾯碎⽚,提⾼内存使⽤效率。如果不进⾏合并,假设⻚⾯⼤⼩为4096字节(内存块基本⼤⼩,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占⽤3个⻚⾯,⽽合并后,它们只需2个⻚⾯。
2、此外,操作系统在加载程序时,会将具有相同属性的section合并成⼀个⼤的segment,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制,方便页表进行权限管理。

总结:section在链接时作⽤,segment在运⾏加载时作⽤。

六、静态链接

⽆论是⾃⼰的.o,
还是静态库中的.o,本质都是把.o⽂件进⾏连接的过程,所以研究静态链接,本质就是研究.o文件是如何链接的。静态链接的行为如下:

  1. 两个.o的代码段合并到了⼀起,并进⾏了统⼀的编址。
  2. 链接的时候,会修改.o中没有确定的函数地址,在合并完成之后,进行call具体地址,完成代码调⽤。这个过程叫做链接时地址重定位。所以我们把.o/.obj文件称为可重定位目标文件。

所以链接其实就是将编译之后的所有⽬标⽂件连同⽤到的⼀些静态库运⾏时库组合,拼装成⼀个独⽴的可执⾏⽂件。当所有模块组合在⼀起之后,链接器会根据我们的.o⽂件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从⽽修正它们的地址。这其实就是静态链接的过程。所以,链接过程中会涉及到对.o中外部符号进⾏地址重定位。

在这里插入图片描述

ELF加载与进程地址空间(虚实地址的转换机制)

首先要明确动静态链接方式影响 “虚拟地址的确定时机”,不影响 “虚实地址的转换机制”,所以不论是动态链接还是静态链接都遵守下面要介绍的虚实地址的转换机制。

一个ELF可执行程序在没有被加载到内存的时候,其实它内部的每行代码和对应数据就已经有地址了,链接重定向可以间接证明这个结论,因为重定向需要拿到函数的地址(函数的本质就是相邻地址的集合)。

当代的计算机对ELF进行编址的时候,都是用一种线性的平坦模式进行编址,就是按照代码和数据出现的次序依次从低到高编址。

上面介绍的这种地址,其实是虚拟地址。在linux系统和平坦模式下,在磁盘/ELF文件中称这种地址为逻辑地址,在内存中,这种地址称为虚拟地址。

所以在磁盘的数据被加载到磁盘之前,磁盘中的数据就已经被统一编址过了,只不过这时的地址是逻辑地址。在磁盘数据加载到内存后,这些数据就会有物理地址,物理地址、逻辑地址都有了,就可以对它们的地址做物理到虚拟的映射,就可以将映射地址填入到页表中。

现在物理、虚拟地址都有了,那么程序加载到内存后从哪里开始执行呢? 这里就要引入之前介绍ELF Header时没有介绍的一个成员:Entry point address,它是当前可执行程序的入口虚拟地址。当程序加载到内存后,就会把Entry point address的值加载到OS的程序计数器EIP寄存器中。

除了EIP外,CPU内还有一个CP3寄存器,它指向当前进程页表的首地址,还有一个CPU内部的硬件组件MMU,它可以查页表完成从虚拟地址到物理地址的映射。有了上面这些,所以MMU就可以当着EIP的虚拟地址,拿着CR3指向的页表,就可以查页表将虚拟地址转换为物理地址、拿着物理地址开始访问物理地址指向的代码和数据了。不仅仅是程序入口虚拟地址是这样转换的,程序中遇到的所有虚拟地址比如程序内部的函数互相调用都是这样转换成物理地址的。所以以后进入CPU的是虚拟地址,出CPU的是物理地址,下面是转换示意图:

在这里插入图片描述

所以虚拟空间技术,需要操作系统(创建内核数据结构)、编译器(平坦模式编址)、CPU硬件(寄存器、MMU)三者协同支持实现。

我们前面还说过,进程创建时要先创建PCB、进程地址空间和页表,那进程地址空间进行划分时各个段的初始值从哪来呢?其实虚拟内存空间的段是从ELF文件中多个section合并成的segment的地址得来的,因为ELF文件中有多个有section合并成的segment,我们可以用指令看一下:

在这里插入图片描述

并且每一个加载进内存的segment都对应一个虚拟内存中的vm_area_struct:

在这里插入图片描述

七、动态链接与动态库加载

进程如何看到动态库

首先小编先说明一点,为什么我们不讲进程如何看到静态库,因为静态库在链接时和可执行程序合并到一起了,进程天然就能看到。

那么动态库呢?首先程序要调用动态库,所以程序运行时动态库也需要加载到内存中,加载的内存中的动态库会提供页表映射到进程地址空间堆栈之间的共享区中,这样进程就能看到动态库了。

一个进程是可能同时映射多个库到共享区的。

在这里插入图片描述

进程间如何共享库的

1、当多个进程都要访问同一个动态库时,因为该动态库只会加载一次,所以每一个进程都会把库映射到自己的进程地址空间中。
2、所以动态库也被称为共享库,这样相比静态库能有效节省内存空间。
3、动态库的本质:通过地址空间映射,对公共代码进行去重。

在这里插入图片描述

动态链接特点

动态链接其实远⽐静态链接要常⽤得多,因为静态链接最⼤的问题在于⽣成的⽂件体积⼤,并且相当耗费内存资源,这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成⼀个独⽴的动态链接库,等到程序运⾏的时候再将它们加载到内存,这样不但可以节省空间,因为同⼀个模块在内存中只需要保留⼀份副本,可以被不同的进程所共享。

_start

那么动态链接到底是如何⼯作的?
⾸先要交代⼀个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使⽤情况为它们动态分配⼀段内存。
当动态库被加载到内存以后,⼀旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。

首先明确_start 是 C 运行时库(如 glibc)提供的程序初始入口函数,它会调用动态链接器(ld-linux-x86-64.so)解析并加载程序依赖的动态库, ld-linux-x86-64.so库如下图所示:

在这里插入图片描述

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

  1. 设置堆栈:为程序创建⼀个初始的堆栈环境。
  2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
  3. 动态链接:这是关键的⼀步, _start 函数会调⽤动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调⽤和变量访问能够正确地映射到动态库中的实际地址。

程序如何和库映射起来

进程如何把动态库映射到自己的进程地址空间里:
首先要把动态库加载到内存中,其中动态库也是文件,所以我们要先打开库文件,因为打开文件后才能把文件加载到内存中。打开文件后OS会为该文件在内存中创建一个struct file,struct file初始化后会间接关联文件的inode。然后OS会创建一个vm_area_struct,并且vm_area_struct会关联struct file。当我们拿到inode后就能拿到inode中的文件数据块信息,然后就能将文件的内容读取到物理内存中(这里把文件内容读取到物理内存中的空间就是文件内核缓冲区),这时动态库的物理地址就有了。接着OS还会在进程地址空间的共享区申请一段连续的空间来和物理地址做映射,然后我们就能拿到库在进程地址空间中的起始虚拟地址。

(提炼:先通过文件系统将库文件加载到内存里,加载到内存中后就能拿到该库对应的inode文件,然后就能通过inode拿到文件的数据块,就能把文件数据加载到内存里,这时物理地址就有了,然后再进程地址空间的共享区建立一段连续的虚拟空间,再将物理与虚拟地址建立映射)

下图是示意图,具体流程分4步走:

在这里插入图片描述

程序如何进行库函数调用

1、库已经被我们映射到了当前进程的地址空间中。
2、库的虚拟起始地址我们也已经知道了,物理地址映射到虚拟地址时确定。
3、库中每⼀个⽅法的偏移量地址我们也知道。因为在编译形成动态库时库中每个方法的偏移量就已经有了。
4、访问库中任意⽅法,只需要知道库的起始虚拟地址+⽅法偏移量即可定位库中的⽅法。该定位发生时机是在程序运行时,所以该过程被称为运行时地址重定向
5、结论1:整个调⽤过程,是从代码区跳转到共享区,调⽤完毕在返回到代码区,整个过程完全在进程地址空间中进⾏的。
6、结论2:动态库被映射到进程地址空间(一般是共享区)的任意位置,进程都能调用。

在这里插入图片描述

但是这里还有一个问题,如上图所示,运行时地址重定向本质是将代码区原本的库文件名修改为库的起始虚拟地址,但是代码区不是只读的吗?为什么可以修改呢?这里就要引入一个概念:全局偏移量表,下面我们来详细介绍。

全局偏移量表GOT

动态链接采⽤的做法是在进程地址空间的数据段 (可执⾏程序或者库⾃⼰)中专⻔预留⼀⽚区域⽤来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每⼀项都是本运⾏模块要引⽤的⼀个全局变量或函数的地址。因为.data区域是可读写的,所以可以⽀持动态进⾏修改。

在这里插入图片描述

1、由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独⽴的GOT表,所以进程间不能共享GOT表。
2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利⽤CPU的相对寻址来找到GOT表。
3. 在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会被修改为真正的地址。
4. 这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT。

库间依赖

不仅仅有可执⾏程序调⽤库库也会调⽤其他库!库之间是有依赖的,如何做到库和库之间互相调⽤也是与地址⽆关的呢?库中也有.GOT,和可执⾏⼀样!这也就是为什么⼤家都是ELF的格式!

由于动态链接在程序加载的时候需要对⼤量函数进⾏重定位,这⼀步显然是⾮常耗时的。为了进⼀步降低开销,我们的操作系统还做了⼀些其他的优化,⽐如延迟绑定,或者也叫PLT(过程连接表(Procedure Linkage Table))。与其在程序⼀开始就对所有函数进⾏重定位,不如将这个过程推迟到函数第⼀次被调⽤的时候,因为绝⼤多数动态库中的函数可能在程序运⾏期间⼀次都不会被使⽤到。
思路是:GOT中的跳转地址默认会指向⼀段辅助代码,它也被叫做桩代码/stup。在我们第⼀次调⽤函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调⽤函数的时候,就会直接跳转到动态库中真正的函数实现。

总结动静态链接

  • 静态链接的出现,提⾼了程序的模块化⽔平。对于⼀个⼤的项⽬,不同的⼈可以独⽴地测试和开发⾃⼰的模块。通过静态链接,⽣成最终的可执⾏⽂件。
  • 我们知道静态链接会将编译产⽣的所有⽬标⽂件,和⽤到的各种库合并成⼀个独⽴的可执⾏⽂件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。
  • ⽽动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是⽆论加载到什么地⽅,都要映射到进程对应的地址空间,然后通过.GOT⽅式进⾏调⽤(运⾏重定位,也叫做动态地址重定位)。

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

http://www.dtcms.com/a/545523.html

相关文章:

  • 【开题答辩全过程】以 儿童疫苗接种提醒系统的设计与实现为例,包含答辩的问题和答案
  • 【linux】基础开发工具(2)vim
  • 宁波找网站建设企业如何使用网络营销策略
  • 关于进一步做好网络安全等级保护有关工作的问题释疑-【二级以上系统重新备案】、【备案证明有效期三年】
  • Flink Keyed State 详解之三
  • LangChain4j学习3:模型参数
  • 驻马店做网站哪家好常州微网站建设
  • 深圳网站建设报价网站开发客户来源
  • 仓颉开发鸿蒙应用:深入理解组件生命周期的设计哲学与实践
  • Java 启动脚本-简介版
  • CFX Manager下载安装教程
  • 基于STM32HAL库判断传感器数据和系统定时器外部中断
  • 仓颉语言中的成员变量与方法:深入剖析与工程实践
  • JavaScript是如何执行的——V8引擎的执行
  • GEO:AI 时代流量新入口,四川嗨它科技如何树立行业标杆? (2025年10月最新版)
  • 【牛客刷题-剑指Offer】BM24 二叉树的中序遍历:左根右的奇妙之旅(递归+迭代双解法详解)
  • 宝山网站建设哪家好平面设计免费模板网站
  • 腾讯云 怎样建设网站免费自助建站工具
  • elasticsearch中文分词器插件下载
  • 【开题答辩全过程】以 叮叮网上图书销售管理系统为例,包含答辩的问题和答案
  • 2025—2028年教育部面47项白名单赛事汇总表(正式版)
  • IPython.display 显示网页
  • Excel怎么根据身份证号码来计算年龄?
  • 江阴网站网站建设免费的舆情网站
  • 服务间的通信之gRPC
  • php做电商网站开题报告wordpress输密码访问
  • Mybatis中# 和 $的区别
  • IDEA开发常用快捷键总结
  • SAP HANA数据库HA双机架构概念及运维
  • Blender 4K渲染背后的技术挑战