程序员的自我修养4.1 动态链接的过程和延迟绑定

2021/9/15 1:05:34

本文主要是介绍程序员的自我修养4.1 动态链接的过程和延迟绑定,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1 动态链接的过程

注意 : 程序员的自我修养的动态链接部分很多地方不容易理解,我读完之后发现,先从宏观角度,了解整个动态链接的过程,之后再了解动态链接的相关结构,各个段的具体作用反而效果更好,所以,本文先介绍动态链接的过程,其中可能会提到一些段比如.dynamic等等,先把疑问记下,之后的文章会说明的,先了解整体的执行过程为主

1.1 进程将控制权交给动态链接器

1.2 动态链接器进行自举

  • 自举 : 动态链接器实现的功能是将主模块和共享对象链接在一起,链接的过程中对共享对象进行重定位,之后就可以调用共享对象的函数和数据(不论是静态链接还是动态链接,都会进行重定位),但是动态链接器怎么办,谁对动态链接器进行重定位呢?(如果有可执行文件需要用到动态链接器,那么动态链接器就会映射到进程的虚拟地址空间,而且可能有多个进程需要用到这个动态链接器,所以动态链接器也是共享文件,映射到虚拟空间后,动态链接器也需要对数据段的内容重定位)所以,动态链接器有自己的特点
    1. 动态链接器本身不可以依赖于其他任何共享对象,这一点,我们可以人为的控制,编写代码的时候刻意的不适用任何系统库,运行库
    2. 动态链接器本身的所需要的全局和静态变量的重定位工作由它本身完成,这个地方就需要用一些非常精妙有创意的代码了
    • 这种具有一定限制条件的启动代码就称之为自举
  • 自举的过程 :
    1. 动态链接器入口地址就是自举代码的入口地址,当操作系统把控制权交给动态链接器的时候,就开始执行自举代码
    2. 自举代码首先找到.got节,而.got的第一个入口(指针)保存的就是.dynamic节的地址
    3. 找到.dynamic节,就知道了动态链接字符串表,动态链接符号表,动态链接重定位表的地址
    4. 有了第三步得到的三个表,我们根据重定位表找到符号对应的重定位入口地址,然后在查找符号表,把符号对应的地址填上,就完成了一个符号的重定位,我们就可以对动态链接器本身进行重定位
    5. 重定位完成之后,动态链接器就可以使用自己的函数和全局变量

1.3 装载共享对象

  1. 动态链接器完成自举以后,就将动态链接器的符号和可执行文件的符号就合并起来,构造出全局符号表
  2. 之后动态链接器查找可执行文件的.dynamic段,该段中有一种类型是DT_NEEDED,指出的就是该执行文件依赖的共享对象在文件系统内的地址,然后动态链接器就将全部的共享对象绘成一个依赖图(这里要注意,可执行文件依赖共享对象,而共享对象也可以依赖别的共享对象),然后对这个依赖图进行广度优先遍历(一定要注意是广度优先,否则后面的符号冲突你可能会思考错误),将每个共享对象的符号表汇总到全局符号表里面(这里还会涉及到一个符号优先级的问题,就是有可能多个共享对象存在符号冲突,一般我们会采用装载序列,就是先装载的共享对象的符号保留,后装载的共享对象的冲突符号舍弃)

1.4 重定位和初始化

  1. 上面的步骤完成之后,我们就得到了最重要的全局符号表
  2. 之后,链接器遍历共享对象的重定位表,找到需要重定位的地方,然后根据全局符号表进行重定位,这个过程,也要将got和plt的指针数据进行赋值,指向重定位后的位置
  3. 重定位结束之后,如果共享对象有.init段,那么动态链接器会执行.init中的代码,用于实现共享对象特有的初始化过程,比如c++中的构造函数,相对应的,进程退出的时候会执行共享对象finit中的代码,比如析构(如果进程的可执行文件也有.init,那么动态链接器不会执行它,可执行文件的init和finit由程序初始化部分的代码实现)
  4. 完成重定位和初始化之后,动态链接器将控制权交给程序的入口开始执行

2 动态链接中的一些问题

2.1 困扰 : 固定装载地址的困扰

  • 可执行文件在进程虚拟空间的地址是很容易确定的,一般都是从进程的固定空闲地址开始,比如linux下就是0x08040000,因为可执行文件一般都是进程执行的第一个文件,那么问题来了,静态链接中,我们提前把文件链接好,成为一个大的可执行文件,然后装入进程虚拟内存空间,但是在动态链接中,我们分成了可执行文件和多个共享对象,那么问题来了,可执行文件的地址可以直接从一个确定的位置开始,那么共享对象呢?我们总需要确定好共享对象的地址,因为可执行文件存在对于共享对象的绝对引用,不知道共享对象的地址,那么可执行文件中的绝对引用也就无法变成最终的地址
  • 在早期,开发商开发共享对象的时候,会指定该共享对象在进程虚拟空间中占据的地址,那么如果一个进程需要的两个模块,被两个开发商巧合的分配到同一个地址空间呢?这个时候就会出错,该进程不能同时使用这两个模块
  • 这就是模块固定地址装载的问题,为了解决这个弊端,我们提出了一个想法,就是让共享对象可以灵活的在任何地址装载,换句话说,共享对象编译的时候不可以确定自己在进程虚拟空间中的地址,这就涉及到了装载时重定位

2.2 解决 : 装载时重定位

  • 当程序被装载的时候,动态链接器会将可执行文件需要的共享对象都装入到进程,动态分配的虚拟空间中(换言之,不用你开发商开发共享对象的时候就指定好地址,而是将这个工作交给动态链接器),之后将可执行文件中的未决议的符号绑定到共享对象中,进行重定位

2.3 依然存在的问题 : 共享对象的指令的共享问题

  • 我们使用动态链接,最主要的目的是节省内存空间,但是如果我们使用了装载时重定位,就会出现一个问题,其实,我们之前使用固定装载地址是由原因的,就是通过固定好共享对象在进程虚拟空间的地址,共享对象中的某些和地址有关的指令也就固定了,不会发生改变,但是使用了装载时重定位,在物理内存中的一个共享对象,可以映射到多个进程虚拟空间的不同位置上,这就导致我们的每个共享对象的指令都发生了改变
  • 共享对象也是elf文件,那么装载的时候是以segment为单位,一般分成代码段和数据段,数据段权限是RW,说明每个进程都可能修改数据段的内容,因此,即使采用了动态链接,我们内存中依然会存在多个数据段的副本,这是无法改变的,但是采用动态链接,可以使得内存中指令只有一份,所有进程共享这个指令段,这也是动态链接节省内存空间的精髓,但是采用装载时链接,我们的指令无法共享,那么我们的指令也需要存在多个副本,这不是做了无用功吗?

2.4 优化 : PIC地址无关代码技术

  • 其实,并不是所有的指令都会随着地址的改变而改变,如果搞一刀切,是非常可惜的,所以,地址无关技术,就是找到共享对象中,那些会随着地址的改变而发生改变的指令,将他们和数据段放在一起,而不是代码段,代码段中的数据依然只存在一份就可以,实现共享,而包含在数据段中的指令随数据段会有多个副本,这样子就依然可以实现节省内存空间的目的
  • 共享对象中指令的地址引用方式一共四种
    1. 模块内部的函数调用,跳转
    2. 模块内部的数据访问,比如定义在模块中的全局变量和静态变量
    3. 模块外部的函数调用,跳转
    4. 模块外部的数据访问,比如定义在其他模块的全局变量
  • 针对3,4,我们一般采用的方法是在数据segment建立一个数组指针,一般称之为全局偏移表(Global Offset Table),当共享对象装载好,确定好地址之后,指针会指向对应的模块外部的函数或数据

3 优化 : 延迟绑定

  • 开头说过,静态链接的时候,程序可以直接运行可执行文件并产生结果,但是动态链接不可以,因为进程运行的时候,要首先把控制权交给动态链接器,完成链接之后,控制权还给进程,进程继续执行,所以很明显,动态链接的速度会比静态链接慢一些,主要就慢在了动态链接会存在一个启动时间,因此,我们要对动态链接的速度进行优化
  • 优化的主要手段就是延迟绑定,在动态链接中,我们的可执行文件会需要多个模块,但是,共享对象的有些函数,可能可执行文件执行完都不会用到,比如异常处理函数,或者一些偶尔需要用到的模块,也就是说,共享对象的各个函数,使用的频率是不平均的
  • 所以我们采用延迟绑定,延迟绑定最核心的思想在于,函数第一次用到的时候,再进行绑定,没有则不绑定,所以,可以大大加快采用动态链接的进程的启动时间


这篇关于程序员的自我修养4.1 动态链接的过程和延迟绑定的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程