linux 动态加载so原理,Linux下动态链接实现原理

地址无关代码(PIC, position independent code)

从前面的介绍我们知道装载时重定位有重大的缺点:

它不能使动态库的指令代码被共享。

程序启动加载动态库后,符号重定位会比较花时间,特别是动态库多且复杂的情况下。

为了克服这些缺陷,ELF 引用了一种叫作地址无关代码的实现方案,该解决方案通过对变量及函数的访问加一层跳转来实现,非常的灵活。

1.模块内部符号的访问

模块内部符号在这里指的是:static 类型的变量与函数,这种类型的符号比较简单,对于 static 函数来说,因为在动态库编译完后,它在模块内的相对地址就已经确定了,而 x86 上函数调用只用到相对地址,因此此时根本连重定位都不需要进行,编译时就能确定地址,稍微麻烦一点的是访问数据,因为访问数据需要绝对地址,但动态库未被加载时,绝对地址是没法得知的,怎么办呢?

ELF 在这里使用了一个小技巧,根据当前 IP 值来动态计算数据的绝对地址,它的原理很简单,当动态库编译好之后,库中的数据段,代码段的相对位置就已经固定了,此时对任意一条指令来说,该指令的地址与数据段的距离都是固定的,那么,只要程序在运行时获取到当前指令的地址,就可以直接加上该固定的位移,从而得到所想要访问的数据的绝对地址了,下面我们用实例验证一下:

int g_share = 1;

static int g_share2 = 2;

int g_func(int a)

{

g_share += a;

return a * 2;

}

int g_func2()

{

int a = 2;

int b = g_func(3);

return a + b;

}

static int g_fun3()

{

g_share2 += 3;

return g_share2 - 1;

}

static int g_func4()

{

int a = g_fun3();

a + 2;

return a;

}

以上代码在x86 linux 下编译,再反汇编看看得到如下结果:

-bash-3.00$ gcc -o liba.so -fPIC -shared a.c

-bash-3.00$ objdump -S liba.so

//skip some of the output

00000564 :

564:55 push %ebp

565:89 e5 mov %esp,%ebp

567:e8 00 00 00 00 call 56c

56c:59 pop %ecx

56d:81 c1 60 11 00 00 add $0x1160,%ecx

573:83 81 20 00 00 00 03 addl $0x3,0x20(%ecx)

57a:8b 81 20 00 00 00 mov 0x20(%ecx),%eax

580:48 dec %eax

581:c9 leave

582:c3 ret

//skip some of the output

现在我们来分析验证一下,首先是地址 567 的指令有些怪,这儿不深究,简单来说,x86 下没有指令可以取当前 ip 的值,因此这儿使了个技巧通过函数调用来获取 ip 值(x86_64下就不用这么麻烦),这个技巧的原理在于进行函数调用时要将返回地址压到栈上,此时通过读这个栈上的值就可以获得下一条指令的地址了,在这儿我们只要知道指令 56c 执行后,%ecx 中包含了当前指令的地址,也就是 0x56c,再看 56d 及 573 两条指令,得知 %ecx + 0x1160 + 0x20 = 0x16ec 就是 573 指令所需要访问的地址,这个地址指向哪里了呢?

-bash-3.00$ objdump -s liba.so

Contents of section .data:

16e0 e0160000 f4150000 01000000 02000000 ................

结果是数据段里的第二个 int,也就是 g_share2!

2.模块间符号的访问

模块间的符号访问比模块内的符号访问要麻烦很多,因为动态库运行时被加载到哪里是未知的,为了能使得代码段里对数据及函数的引用与具体地址无关,只能再作一层跳转,ELF 的做法是在动态库的数据段中加一个表项,叫作 GOT(global offset table), GOT 表格中放的是数据全局符号的地址,该表项在动态库被加载后由动态加载器进行初始化,动态库内所有对数据全局符号的访问都到该表中来取出相应的地址,即可做到与具体地址了,而该表作为动态库的一部分,访问起来与访问模块内的数据是一样的。

仍然使用前面的例子,我们来看看 g_func 是怎么访问 g_share 变量的。

00000504 :

504:55 push %ebp

505:89 e5 mov %esp,%ebp

507:53 push %ebx

508:e8 00 00 00 00 call 50d

50d:5b pop %ebx

50e:81 c3 bf 11 00 00 add $0x11bf,%ebx

514:8b 8b f0 ff ff ff mov 0xfffffff0(%ebx),%ecx

51a:8b 93 f0 ff ff ff mov 0xfffffff0(%ebx),%edx

520:8b 45 08 mov 0x8(%ebp),%eax

523:03 02 add (%edx),%eax

525:89 01 mov %eax,(%ecx)

527:8b 45 08 mov 0x8(%ebp),%eax

52a:d1 e0 shl %eax

52c:5b pop %ebx

52d:c9 leave

52e:c3 ret

上面的输出中,508 与 50d 处的指令用于获取 ip 值, 执行完 50d 后, %ebx 中放的是 0x50d, 地址 50e 用于计算 g_share 在 GOT 中的地址 0x50d + 0x11bf + 0xfffffff0 = 0x16bc, 我们检查一下该地址是不是 GOT:

-bash-3.00$ objdump -h liba.so

liba.so: file format elf32-i386

Sections:

Idx Name Size VMA LMA File off Algn

//skip some of the output

16 .got 00000010 000016bc 000016bc 000006bc 2**2

CONTENTS, ALLOC, LOAD, DATA

显然,0x16bc 就是 GOT 表的第一项。

事实上,ELF 文件中还包含了一个重定位段,里面记录了哪些符号需要进行重定位,我们可以通过它验证一下上面的计算是否与之匹配:

-bash-3.00$ objdump -R liba.so

liba.so: file format elf32-i386

DYNAMIC RELOCATION RECORDS

OFFSET TYPE VALUE

000016e0 R_386_RELATIVE *ABS*

000016e4 R_386_RELATIVE *ABS*

000016bc R_386_GLOB_DAT g_share

000016c0 R_386_GLOB_DAT __cxa_finalize

000016c4 R_386_GLOB_DAT _Jv_RegisterClasses

000016c8 R_386_GLOB_DAT __gmon_start__

000016d8 R_386_JUMP_SLOT g_func

000016dc R_386_JUMP_SLOT __cxa_finalize

如上输出, g_share 的地址在 0x16bc,与前面的计算完全吻合!

致此,模块间的数据访问就介绍完了,模块间的函数调用在实现原理上是一样的,也需要经过一个类似 GOT 的表格进行跳转,但在具体实现上,ELF 为了实现所谓延迟绑定而作了更精细的处理,接下来会介绍。

延迟加载

我们知道,动态库是在程序启动的时候加载进来的,加载后,动态链接器需要对其作一系列的初始化,如符号重定位,这些工作是比较费时的,特别是对函数的重定位,那么我们能不能把对函数的重定位延迟进行呢?这个改进是很有意义的,毕竟很多时候,一个动态库里可能包含很多的全局函数,但是我们往往可能只用到了其中一小部分而已,完全没必要把那些没用到的函数也过早进行重定位,具体来说,就是应该等到第一次发生对该函数的调用时才进行符号绑定 -- 此谓之延迟绑定。

延迟绑定的实现步骤如下:

建立一个 GOT.PLT 表,该表用来放全局函数的实际地址,但最开始时,该里面放的不是真实的地址而是一个跳转,接下来会讲。

对每一个全局函数,链接器生成一个与之相对应的影子函数,如 fun@plt。

所有对 fun 的调用,都换成对 fun@plt 的调用,

长成如下样子:

fun@plt:

jmp *(fun@got.plt)

push index

jmp _init

其中第一条指令直接从 got.plt 中去拿真实的函数地址,如果已经之前已经发生过调用,got.plt 就已经保存了真实的地址,如果是第一次调用,则 got.plt 中放的是 fun@plt 中的第二条指令,这就使得当执行第一次调用时,fun@plt中的第一条指令其实什么事也没做,直接继续往下执行,第二条指令的作用是把当前要调用的函数在 got.plt 中的编号作为参数传给 init(),而init() 这个函数则用于把 fun 进行重定位,然后把结果写入到 got.plt 相应的地方,最后直接跳过去该函数。

仍然是使用前面的例子,我们看看 g_func2 是怎样调用 g_func 的:

0000052f :

52f:55 push %ebp

530:89 e5 mov %esp,%ebp

532:53 push %ebx

533:83 ec 14 sub $0x14,%esp

536:e8 00 00 00 00 call 53b

53b:5b pop %ebx

53c:81 c3 91 11 00 00 add $0x1191,%ebx

542:c7 45 f8 02 00 00 00 movl $0x2,0xfffffff8(%ebp) // a = 2

549:83 ec 0c sub $0xc,%esp

54c:6a 03 push $0x3 // push argument 3 for g_func.

54e:e8 d5 fe ff ff call 428

553:83 c4 10 add $0x10,%esp

556:89 45 f4 mov %eax,0xfffffff4(%ebp)

559:8b 45 f4 mov 0xfffffff4(%ebp),%eax

55c:03 45 f8 add 0xfffffff8(%ebp),%eax

55f:8b 5d fc mov 0xfffffffc(%ebp),%ebx

562:c9 leave

563:c3 ret

如上汇编,指令 536, 53b, 53c, 用于计算 got.plt 的具体位置,计算方式与前面对数据的访问原理是一样的,经计算此时, %ebx = 0x53b + 0x1191 = 0x16cc, 注意指令 54e, 该指令调用了函数 g_func@plt:

00000428 :

428:ff a3 0c 00 00 00 jmp *0xc(%ebx)

42e:68 00 00 00 00 push $0x0

433:e9 e0 ff ff ff jmp 418 <_init>

注意到此时, %ebx 中放的是 got.plt 的地址,g_func@plt 的第一条指令用于获取 got.plt 中 func 的具体地址, func 放在 0xc + %ebx = 0xc + 0x16cc = 0x16d8, 这个地址里放的是什么呢?我们查一下重定位表:

-bash-3.00$ objdump -R liba.so

liba.so: file format elf32-i386

DYNAMIC RELOCATION RECORDS

OFFSET TYPE VALUE

000016e0 R_386_RELATIVE *ABS*

000016e4 R_386_RELATIVE *ABS*

000016bc R_386_GLOB_DAT g_share

000016c0 R_386_GLOB_DAT __cxa_finalize

000016c4 R_386_GLOB_DAT _Jv_RegisterClasses

000016c8 R_386_GLOB_DAT __gmon_start__

000016d8 R_386_JUMP_SLOT g_func

000016dc R_386_JUMP_SLOT __cxa_finalize

可见,该地址里放的就是 g_func 的具体地址,那此时 0x16d8 放的是真正的地址了吗?我们再看看 got.plt:

Contents of section .got.plt:

16cc fc150000 00000000 00000000 2e040000 ................

16dc 3e040000

16d8 处的内容是: 2e040000, 小端序,换回整形就是 0x000042e, 该地址就是 fun@plt 的第二条指令!是不是觉得有点儿绕?你可以定下心来再看一遍,其实不绕,而是很巧妙。

后话

对动态链接库来说,加载时重定位与链接时重定位各有优缺点,前者使得动态库的代码段不能被多个进程间所共享,加载动态库时也比较费时,但是加载完成后,因为对符号的引用不需要进行跳转,程序运行的效率相对是较高的。而对地址无关的代码,它的缺点是动态库的体积相对较大,毕竟增加了很多表项及相关的函数,另外就运行时对全局符号的引用需要通过表格进行跳转,程序执行的效率不可避免有所损失,优点嘛,就是动态库加载比较快,而且代码可以在多个进程间共享,对整个系统而言,可以大大节约对内存的使用,这个好处的吸引力是非常大的,所以你可以看到,目前来说在常用的动态库使用上,PIC 相较而言是更加被推崇的,道理在此。

0b1331709591d260c1c78e86d0c51c18.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值