收集一些技术好文程序员代码改变世界

5、动态链接

2017-03-04  本文已影响265人  eesly_yuan
初识动态链接

随着软件规模的逐渐增大,静态链接的缺点也逐渐暴露,有如下几个问题

为解决上述问题,引入了动态链接,其原理的基本思想是:在编译链接阶段,不对那些目标文件进行链接,而是将链接这个过程推迟到运行时再进行。在实现上基本思想是:将程序按模块拆分成几个独立的部分,在运行时才将它们链接成为一个完整的程序,即由动态链接器将动态库文件装载到进程的地址空间,并进行符号绑定和重定位,而不是像静态链接一样将所有目标文件直接链接成为一个独立的执行文件,相比于静态链接存在一定的性能损失,但在各种优化后损失在%5以下。
同时由于动态链接虚拟地址空间会比较复杂,需要系统的支持,linux中ELF动态链接文件被称为动态共享对象DSO(dynamic shared objects)。linux下编译一个动态库命令如下

gcc -fPIC -shared -o libxxx.so xxx.c

一个简单动态链接的例子如下图所示

gcc -o program1 program1.c ./xxx.so

上述编译过程介绍后,并不会把xxx.so链接到输出文件program1中。链接过程如下:链接器发现program1.o中引用的外部符号在一个动态库(动态库中保存由其到处的符号信息)中,此时链接器并不会对该符号进行重定位,而是标记其为动态链接符号,把真正的链接过程保留到装载时执行。


动态链接示例
地址无关代码

1、背景

上一节提到,动态链接在运行时将进程需要的动态库加载到进程的地址空间,那这个动态库的加载地址如何确定呢?是否和静态链接一样在编译时确定呢?本节将针对这个问题进行介绍。

gcc -shared -fPIC hello.c -o hello.so
-shared表示采用装载重定位技术
-fPIC表示采用地址无关代码技术

总结一下:装载重定位技术,解决动态库加载地址的问题(重点解决共享库中数据段符号重定位问题),PIC技术解决共享库在多个进程中共用问题(重点解决共享库中代码段中绝对地址引用文件)

2、PIC实现方式

下面将详细介绍以下PIC技术的实现方式。针对动态库中符号的引用方式不同,实现方式有所区别。动态库的地址引用方式可以分为以下四种


动态库符号引用方式分类

总结上述四中调用方式,可以用如下表表示

数据 函数
模块内部 相对地址访问 相对跳转调用
模块间 GOT表间接跳转 GOT表间接跳转

tips:如何区分一个dso是否为PIC,PIC的共享库将没有代码段的重定位表,通过判断下面命令是否有输出即可,有输出则不是PIC

readelf -d foo.so | grep TEXTREL
3、共享对象中全局变量问题

上面四种方式,基本覆盖了模块的符号调用方式,但还有一种情况没有考虑,即模块内部的全局变量(处理方式不能采用上述的第一种方式当成模块内部的数据访问)原因如下

exten int nglobal;

为解决上面的问题,针对共享库内的全局变量全部当作定义在其他模块的全局变量,此时采用上面第三种方式进行访问,即GOT间接访问,在加载重定位时,如果发现可执行文件的代码段中存在该符号的副本,则将GOT表对应位置指向该副本,如果执行模块中无该符号,则将GOT表中对应位置指向模块内部的该变量副本,如果模块内部对该变量进行初始化也需要对主执行文件的该变量进行初始化。

4、数据段的地址无关性

上面主要是解决共享对象中代码段中存在的绝对地址引用,那数据段中是否也存在绝对地址引用呢?如果存在该如何解决,考虑如下代码

static int a = 1;
static int * p = &a;

由于直接引用了数据a的地址,这是一个绝对地址引用。由于数据段在每个进程中都是独立管理的,因此针对数据段中存在的绝对地址引用可以采用上述介绍的加载重定位进行解决。具体实现如下:如果发现共享对象的数据段存在绝对地址引用,编译链接时将生成一个针对此种引用的重定位表,在运行加载时动态链接器,如果发现此种重定位表,将对数据段的绝对地址引用进行重定位。

延迟绑定PLT,procedure linkage table

动态链接相比静态链接有很多优势,但其是以牺牲一部分性能换取的,性能损失主要在以下两个方面
1、全局和静态数据访问、模块间的调用都需通过GOT间接访问
2、链接工作是在程序运行时进行的
这里将主要介绍延迟绑定,这是一种优化动态链接性能的方法,其基本思想是减小上面第二个问题带来的性能损失,当函数被第一次调用的时候进行重定位,如果没有用到则不进行重定位。
PLT表是ELF实现延迟绑定的方式,具体来说则是在模块间的函数调用时本来是应该通过GOT表进行间接调转,但为了实现延迟绑定,在中间又加了一层PLT,动态库中每个导出符号在PLT表中的结构如下

***symble***@plt:
jmp * (***symble***@got)
push n
push moduleID
jump _dl_runtime_resolve

可以看到第一条指令是调到got表中进行间接调整,如果got表中已经重定位了,那就直接实现了符号的查找,但是为例实现延迟绑定,链接器在初始化阶段并没有将符号的真实地址填入got表对应位置,而是将第二调指令push n填入了got表,这样跳转到got表后,又将跳转到push n这条命令。后面这三条命令的含义是_dl_runtime_resolve(moduleID,n)即解析本模块的第n个符号的地址,进行真正的符号解析和重定位工作,如果找到则将对应地址填入got表对应位置。这样当第一次调用的时候将进行重定位工作,之后再使用就直接通过GOT表间接选址了。假设liba.so需要用到libc.so的第3个符号bar函数则其PLT项大致如下所示

bar@plt
jmp *(bar@got)
push 3
push liba.so_id
jmp _dl_runtime_resolve

上面只是延迟绑定的原理,其实际实现还有所差别,ELF动态库在实现延迟绑定时实际的GOT表和PLT表如下所示


ELF动态库延迟绑定实现
动态链接相关结构

在上一篇进程加载中由提到,在加载完可执行文件后,如果是静态链接就将控制权转交给执行文件入口地址,如果是动态链接则转交给动态链接器ld.so,动态链接器实际上本身是一个共享对象,可以被加载到任意进程的地址空间,但其本身是静态链接,不依赖任何其他共享对象。这一节简单介绍一下,针对动态链接ELF中存在哪些相关是结构。

动态链接的实现步骤

本节将简单介绍以下动态链接的实现步骤,通常包含以下三个步骤

显示运行时链接

最后简单介绍显示运行时链接技术,这里运行时链接是特指执行文件在执行过程中,显示的执行加载一个共享对象,进行符号重定位的工作,并非程序在启动加载时动态链接器动态链接的过程。其实程序主要进行该动作也是通过调用动态链接器导出的相关函数完成的,主要包括以下几个函数

上一篇 下一篇

猜你喜欢

热点阅读