C程序的动态链接

在一个简单的hello,world程序中,我们调用了libc库中的printf函数。libc是动态加载库(shared object),即libc的代码在内存中只有一份,即使有多个程序会调用到它。为了使不同的进程共享同一份代码,libc的代码是位置无关(Position Independent)的,即无论代码被加载到内存的何处,都可以正常运行。 但是这也给程序的运行带来了困扰,正由于位置无关代码(Position Independent Code,PIC)被动态地加载到内存,因此在编译期我们无法得知符号(全局变量或函数)的具体地址。那么程序是怎么在运行期得知这些符号的地址的呢?我们将结合下面的代码进行分析。

//main.c
#include <stdio.h>

int main() {
        printf("hello, world\n");
    return 0;
}

main.c通过gcc -m32 main.c编译成a.out,并使用objdump -D a.out命令反汇编,首先看一下main函数部分:

00001060 <main>:
    1060:       8d 4c 24 04             lea    0x4(%esp),%ecx
    1064:       83 e4 f0                and    $0xfffffff0,%esp
    1067:       ff 71 fc                push   -0x4(%ecx)
    106a:       55                      push   %ebp
    106b:       89 e5                   mov    %esp,%ebp
    106d:       53                      push   %ebx
    106e:       e8 6d 00 00 00          call   10e0 <__x86.get_pc_thunk.bx>
    1073:       81 c3 8d 2f 00 00       add    $0x2f8d,%ebx
    1079:       51                      push   %ecx
    107a:       83 ec 0c                sub    $0xc,%esp
    107d:       8d 83 08 e0 ff ff       lea    -0x1ff8(%ebx),%eax
    1083:       50                      push   %eax
    1084:       e8 b7 ff ff ff          call   1040 <puts@plt>
    1089:       8d 65 f8                lea    -0x8(%ebp),%esp
    108c:       31 c0                   xor    %eax,%eax
    108e:       59                      pop    %ecx
    108f:       5b                      pop    %ebx
    1090:       5d                      pop    %ebp
    1091:       8d 61 fc                lea    -0x4(%ecx),%esp
    1094:       c3                      ret    
    1095:       66 90                   xchg   %ax,%ax
    1097:       66 90                   xchg   %ax,%ax
    1099:       66 90                   xchg   %ax,%ax
    109b:       66 90                   xchg   %ax,%ax
    109d:       66 90                   xchg   %ax,%ax
    109f:       90                      nop

因为a.out会被加载到内存中执行,这样在描述一条指令时就会有在内存中的地址和在文件中的地址这两套地址,为简单起见,我们假设程序被加载到0x0开始的区域,从而两套地址相同。

0x1060-0x106b是对堆栈进行一些初始化设置;

0x106d-0x1073是将GOT(Global Offset Table,全局偏移表)的地址保存到%ebx寄存器中。由于这里我们在编译时使用了-m32选项,我们得到的程序是使用32位机器码的,不支持64位机器的相对%rip寻址,因此这里需要借助__x86.get_pc_thunk.bx函数实现。看一下0x10e0地址处的__x86.get_pc_thunk.bx实现:

000010e0 <__x86.get_pc_thunk.bx>:
    10e0:	8b 1c 24             	mov    (%esp),%ebx
    10e3:	c3                   	ret    

非常简单,就两行汇编代码。首先取出%esp寄存器中的值,将其存放到%ebx寄存器中。%esp寄存器中的值是什么呢?其实就是__x86.get_pc_thunk.bx的调用者的下一条指令在内存中的地址,也就是main函数中0x1073处add $0x2f8d,%ebx这条指令的地址。为什么呢?因为call指令会首先把返回地址(也就是上述指令的地址)压入栈中,然后再跳转到目标函数(__x86.get_pc_thunk.bx)处。在__x86.get_pc_thunk.bx中,%esp寄存器中的值就是add $0x2f8d,%ebx这条指令的地址!__x86.get_pc_thunk.bx返回后,再执行add $0x2f8d,%ebx,其中$0x2f8d是该指令与GOT之间的偏移值,可以在编译期确定。0x1073+0x2f8d=0x4000。果然,0x4000处的内容正是GOT!这样,%ebx寄存器中的值就是GOT的地址了。

00004000 <_GLOBAL_OFFSET_TABLE_>:
    4000:	fc                   	cld    
    4001:	3e 00 00             	add    %al,%ds:(%eax)
	...
    400c:	46                   	inc    %esi
    400d:	10 00                	adc    %al,(%eax)
    400f:	00 56 10             	add    %dl,0x10(%esi)

接着是地址0x1084处的指令call 1040 <puts@plt>,这里调用了地址0x1040处的puts(如果printf的参数是纯字符串,gcc会将其优化成puts)。来看下0x1040处的指令:

00001040 <puts@plt>:
    1040:       ff a3 0c 00 00 00       jmp    *0xc(%ebx)
    1046:       68 00 00 00 00          push   $0x0
    104b:       e9 e0 ff ff ff          jmp    1030 <_init+0x30>

0x1040处的指令jmp *0xc(%ebx)首先读取(%ebx)+0xc处的值,然后跳转到该值处。%ebx寄存器中的值是0x4000,加上0xc是0x400c,即GOT[3],此处的值正好是0x1046。于是跳转到下一条的push $0x0指令处继续执行。

    400c:	46                   	inc    %esi
    400d:	10 00                	adc    %al,(%eax)
    400f:	00 56 10             	add    %dl,0x10(%esi)

1046处的指令将0压栈,然后跳转到0x1030处。

   00001030 <puts@plt-0x10>:
    1030:	ff b3 04 00 00 00    	push   0x4(%ebx)
    1036:	ff a3 08 00 00 00    	jmp    *0x8(%ebx)

GOT表中第0项是保存给动态链接器使用的,第1项是指向link map结构的指针,动态链接器将在这些结构中查找符号,第2项是指向_dl_runtime_resolver_的指针,即动态链接器。因为%ebx寄存器中的值是GOT的地址0x4000,push 0x4(%ebx)首先将GOT[1]压栈,然后调用GOT[2],即_dl_runtime_resolver_,动态链接器查找得到puts的真实地址,并回填到GOT[3]。这样,下次调用puts就可以直接从GOT[3]调用,不需要动态链接器再进行查找了。

由于学识所限,文章中必然存在问题。还请大家不吝赐教。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值