linux got分析,Linux动态链接之GOT与PLT

我们知道函数名就是一个内存地址,这个地址指向函数的入口。调用函数就是压入参数,保存返回地址,然后跳转到函数名指向的代码。问题是,如果函数在共享库中,共享库加载的地址本身就不确定,函数地址也就不确定了,那如何调用共享库中的函数呢?这就是本文要回答的。

我们先来看一小段代码(test.c):

34543528_1.gif

#include

void hello_world(void)

{

printf("Hello world!\n");

return;

}

int main(int argc, char* argv[])

{

hello_world();

return 0;

}

34543528_1.gif

编译并反汇编:

gcc -g test.c -o test

objdump -S test

34543528_1.gif

void hello_world(void)

{

80483b4: 55 push %ebp

80483b5: 89 e5 mov %esp,%ebp

80483b7: 83 ec 08 sub $0x8,%esp

printf("Hello world!\n");

80483ba: c7 04 24 b4 84 04 08 movl $0x80484b4,(%esp)

80483c1: e8 2a ff ff ff call 80482f0

return;

}

80483c6: c9 leave

80483c7: c3 ret

080483c8 :

int main(int argc, char* argv[])

{

80483c8: 8d 4c 24 04 lea 0x4(%esp),%ecx

80483cc: 83 e4 f0 and $0xfffffff0,%esp

80483cf: ff 71 fc pushl -0x4(%ecx)

80483d2: 55 push %ebp

80483d3: 89 e5 mov %esp,%ebp

80483d5: 51 push %ecx

80483d6: 83 ec 04 sub $0x4,%esp

hello_world();

80483d9: e8 d6 ff ff ff call 80483b4

return 0;

80483de: b8 00 00 00 00 mov $0x0,%eax

}

34543528_1.gif

调用hello_world时,汇编代码对应于call   80483b4 ,这是个绝对地址。hello_world是在可执行文件中,可执行文件是加载到一个固定地址的,因此hello_world的地址是确定的。

调用printf时,汇编代码对应于call   80482f0 ,这是个绝对地址。但函数名却是puts@plt,这是怎么回事呢?puts@plt显然是编译器加的一个中间函数,我们看一下这个函数对应的汇编代码:

34543528_1.gif

080482f0 :

80482f0: ff 25 2c 96 04 08 jmp *0x804962c

80482f6: 68 10 00 00 00 push $0x10

80482fb: e9 c0 ff ff ff jmp <_init>

34543528_1.gif

现在我们用调试器分析一下:

gdb test

(gdb) b main

Breakpoint 1 at 0?80483d9: file test.c, line 12.

(gdb) r

Starting program: /root/test/plt/test

Breakpoint 1, main () at test.c:12 12 hello_world();

puts@plt先跳到*0?804962c,我们看看*0?804962c里有什么?

(gdb) x 0?804962c 0?804962c <_global_offset_table_>:   0?080482f6

*0?804962c等于0?080482f6,这正是puts@plt中的第二行汇编代码的地址。也就是说puts@plt整个函数会顺序执行,直到跳转到0?80482c0.

再来看看0?80482c0处有什么,通过汇编可以看到:

ff 25 20 96 04 08 jmp    *0?8049620

又跳到了*0?8049620,转的弯真多,没关系,我们再看*0?8049620:

(gdb) x 0?8049620 0?8049620 <_global_offset_table_>:    0?009ce4c0

(gdb) x /wa 0?009ce4c0 0?9ce4c0 <_dl_runtime_resolve>: 0?8b525150

原来转来转去就是为了调用函数_dl_runtime_resolve, _dl_runtime_resolve的功能就是找到要调用函数(puts)的地址。

为什么不直接调用_dl_runtime_resolve,而要转这么多圈子呢?

先执行完这个函数hello_world:

(gdb) n

再回头来看看puts@plt的第一行代码:

80482f0:   ff 25 2c 96 04 08 jmp    *0?804962c

(gdb) x 0?804962c 0?804962c <_global_offset_table_>:   0xa39a60

对比前面的:

(gdb) x 0?804962c 0?804962c <_global_offset_table_>:   0?080482f6

也就是说第一次执行时,通过_dl_runtime_resolve解析到函数地址,并保存puts的地址到0?804962c里,以后执行时就直接调用了。

--------------------------------------------

34543528_2.png

/*如果是第一次的函数调用,它所走的路线就是我在上图中用红线标出的,而要是在第二次以后调用,那就是蓝线所标明的。*/

最后我们讨论ELF文件的动态连接机制。每一个外部定义的符号在全局偏移表 (Global Offset Table GOT)中有相应的条目,如果符号是函数则在过程连接表(Procedure Linkage Table PLT)中也有相应的条目,且一个PLT条目对应一个GOT条目。对外部定义函数解析可能是整个ELF文件规范中最复杂的,下面是函数符号解析过程的一个 描述。

1:代码中调用外部函数func,语句形式为call 0xaabbccdd,地址0xaabbccdd实际上就是符号func在PLT表中对应的条目地址(假设地址为标号.PLT2)。

2:PLT表的形式如下

.PLT0: pushl          4(%ebx)           /* GOT表的地址保存在寄存器ebx中 */

jmp            *8(%ebx)

nop; nop

nop; nop

.PLT1: jmp            *name1@GOT(%ebx)

pushl          $offset

jmp            .PLT0@PC

.PLT2: jmp            *func@GOT(%ebx)

pushl          $offset

jmp            .PLT0@PC

3:查看标号.PLT2的语句,实际上是跳转到符号func在GOT表中对应的条目。

4:在符号没有重定位前,GOT表中此符号对应的地址为标号.PLT2的下一条语句,即是pushl $offset,其中$offset是符号func的重定位偏移量。注意到这是一个二次跳转。

5:在符号func的重定位偏移量压栈后,控制跳到PLT表的第一条目(.PLT0),把GOT[1]的内容(放置了用来标识特定库的代码)压栈,并跳转到GOT[2]对应的地址。

6:GOT[2]对应的实际上是动态符号解析函数的代码,在对符号func的地址解析后,会把func在内存中的地址设置到GOT表中此符号对应的条目中。

7:当第二次调用此符号时,GOT表中对应的条目已经包含了此符号的地址,就可直接调用而不需要利用PLT表进行跳转。

动态连接是比较复杂的,但为了获得灵活性的代价通常就是复杂性。其最终目的是把GOT表中条目的值修改为符号的真实地址,这也可解释节.got包含在可读可写段中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值