链接(2)——动态链接汇编探秘
2015-06-05 12:27
281 查看
关于动态链接原理性文章有很多,在此本人尽量以深入浅出和少量的篇幅将问题阐述清楚,抛开无关的扩展。
一、linux加载可执行文件时的存储器映像
下面图片所描述的栈空间,是linux加载应用程序时所生成的:
![](http://img.blog.csdn.net/20150605125109415?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMzQ3MTk0Ng==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
首先,如果不更改内核,那么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个全局量的偏移地址,它符合下表所描述的结构特性:
接着分析,我们先看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的地址绑定不发生在链接器做链接时,而是延迟到程序被执行,动态链接器加载动态库后,程序第一次执行动态函数时,才完成函数地址绑定。
一、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地址 |
反汇编 .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的地址绑定不发生在链接器做链接时,而是延迟到程序被执行,动态链接器加载动态库后,程序第一次执行动态函数时,才完成函数地址绑定。
相关文章推荐
- 开始刷leetcode day32: Rotate List
- 团队工作总结
- SQL注入原理解说,非常不错!
- win7远程连接ubuntu14.04.1桌面
- ubuntu创建启动器
- 融云 Android sdk kit 头像昵称更新机制
- LightOJ1008---Fibsieve`s Fantabulous Birthday (规律)
- leetcode 21 -- Merge Two Sorted Lists
- Theano3.1-练习之初步介绍
- Theano3.1-练习之初步介绍
- iOS中动态计算字符串的长度
- 日期类-Java
- python学习――模块和模块的常用方法
- IOS的企业版安装包 点击链接直接安装的方式 plist方式
- js+html5通过canvas指定开始和结束点绘制线条的方法
- hdoj1090(2)
- CCNA学习指南 TCP/IP
- AndroidManifest.xml——action 活动
- “automation服务器不能创建对象”的问题的解决方案大全
- leetcode 20 -- Valid Parentheses