您的位置:首页 > 其它

链接(2)——动态链接汇编探秘

2015-06-05 12:27 281 查看
关于动态链接原理性文章有很多,在此本人尽量以深入浅出和少量的篇幅将问题阐述清楚,抛开无关的扩展。

一、linux加载可执行文件时的存储器映像

下面图片所描述的栈空间,是linux加载应用程序时所生成的:



首先,如果不更改内核,那么linux系统加载程序(包括内核里的子进程)都是从0x08048000地址开始的。当加载器运行时,先对应用程序中的可执行文件进行解析,将代码段和数据段按照4kB对齐的方式,从0x08048000开始往高地址放。紧接着就是存放堆,堆的增长方向会按照箭头所指往高地址增长。

但是堆的空间是有限的,它只能增长到0x3fffffff,从0x40000000开始,需要存放动态库,这个动态库的加载,仍然属于加载器的工作范畴,有多少库就加载多少库,但同样我们也看到,动态库的加载量也是有限的,它和用户栈的增长方向相反,过多的库会压制用户栈的增长,而用户栈的无限扩展也会影响到动态库的调用数量。

用户栈是从0xbfffffff开始的,那么从0xc0000000开始向上,保留的就是内核代码,用于对所加载程序的控制。

由于C语言的指针访问时不受程序和操作系统限制的,理论上我们可以访问到空间内任意的地址,当然,内核本身有预警机制,当你的指针试图修改代码段、共享库段甚至是内核段时,很可能就会出现著名的“segmentation fault (core dumped) ”段错误,这是操作系统的自我保护机制。

现在就有个问题可以思考下,我们都知道动态链接库是在程序运行时才由加载器提供的,我们同样还知道,链接器在生成可执行文件时,已经把函数跳转的逻辑地址写死了,那请问调用动态函数(比如printf)时,由于未执行即未加载,汇编代码是如何解释printf的跳转地址呢?程序在运行时,以什么依据到上图中的共享库区域寻找自己想要的函数地址呢?

二、位置无关代码PIC

动态库存在的一个主要目的就是,允许多个正在运行的进程来共享相同的库代码,从而节约宝贵的存储资源。那么库代码本身在硬盘的哪个位置并不重要,只要事先已被编译,任何进程随时都可以把它需要的,库代码移花接木到共享库映射区域中。

我们就拿最简单的printf函数来说,它是属于libc.so中的库函数,于是我们写个最简单的调用函数:

#include <stdio.h>

void main(void)

{

printf("haha!\n");

return;

}

直接gcc -O2编译出来a.out,于是进行反编译:objdump -D a.out:

真是够长的,先看main函数部分的printf调用:

08048368 <main>:

8048368: 55 push %ebp

8048369: 89 e5 mov %esp,%ebp

804836b: 83 ec 08 sub $0x8,%esp

804836e: 83 e4 f0 and $0xfffffff0,%esp

8048371: 83 ec 1c sub $0x1c,%esp

8048374: 68 60 84 04 08 push $0x8048460

8048379: e8 22 ff ff ff call 80482a0 <puts@plt>

804837e: c9 leave

804837f: c3 ret

call语句让我们去找0x80482a0地址,好吧,那么我们去找找发现:

080482a0 <puts@plt>:

80482a0: ff 25 58 95 04 08 jmp *0x8049558

80482a6: 68 00 00 00 00 push $0x0

80482ab: e9 e0 ff ff ff jmp 8048290 <_init+0x18>

好吧,又要跳转到0x8049558,于是我们走着:

反汇编 .got 节:

08049548 <.got>:

8049548: 00 00 add %al,(%eax)

...

反汇编 .got.plt 节:

0804954c <_GLOBAL_OFFSET_TABLE_>:

804954c: 80 94 04 08 00 00 00 adcb $0x0,0x8(%esp,%eax,1)

8049553: 00

8049554: 00 00 add %al,(%eax)

8049556: 00 00 add %al,(%eax)

8049558: a6 cmpsb %es:(%edi),%ds:(%esi)

8049559: 82 (bad)

804955a: 04 08 add $0x8,%al

804955c: b6 82 mov $0x82,%dh

804955e: 04 08 add $0x8,%al

我去……8049558对应的cmpsb语句,什么东西???接下来是不是要去百度cmpsb关键字?省了吧,.got节、.got.plt节明显被objdump曲解了。先了解下got和plt到底是什么东西。

.got叫做全局偏移量表(global offset table),而plt是过程链接表(procedure linkage table)。在got表中有got[0]~got
的n个全局量的偏移地址,它符合下表所描述的结构特性:

地址表目内容描述
GOT[0].dynamic节地址
GOT[1]链接器标识信息
GOT[2]动态链接器入口点
GOT[3]printf函数调用push地址
接着分析,我们先看804954c,它的内容80 94 04 08,是不是看起来很眼熟?对了,倒过来就是地址0x8049480,到这个地址去看看:

反汇编 .dynamic 节:

08049480 <_DYNAMIC>:

8049480: 01 00 add %eax,(%eax)

8049482: 00 00 add %al,(%eax)

8049484: 24 00 and $0x0,%al

8049486: 00 00 add %al,(%eax)

8049488: 0c 00 or $0x0,%al

804948a: 00 00 add %al,(%eax)

是不是刚好为上表中说的.dynamic节起始地址?!也就是说这个是GOT[0]的内容,也就是说它的长度是4字节。依次推下去,如果我们想获得所关心的GOT[3]的地址,只需用0x804954c加个3*4字节即可,于是得到0x8049558,那么GOT[3]里的内容拼起来就是0x80482a6!继续找这个地址:

08048290 <puts@plt-0x10>:

8048290: ff 35 50 95 04 08 pushl 0x8049550

8048296: ff 25 54 95 04 08 jmp *0x8049554

804829c: 00 00 add %al,(%eax)

...

080482a0 <puts@plt>:

80482a0: ff 25 58 95 04 08 jmp *0x8049558

80482a6: 68 00 00 00 00 push $0x0

80482ab: e9 e0 ff ff ff jmp 8048290 <_init+0x18>

压入0,这个是首个被调用的外部函数所以标识为0,接下来跳转到8048290,也就是<puts@plt-0x10>: 的部分,压入0x8049550,这是GOT[1]的地址也就是链接器的标识信息。继续jmp到0x8049554,这是GOT[2]也就是动态链接器入口点,这两个跳转都是跳到内核中执行相应的代码,最后动态链接器会通过一系列变态运算,将printf的地址定位出来,假设是0x41111111,并用此地址值覆盖GOT[3]里的值,并把控制传递给printf。

当下次再调用printf时,main函数执行call 80482a0 <puts@plt>时,会继续执行 80482a0: ff 25 58 95 04 08 jmp *0x8049558跳转到GOT[3],但此时GOT[3]中的0x80482a6已经被0x41111111覆盖,因此程序直接跳转到0x41111111也就是printf库函数地址去执行!

我之所以敢随便编一个0x41111111,首先因为我根据第一部分所描述的程序进程得知动态库地址从0x40000000开始,而具体映射到哪个值,只有进程运行后才晓得,所以怎么编都没人敢说我错,哈哈!

回顾上面对动态函数的分析,我们发现这是ELF编译系统一个很有趣的技术,他被成为延迟绑定(lazy binding),很奇怪为啥不是懒人绑定呢O(∩_∩)O~,意思就是说,printf的地址绑定不发生在链接器做链接时,而是延迟到程序被执行,动态链接器加载动态库后,程序第一次执行动态函数时,才完成函数地址绑定。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: