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

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

 

一、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语句是,是什么sb东西???接下来是不是要去百度cmpsb关键字?省了吧,.got节、.got.plt节明显被objdump曲解了。先了解下got和plt到底是什么东西。

        .got叫做全局偏移量表(global offset table),而plt是过程链接表(procedure linkage table)。在got表中有got[0]~got[n]的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]的地址,只需在GOT数组上跳3个步进,用0x804954c加个3*4字节即可,于是得到0x8049558,那么GOT[3]里的内容拼起来就是0x80482a6!什么?这个值怎么得出来的?是啊,我添加这几个字的几分钟之前也纳闷,自己去年写的东西,当初是怎么想出来的?结果稍微观察了下发现,既然GOT数组是按4字节对齐(别问我为什么,有本事找glibc开发者问去!),那么你可以从上表中804955a到8049558地址里面存的值倒着拼回来不就是0x80482a6了么?唉,看不懂的人一定是大笨蛋!大啊大……笨……egg……

继续找这个地址:

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的地址绑定不发生在链接器做链接时,而是延迟到程序被执行,动态链接器加载动态库后,程序第一次执行动态函数时,才完成函数地址绑定。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值