动态链接

动态链接

    • 一、为什么要动态链接
      • 1.1 静态链接
      • 1.2 动态链接
    • 二、-shared、-fPIC
      • 2.1 -shared:装载时重定位
      • 2.2 -fPIC:地址无关代码
    • 三、GOT:全局偏移表
      • 3.1 .got、.got.plt
      • 3.2 .got.plt 前三项
    • 四、延迟绑定(PLT)
      • 4.1 如何实现延迟绑定

一、为什么要动态链接

1.1 静态链接

内存和磁盘空间浪费严重
每个静态链接库,都存在相同的公共库副本,浪费内存空间
程序开发和发布
每次静态库更新,整个源程序都要和静态库重新进行链接

1.2 动态链接

程序的模块相互分隔开,形成独立文件
不对组成程序的目标文件进行链接,等到程序运行时再进行链接
所有需要依赖的目标文件全部加载到内存后,如果依赖关系满足,系统才开始进行链接工作(包括:符号解析、地址重定位等操作),完成操作后程序运行前,系统开始把控制权交给程序入口处,然后程序开始运行
因为不是提前链接目标文件,加载后的目标文件在内存中最多存在一份
可扩展和兼容性

二、-shared、-fPIC

2.1 -shared:装载时重定位

使用 -shared 参数,生成一个装载时重定位的共享对象

gcc -shared xxx.c -o xxx.so
  • 装载共享对象时,如何确定进程虚拟地址空间中的位置
    共享对象在链接时不对绝对地址引用进行重定位,而推迟到装载时(程序运行时?数据段是装载时重定位,而代码段是程序运行时再进行链接,即程序运行前才会开始符号解析、地址重定位等操作)完成,而一旦模块装载地址(目标地址)确定,系统就对程序中所有的绝对地址引用进行重定位

  • 使用装载时重定位可以解决数据段中绝对地址引用的问题
    对于数据段,每个进程都有一份独立的副本,不用担心被进程改变。若共享对象数据段有绝对地址引用,那么编译器和链接器会产生一个重定位表,表中包含相应的重定位入口。当动态链接器装载共享对象时,发现重定位入口时,会对该共享对象进行重定位

  • 代码段是否可用使用装载时重定位
    可以,但是如果代码不是地址无关的,就不能被多个进程共享,就没有了节省内存的优点,好处是装载时重定位的共享对象的运行速度比使用地址无关代码的共享对象快,因为在地址无关代码中每次访问全局变量(数据和函数)时都需要一次计算当前地址以及间接地址寻址的过程

-shared:产生共享对象
现在解决了装载时重定位的问题,但是指令部分还是无法在多个进程之间共享(目前是各进程各有一份指令,这样无法节省内存),这样并没有解决静态链接空间浪费的问题(下面的 “## 地址无关代码” 可以解决)

2.2 -fPIC:地址无关代码

地址无关代码:把指令中需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分可以保持不变,数据部分可以在每个进程中拥有一个副本

三、GOT:全局偏移表

3.1 .got、.got.plt

ELF 把 GOT 拆成两个表

  • .got
    用来保存全局变量引用的地址
  • .got.plt
    用来保存函数引用的地址

3.2 .got.plt 前三项

GOT[0]:当前 ELF 动态段 .dynamic(该段描述了本模块动态链接相关的信息)的装载地址
GOT[1]:保存了被调用函数所在模块的 ID
GOT[2]:函数 _dl_runtime_resolver() 的地址(在动态链接器中负责完成地址绑定工作)
动态链接器在加载完 ELF 之后,会将上面三项地址写到 GOT 表的前三项

其余项:存储符号(变量或函数)的地址,该地址的值由 Resolver 动态填写。当调用者第一次访问这些符号时,会触发 interpreter 的 Resolver 函数的调用,去计算符号的地址,然后填回 GOT 对应项中(符号地址的计算方法依赖于 ELF 文件中重定位和符号表中的一些信息)

四、延迟绑定(PLT)

动态链接把链接过程从推迟到了程序装载时,程序每次在被装载时,都要进行重新链接,导致动态链接在性能上会有一些损失

  • 程序装载后,动态链接器需要进行一次链接工作
  • 对全局和静态的数据访问,需要进行复杂的 GOT 定位,然后间接寻址

为了优化,所以推迟,暂时用不到函数不需要马上完成链接过程,等到函数第一次被用到时才进行绑定(符号查找、重定位等操作),没有用到则不进行绑定

模块间的函数调用没有进行绑定,而是等到需要时才会由动态链接器负责绑定,大大优化了程序的启动速度

4.1 如何实现延迟绑定

调用外部模块的函数,通常做法是通过 GOT 中相应的项进行跳转,PLT 为了实现延迟绑定,在这个过程中增加了一层间接跳转

bar@plt 实现:

bar@plt:
jmp *(bar@GOT) // 初始阶段存放的是下一条指令的地址,下面的流程走完之后,这里存放的就是函数 bar() 的地址
// 计算函数 bar() 的地址
push n // 函数的编号,符号 bar 在重定位表 .rel.plt 中的下标
push moduleID // 压入模块 ID
jump _dl_runtime_resolve // 跳转到函数 _dl_runtime_resolve(),根据模块 ID 和函数编号,完成地址绑定,然后将函数 bar() 的地址回填到 bar@GPT 中

在一个模块中,模块 ID 是相同的,且所要运行的函数的绑定地址也是相同的,因此,这部分可以抽成一个函数节省空间

PLT0:
push *(GOT + 4) // 保存模块 ID
push *(GOT + 8) // 找到函数 _dl_runtime_resolver() 地址

bar@plt 实现:

bar@plt:
jmp *(bar@GOT)
push n // 保存函数的编号,符号 bar 在重定位表 .rel.plt 中的下标
jump PLT0 // 跳转到上面的 PLT0 处

延迟绑定
链接器在初始化阶段没有将函数 bar() 的地址填入 bar@GPT 相应项中,此时该项存放的内容是下一条指令 push n 的地址,之后先后将函数编号、模块 ID 压入堆栈,然后调用动态链接器的 _dl_runtime_resolve() 函数完成符号解析和重定位工作,并且会将函数 bar() 真正地址回填到 bar@GOT

一旦函数 bar() 被解析完毕,当再次调用 bar@GOT 时,由于该项存放的内容已经被回填为函数 bar() 的真正地址,所以遇到指令 jmp *(bar@GOT) 时,就能直接跳转到函数 bar() 去执行

你可能感兴趣的:(读书笔记)