问题
有两个程序公用一个动态库,一个程序加载使用后,另一个程序是如何知道动态库的地址,并使用它?
动态链接的基本思想是把程序按照模块的形式拆分成各个相互独立的部分,在程序运行的时候才将他们链接起来,形成一个完整的程序。前面蒋介绍了静态链接数时,是如何解决符号重定位问题。动态链接的符号重定位又是如何解决的,与静态链接的符号重定位有什么不同?
动态链接与静态链接的重定位阶段不一样
动态链接与静态链接不一样,静态链接是在链接时重定位,因为静态链接在链接时,符号地址是确定的。而动态链接,在链接时,共享目标文件的符号地址是不确定的,只有在程序运行、加载时,地址才被分配,才能确认的,因此动态链接只能在装载时重定位。
动态链接实现的方式
目前来说,动态链接可以使用两种方式:装载时重定位和地址无关码。装载时重定位与静态链接的原理一样,只是把重定位的时机放在动态库加载后,是解决模块中有绝对地址引用的方法之一,但缺点是无法再多个人进程间共享,因为模块被动态加载时,会映射到虚拟空间,部分指令是被多个进程之间共享的。由于装载时重定位需要修改指令,一个进程修改后,对另一个进程是不适用的,因为动态模块映射到每个进程的虚拟地址是不一样的。没有方法做到用一份指令重定位后,被多个进程共享。 除非动态链接在每个进程都有一个副本,只是这样就是失去了动态链接节省内存的优势。这时需要引入地址无关码的方式来实现。
地址无关码原理
地址无关码的实现原理是指把指令中需要修改的部分分离出来,跟数据部分放在一起,这样指令的部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,即是数据被改变,不同进程间也互不影响。这个方案被称为地址无关码技术。
动态链接的地址无关码的实现引入了GOT表(全局偏移表),GOT表是一个建立在数据段的指针数组,指向引用的变量和函数加载后的地址。当代码需要引用变量或者函数时,可以通过GOT表间接去引用。GOT表存放着引用的变量和函数真实地址。
下面介绍动态链接的函数引用例子
sub.c文件代码:
#include<stdio.h>
int g_share = 8;
int prin()
{
printf("chen_test\n");
}
int sub(int a,int b)
{
g_share++;
prin();
return a-b;
}
main.c文件代码
#include<stdio.h>
int main()
{
int a = 10,b = 12;
sub(b,a);
return 0;
}
编译、制作动态库libsub.so
arm-himix200-linux-gcc -fPIC -shared -o libsub.so sub.c
main.c链接动态库
arm-himix200-linux-gcc -o main main.c -lsub -L.
查看可执行文件的重定位表
root@chen:/home/chenjg/share/test/dynamic# arm-himix200-linux-readelf -r main
Relocation section '.rel.dyn' at offset 0x40c contains 1 entries:
Offset Info Type Sym.Value Sym. Name
0002101c 00000815 R_ARM_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x414 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
0002100c 00000716 R_ARM_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.4
00021010 00000816 R_ARM_JUMP_SLOT 00000000 __gmon_start__
00021014 00000e16 R_ARM_JUMP_SLOT 00000000 sub
00021018 00000f16 R_ARM_JUMP_SLOT 00000000 abort@GLIBC_2.4
查看可执行文件GOT表的虚拟地址为00021000
arm-himix200-linux-objdump -h main
20 .got 00000020 00021000 00021000 00001000 2**2
可以看出,main.c文件引用动态库libsub.so提供的sub函数,由于是对外引用,所以把sub符号表归入了重定位表中,其中
查看可执行文件的反汇编代码
arm-himix200-linux-objdump -S main
0001046c <sub@plt>:
1046c: e28fc600 add ip, pc, #0, 12
10470: e28cca10 add ip, ip, #16, 20 ; 0x10000
10474: e5bcfba0 ldr pc, [ip, #2976]! ; 0xba0
000105ac <main>:
105ac: e92d4800 push {fp, lr}
105b0: e28db004 add fp, sp, #4
105b4: e24dd008 sub sp, sp, #8
105b8: e3a0300a mov r3, #10
105bc: e50b3008 str r3, [fp, #-8]
105c0: e3a0300c mov r3, #12
105c4: e50b300c str r3, [fp, #-12]
105c8: e51b1008 ldr r1, [fp, #-8]
105cc: e51b000c ldr r0, [fp, #-12]
105d0: ebffffa5 bl 1046c <sub@plt>
105d4: e3a03000 mov r3, #0
105d8: e1a00003 mov r0, r3
105dc: e24bd004 sub sp, fp, #4
105e0: e8bd8800 pop {fp, pc}
主函数main调用动态库sun函数,首先是先经过一个中间跳转,即sub@plt,这个跳转点的地址是1046c,跳到1046c地址后经过计算后(10474+12的十六进制及c+0x10000+0xba0 = 00021014),最后跳转到00021014,而00021014位于.got 上(.got的虚拟地址为00021000 ),即是存放sub函数的真实地址。
正常情况下,主函数main调用动态库sun函数,应该可以直接通过GOT表跳转到sun函数地址上,为什么还需要一个中间跳转,即sub@plt。其实是有一定的原因。是为了实现延迟绑定。进一步说是为了优化动态链接的性能。
在动态链接下,程序启动时,是要耗时去完成所有函数引用的符号查找、重定位等工作的,方便函数之间无障碍地调用。如果程序模块之间有大量的函数引用,而且很多的函数甚至很多功能模块都没有用到,程序也要函数的引用的符号查找、重定位工作,就显得有些多余,也影响程序的启动速度。所有就有了延迟绑定的概念。
延迟绑定的核心思想是函数只有第一次被调用时才进行绑定(符号查找、重定位),没有用到的函数是不需要绑定工作的。延迟绑定使用PLT的方法来实现,每个函数引用都会配对一个fun@plt(fun为函数名),程序启动时,被调用的函数第一次都会跳转到fun@plt(fun为函数名),去完成符号查找、重定位工作,完整这些工作后,最终将引用函数的真正地址放在fun@GOT中。再次调用该函数时,直接通过fun@plt(fun为函数名)跳转到fun@GOT,即该函数的真正地址中。
而sun的真实地址是如何被重定位的?
动态链接器会先查找sub的真实地址,会在全局符号表里去查,因为动态库加载后,所有符号的信息包括地址都会更新到全局符号表中。在全局符号表中找到sub地址后。借助重定位表里的“.rel.plt”的sub的偏移地址00021014(位于.got上,该地址存放这个sub的真实地址),将sub的地址修正。在链接时,由于sub函数的地址还没确认,一开始的地址是错误的,所以进程加载动态库时需要修正。
**总结:**装载重定位和地址无关码是解决绝对地址引用问题的两个方法,装载重定位的缺点是无法共享代码段,每个进程都要有一个副本。但运行速度相对较快。而无关代码的缺点是运行速度相对慢,但可以实现代码段在各个进程间的共享,节省空间。