文章目录
  1. 动态链接的意义
  2. 地址无关代码
    1. 延迟绑定的实现
    2. 共享对象的全局变量
  3. 动态链接相关结构
    1. .interp段
    2. .dynamic段
    3. 动态符号表
    4. 动态链接重定位表
    5. 进程堆栈初始化

动态链接的意义

静态链接的不足:浪费计算机内存与磁盘空间(许多公用库函数在内存&可执行文件中有多份,这是没有必要的);极大降低程序开发的效率(一旦程序中有任一模块更新,整个程序就要重新链接,然后发布给用户)。

为了避免这些不足,出现了动态链接——将程序需要的模块相互分割开,等运行时再进行链接。使得程序具有可扩展性,在运行时可以动态地选择加载各种程序模块。

ELF动态链接文件以.so为扩展名,在Windows系统中动态链接库以.dll为扩展名。

地址无关代码

显然,动态链接必然要求共享对象在被装载前无法得知其装载地址。与链接时重定位不同,由于指令是多个进程之间共享的,共享对象映射到每个进程的地址空间中的虚拟地址是不同的,从而指令中对绝对地址的引用也是不同的,这就导致指令部分无法在多进程之间共享。

解决办法是将指令中会因装载地址改变而改变的部分分离出来,与数据部分放在一起。于是多个进程可以共享指令部分,而数据部分每个进程都有一个副本。这就是地址无关代码。

那么编译器如何编写地址无关的代码,先考虑在源代码中有哪些地址引用方式。按照是否跨模块与指令引用OR数据访问分成如下。

  • 模块内部的函数调用、跳转;
  • 模块内部的数据访问;
  • 模块外部的函数调用、跳转;
  • 模块外部的数据访问;

对于第一种地址引用方式 被调用函数与调用指令的相对位置固定,可使用相对地址调用或基于寄存器的相对调用,对于这种指令无需重定位。

对于第二种地址引用方式 被访问变量与访问指令的相对位置固定,但是,现代的计算机体系结构中没有相对于当前指令地址的寻址数据方式,因此在ELF文件中用如下巧妙的方式来得到当前指令地址;

在需要知道当前指令地址的指令前调用__i686.get_pc_thunk.cx函数如下所示;

1
2
3
00000494 <__i686.get_pc_thunk.cx>:
494: 8b 0c 24 mov (%esp), %ecx
497: c3 ret

这时因为CPU执行call指令时会将下一指令的地址压栈,而esp寄存器始终指向栈顶,因此__i686.get_pc_thunk.cx函数的作用就是将需要知道的指令地址存入ecx寄存器。之后加上偏移得到数据的绝对地址即可访问变量。

对于第四种地址引用方式 被访问变量所在模块的装载地址在装载前不确定,因此被访问变量的绝对地址在装载前也不确定。为保证代码的地址无关性,ELF在数据段建立一个指向这些变量的指针数组——全局偏移表GOT,代码通过GOT中对应项间接访问变量。链接器在装载模块时会查找每个变量所在地址,并填充.got表中各项。而访问.got表中对应项即第二种地址引用方式。

对于第三种地址引用方式 为加快程序的启动速度,ELF对于模块外部的函数采用延迟绑定——当函数第一次被调用时将其绑定。ELF将GOT拆分为.got与.got.plt两个表,分别保存全局变量的地址与外部函数的地址;其中,.got.plt的前三项分别为.dynamic段的地址、本模块的ID、_dl_runtime_resolve函数的地址,之后就是外部函数的地址。不同的是,链接器在装载模块时并不会将外部函数进行绑定。

使用readelf查看所有段的信息。

1
objdump -s -d Lib.so

延迟绑定的实现

为了实现延迟绑定,ELF在间接访问外部函数的过程中增加了一次间接跳转,调用外部函数gfun时先跳转到外部函数在.plt表中对应项gfun@plt,gfun@plt的实现如下。

1
2
3
4
gfun@plt:
jmp *(gfun@got) #gfun@got为gfun在.got表中对应项,即gfun的地址,为实现延迟绑定,gfun@got最初存放的是下面push n指令的地址
push n #n为gfun这个符号在重定位表.rel.plt中的下标
jmp PLT0

其中,PLT0为.plt表中第一项,它的实现为

1
2
3
push *(GOT+4) #GOT为.got.plt表的地址,而根据.got.plt表的前三项内容可知,该指令是将本模块的ID压栈
jmp *(GOT+8) #同理,该指令跳转到_dl_runtime_resolve函数处执行,该函数用来绑定gfun函数,前面的压栈是模拟函数的参数进栈
nopl #.plt表中每一项为16字节

要使gcc编译时产生地址无关代码,加上-fPIC选项即可。要想判断一个so文件是否是地址无关代码,可使用如下命令。

1
readelf -d lib.so | grep TEXTREL

共享对象的全局变量

由于共享对象的全局变量可能在模块外部修改,因此编译器在编译共享对象时,默认将模块内部的全局变量当作模块外部的全局变量。采用上述第四种地址引用方式访问全局变量。共享对象被装载时,对其中的全局变量var:若var在可执行文件中被分配了空间,则var在GOT中的地址指向可执行文件中的var(若var在共享对象中被初始化,还需要将初始化的值复制到可执行文件的var);否则,var在GOT中的地址指向共享对象内的var。

动态链接相关结构

.interp段

.interp段中保存可执行文件所需动态链接器的路径。通常为/lib64/ld-linux-x86-64.so.2,这是一个软链接,从而动态链接器更新版本时只需要将/lib64/ld-linux-x86-64.so.2指向新的动态链接器,而不用改动可执行文件的.interp段。

.dynamic段

.dynamic段保存动态链接器所需的基本信息(依赖的共享对象、动态链接符号表地址、动态链接重定位表地址,共享对象初始化代码地址等)。.dynamic段就是一个结构体数组,结构体定义如下。

1
2
3
4
5
6
7
typedef struct{
Elf32_Sword d_tag;
union{
Elf32_Word d_val;
Elf32_Addr d_ptr;
}d_un;
}Elf32_Dyn;

d_tag代表后面的d_un的含义,常见的d_tag类型及其含义如下。

 

使用readelf工具可查看.dynamic段的内容。

1
readelf -d Lib.so

动态符号表

保存动态链接的共享模块间符号的导入导出关系。动态符号表在可执行文件中为.dynsym段。

动态链接重定位表

.rel.dyn段修正的地址位于.got段与数据段,.rel.plt修正的地址位于.got.plt(也存在.got未被拆分的情况,此时.rel.plt修正的符号也位于.got)。使用readelf工具可查看可执行文件的重定位表。

1
readelf -r Lib.so

其中有如下三种类型的重定位入口:R_386_RELATIVE、R_386_GLOB_DAT、R_386_JUMP_SLOT。R_386_GLOB_DAT与R_386_JUMP_SLOT。被修正位置只需要直接填入符号的地址即可;R_386_RELATIVE入口的重定位方式为基址重置,这是由于编译器在编译共享对象时,为其分配的地址空间从0开始,假设一个静态变量的地址为B,则对此静态变量的地址重定位时,再加上共享对象的装载地址A,即A+B。

进程堆栈初始化

同样地,操作系统在将控制权交给动态链接器之前,会在进程的堆栈中保存动态链接器所需信息,以辅助信息结构体数组的形式存放,结构体定义如下。

1
2
3
4
5
6
typedef struct{
uint32_t a_type;
union{
uint32_t a_val;
}a_un;
}Elf32_auxv_t;

该结构体定义与.dynamic段的结构体定义类似,a_type为类型值,a_val为数值,常见的类型及其含义如下。

 

辅助信息数组存放在环境变量指针之后。