Linux内存管理--系列文章伍——动态链接

接上文,静态链接的过程写完了。本篇开始写动态链接。

一、为什么要动态链接

上文在文章最后写出了静态链接的缺点,正是因为这些缺点使得人们要使用一种更好的方式来组织程序的各种模块。为解决静态链接的缺点提出了动态链接。

二、动态链接要做什么

静态链接的缺点是因为其在编译过程时,将各目标文件进行链接,形成一个完整的文件。这样就会导致增加可执行文件大小浪费资源更新困难等问题。那要是在程序开始运行时再将文件链接起来,这些问题就可以解决。所以动态链接的基本思想就是将链接这个过程移到程序装载时进行。

2.1 地址和空间的分配

经过静态链接的程序开始运行时,整个进程只有一个文件会被映射进去,上一篇文章已经讲述了是怎么对可执行文件中每个符号进行地址分配的。但动态链接的程序在运行时,映射进内存的会是多个目标文件和所使用的依赖库。这个时候地址空间的分配是由装载器完成的。装载器会根据当前地址、空间的情况,动态分配地址给各文件。
如果加载进内存的程序中有使用绝对地址的情况,且各绝对地址发生了地址冲突,那就会产生程序的错乱。当然程序员可以手动指定各模块的地址,但实际生产中多个模块被多个程序使用、相同的模块被分到同一个地址等情况会时常发生,这些情况下手动管理一般是不可能的。但在一些系统上确实使用的是这样的办法,这种做法被叫做静态共享库。由于静态共享库地址有关性,人们提出了动态库动态库在编译时不固定自己的虚拟地址,转而到装载时进行定位,该技术成为装载时重定位
装载时重定位,在编译时不能确定各符号的位置,在装载时又会进行符号的重定位,导致每一个进程都要将动态库的全部指令也加载进自己的进程内存中。那有没有办法让程序只加载共享库的数据段,而指令代码被各进程共享。为解决动态库的指令部分无法在多个进程中共享,我们可以将动态库的指令代码再进行重定位,这就是地址无关码技术。

2.1.1 装载器(Loader)

装载器是操作系统中负责将可执行文件和动态链接库(DLL或共享库)加载到内存中,并为程序准备执行环境的关键组件。

工作原理通常可以分为以下几个步骤
读取可执行文件: 装载器会读取可执行文件,获取程序的代码、数据、符号表等信息。
分配内存空间: 装载器会为程序分配必要的内存空间,包括代码区、数据区、堆区和栈区等。这些段的大小和位置由可执行文件的头部信息决定。
加载程序代码: 装载器会将程序代码加载到内存中的代码区中。
加载程序数据: 装载器会将程序数据加载到内存中的数据区中。
解析符号表: 装载器会解析符号表,确定程序中未定义的符号所对应的地址。
链接外部库: 如果程序需要使用外部库,装载器会将外部库加载到内存中,并链接到程序中。
设置程序入口地址: 装载器会设置程序的入口地址,指示处理器从哪里开始执行程序。
启动程序执行: 装载器会启动程序执行,将控制权转移给程序。

2.1.2 静态共享库(Static Shared Library)

静态共享库不是静态库,因为静态库静态共享库是两种不同的库类型,它们在使用和链接方式上有明显的区别。静态共享库是将程序的各部分都交给操作系统来处理,由操作系统在某个特定的地址划分出一些地址块,并根据它们的大小留出空间。
缺点
地址有关:库的代码不可以在内存中的任何位置运行,而修改代码后需要程序重新链接。
升级问题:因为位置有关,导致了被分配到的虚拟地址固定,而且空间有限。程序在更新时不能增加的过长,否则会超出自己的空间。

2.1.3 动态库(Dynamic Library)

动态库(Dynamic Library)是共享库(Shared Library)的一种特殊类型。它通常使用动态链接的方式加载,并且具有以下特点

位置无关: 动态库的代码可以在内存中的任何位置运行,而无需修改代码本身。
延迟加载: 动态库的代码只有在需要时才会被加载到内存中。
版本控制: 动态库可以有多个版本,程序可以根据需要选择加载不同的版本。

优点:
提高代码重用性: 相同的代码可以被多个程序共享使用,减少了代码冗余。
减小程序大小: 可执行文件无需包含所有共享的代码,从而减小了文件大小。
提高运行效率: 由于动态库的代码只有在需要时才会被加载到内存中,因此可以提高内存利用率和运行效率。
支持版本控制: 动态库可以有多个版本,程序可以根据需要选择加载不同的版本,从而提高程序的兼容性和灵活性。
缺点:
增加程序复杂性: 动态链接的程序运行过程更加复杂,因为需要在运行时加载和链接共享库。
依赖外部库: 动态链接的程序依赖于外部库,如果库丢失或损坏,程序可能无法正常运行。
潜在的安全风险: 动态链接的程序可能存在安全风险,因为攻击者可以通过恶意库来攻击程序。

2.1.4 装载时重定位(Load-time relocation)

装载时重定位(Load-time relocation)是指在动态库加载到内存并链接到进程地址空间时,系统对其中的符号引用进行修正的过程。这个过程是在动态链接器(或加载器)执行的,它负责加载和链接共享库到进程的地址空间。
过程
加载库文件:操作系统的加载器首先将共享库加载到进程的地址空间中。这个过程通常包括将库文件的代码段、数据段等映射到进程的虚拟内存空间中。
解析符号引用:加载器会解析共享库中的符号引用,找到这些符号在库文件中的具体地址。
修正地址引用:一旦符号引用被解析,加载器会遍历库中的重定位表,根据其中的信息将所有的符号引用修正为正确的地址。这个过程被称为装载时重定位。
更新全局偏移表(GOT)和程序链接表(PLT):在装载时,系统会更新进程的GOT和PLT,以确保所有的外部符号引用都能正确链接到其实际地址。

对于动态库来讲装载时重定位是确保程序正确链接和执行的重要步骤,主要有以下原因
地址无关码(PIC):动态库通常使用位置无关代码编译,这意味着它们的地址不是在编译时就确定的,而是在运行时才确定。因此,需要在加载时将这些地址修正为正确的值。
地址空间独立性:不同进程的地址空间可能不同,所以共享库的代码和数据段需要在加载时根据实际地址空间进行调整,以确保正确性。
符号解析:动态库中的符号可能会被多个进程引用,而这些符号的实际地址在不同进程中可能不同,所以需要在加载时对这些符号引用进行解析和修正。

目的
解析符号引用: 当程序被编译时,它可能包含对其他共享对象或库中定义的符号的引用。装载时重定位确保在程序加载到内存时这些引用得到正确解析。
处理地址有关码(PDC): 位置相关代码(PDC)是包含对绝对内存地址引用的代码。装载时重定位可以调整这些引用,使代码能够在内存中的任何位置正确运行。

2.1.5 地址无关码(Position Independent Code,PIC)

是指不包含任何绝对内存地址引用的代码。这意味着该代码可以在内存中的任何位置运行,而无需修改代码本身。
特点和优点
可在任意地址执行:PIC不依赖于固定的内存地址,因此可以在内存的任意位置加载和执行,而不需要进行重定位。
适用于共享库:共享库的地址在每个进程中都可能不同,因此需要使用PIC以保证共享库在每个进程中的可执行性。
防止地址冲突:使用PIC可以避免不同库之间以及库与主程序之间的地址冲突,从而提高程序的稳定性和安全性。
方便内存随机化(ASLR):操作系统的内存随机化技术可以随机化动态库的加载地址,以增加系统的安全性。使用PIC可以使得动态库更容易适应ASLR。

2.2 动态链接怎样使用符号地址

我们可以按模块内部和外部、符号类型是数据或函数来大致分类,来分析在动态链接中各符号是怎么被使用的。

2.2.1 模块内部的函数

模块内部的函数,在编译时,各函数的相对位置已经被固定。函数的调用是相对地址寻址。

函数相对地址寻址
在相对地址寻址方式中,编译器在编译时并不直接将函数调用转换为目标函数的绝对地址,而是生成一个相对于当前位置的偏移量。这个偏移量会在运行时与当前代码段的起始地址相加,得到目标函数的绝对地址,然后跳转到该地址执行函数。
相对地址寻址的优点是程序更加灵活,因为偏移量是相对于当前位置的,所以即使目标函数的地址发生变化,也不会影响到程序的执行。但缺点是在运行时需要进行地址计算,稍微增加了一点额外的开销。

2.2.2 模块内部的数据

很明显函数和数据没有什么不一样,模块内部的数据也是相对地址寻址。每个模块的代码段和数据段之间的位置是固定,而且数据段内数据的位置也是相对固定的。也就是说在这个模块里所有东西的位置都是相对固定的,这样就可以直接使用当前位置(PC寄存器)加上相对位置就可以随意访问任何一个数据或函数。
数据相对地址寻址
数据的相对寻址是指在程序执行过程中,通过相对于当前指令的位置的偏移量来寻址数据。这种寻址方式通常用于访问局部变量、栈上的数据以及程序内部的数据结构等。相对寻址的偏移量是相对于当前指令位置计算的,因此数据的地址相对于指令的位置是固定的,不受程序加载地址的影响。
相对寻址的优点是程序更加灵活,因为数据的地址是相对于当前位置计算的,所以即使程序的加载地址发生变化,也不会影响到数据的访问。相对寻址通常会通过基址寄存器(Base Register)或栈指针(Stack Pointer)来实现。

2.2.3 模块外部的数据

动态链接时,模块间数据的地址需要等到装载时才能决定。这个时候上文提到的 地址有关码就发挥作用了。全局变量的地址是有模块装载地址有关,这个时候会在数据段里建立一个指针数组,来指向这些全局变量,这个数组叫做全局偏移表。当代码需要使用该全局变量时,就会去查询全局偏移表,通过相应的项进行间接引用。

全局偏移表(Global Offset Table,GOT)
作用
全局变量的访问:在动态链接过程中,共享库中的全局变量的地址不是在编译时就确定的,而是在程序运行时动态分配的。GOT存储了这些全局变量的地址,程序在运行时通过GOT来访问这些全局变量。
函数调用:对于共享库中的函数调用,同样是在运行时动态解析的。GOT中存储了这些函数的地址,程序在运行时通过GOT来调用这些函数。
结构
GOT是一个全局的数据结构,它通常是一个由系统在程序运行时自动创建和维护的表格。每个进程都有自己的GOT,它存储了当前进程所链接的所有共享库中的全局变量和函数地址。
GOT中的每个条目对应一个全局变量或函数的地址。当程序需要访问某个全局变量或调用某个函数时,它会先从GOT中获取相应的地址,然后再进行访问或调用。
更新
GOT的内容通常在程序启动时由动态链接器完成初始化,然后在程序运行过程中根据需要进行更新。主要有以下两种情况:
延迟绑定:在某些系统中,共享库中的全局变量和函数的地址可能在第一次访问时才被解析并存储到GOT中,这样的机制称为延迟绑定(Lazy Binding)。
重定位:如果共享库的地址发生变化(如共享库被加载到不同的地址),那么GOT中存储的地址也需要进行更新,以确保程序能够正确地访问全局变量和调用函数。

2.2.3 模块外部的函数

既然模块外部的数据可以通过全局偏移表,去寻找相应的项进行间接引用。那模块外部的函数应该也可以这样做。

三、动态链接的优化

上面动态链接已经完成,并介绍了它特点。可又因为它的特点,导致了动态链接一定会牺牲一部分性能。一般测算,静态链接要比动态链接快1%-5%
动态链接比静态链接慢主要有以下几个原因
延迟加载: 动态链接需要在程序运行时加载共享库,这需要额外的加载时间。而静态链接的库代码已经直接链接到程序中,因此无需加载。
符号解析: 动态链接需要解析共享库中的符号,以找到所需的函数和变量。这需要额外的解析时间。而静态链接的符号解析工作是在编译时完成的。
重定位: 动态链接的共享库可能被加载到不同的内存地址,因此需要对共享库中的代码和数据进行重定位。这需要额外的重定位时间。而静态链接的库代码已经直接链接到程序中,因此无需重定位。
GOT/PLT: 动态链接需要使用全局偏移表 (GOT) 和过程链接表 (PLT) 来访问共享库中的函数和变量。这需要额外的间接寻址和跳转。而静态链接的函数和变量可以直接通过地址访问,无需间接寻址和跳转。
碎片化: 动态链接可能会导致内存碎片化,因为共享库可能被加载到不同的内存区域。这可能会降低内存的利用效率。而静态链接的库代码已经直接链接到程序中,因此不会导致内存碎片化。

常见的动态链接优化技术
延迟绑定(Lazy Binding):延迟绑定是一种优化技术,它推迟了共享库中函数和全局变量的解析和地址绑定操作,直到第一次被调用或访问时才执行。这样可以减少程序启动时间和内存占用,并且可以减少动态链接器的工作量。
符号版本(Symbol Versioning):符号版本是一种动态链接优化技术,它允许在共享库中使用不同版本的同名函数或全局变量。通过为每个版本的符号定义不同的版本号,可以使得程序在链接时自动选择最适合的符号版本,从而提高了程序的兼容性和可维护性。
共享库预加载(Preloading):共享库预加载是一种通过预先加载常用的共享库来减少程序启动时间和库加载时间的技术。通过将常用的共享库预先加载到内存中,可以减少程序启动时动态链接器的工作量,加快程序的启动速度。
共享库缓存(Caching):共享库缓存是一种通过缓存已经加载的共享库来减少程序启动时间和库加载时间的技术。通过将已经加载的共享库缓存到内存中,可以减少后续程序启动时动态链接器的工作量,并且可以避免重复加载相同的共享库,从而提高了程序的启动速度。
共享库压缩(Compression):共享库压缩是一种通过压缩共享库文件来减小文件大小和加快加载速度的技术。通过使用压缩算法对共享库文件进行压缩,可以减小共享库文件的大小,并且可以减少网络传输和磁盘IO的开销,从而提高了程序的加载速度。

四、动态链接的条件

4.1 动态链接器(Dynamic Linker)

动态链接器负责在程序运行时将程序与共享库进行链接,解析程序的符号表,并处理共享库的加载和符号解析等操作。工作在程序执行时,当程序需要调用共享库中的函数或使用共享库中的全局变量时才会介入。主要负责解析程序的符号表、加载共享库、进行符号解析和重定位等操作,以实现动态链接和共享库的使用。
动态链接器之前,没有工具可以进行动态链接,所以动态链接器不能依赖任何其他的共享对象、和使用全局或静态变量。这种启动方式成为自举
过程
静态链接器阶段:首先,动态链接器需要通过静态链接的方式将自身的代码和依赖的库链接成一个可执行文件。
初始动态链接:然后,操作系统会使用操作系统自带的静态链接的动态链接器来加载这个可执行文件,并将其链接到操作系统提供的动态链接器的运行时库中。这个过程中,动态链接器会使用到操作系统提供的一些基本功能,比如文件操作、内存管理等。
动态链接:一旦自举的动态链接器成功加载并链接到操作系统提供的动态链接器运行时库中,它就可以开始工作了。它可以被用来加载其他的动态链接库,并执行程序的动态链接过程。

重要性
启用动态链接: 成功自举对于动态链接器履行其在启用动态链接中的作用至关重要。它允许动态链接器独立运行并有效地管理共享库。
功能封装: 自举证明了动态链接器即使在任何其他程序或共享库加载之前也能独立运行的能力。
鲁棒性和灵活性: 自举过程突显了动态链接器的鲁棒性和灵活性,因为它可以在受限条件下运行并为各种程序和共享库场景做好准备。

本篇大致讲述了动态链接的情况,下一篇文章将会对程序装载的情况进行阐述。

  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值