首页 > 代码库 > Android漫游记(3)---重定位之GOT & PLT & R_ARM_JUMP_SLOT

Android漫游记(3)---重定位之GOT & PLT & R_ARM_JUMP_SLOT

    Android系统的动态链接工具是/system/bin/linker(一般的Linux系统是ld.so),虽然名字不同,但是基本的动态链接过程是类似的。需要注意的一点是,Linux一般是Lazy,即所谓的“懒”加载方式,但是Android系统有点区别,是非Lazy方式,即所有的重定位操作,在进程首次执行以前已经全部完成。这大概也是Android应用首次启动比较慢的原因之一吧!

    关于Android系统的PLT和GOT可以写上一篇高考作为,在这里就不提概念性的东西了,网上有一篇博文:http://www.codeproject.com/Articles/70302/Redirecting-functions-in-shared-ELF-libraries ,这篇文章里是基于I386架构的,和Arm有所不同,但原理是一样的。如果有同学对下面的内容感兴趣,可以先去看看,把基本的东西先搞清楚,这样可能会顺利些,当然,最好了解些arm汇编~~~

    还是老习惯,我会直接写个小Demo也演示Android系统的Linker是如何处理重定位的,当然,ELF重定位的类型非常多,我们这里重点关注:R_ARM_JUMP_SLOT(386架构上叫R_386_JUMP_SLOT,很像,不是么微笑)。该类型重定位一般是针对外部函数引用的,即你要引用一个外部定义的函数符号,linker在加载你的应用的时候,就需要应用此种类型的重定位,来完成符号到真实函数的链接!

举例:比如你的应用需要打印一段文本到中断,你可能会调用libc.so这个C库的puts函数。这就是外部引用,那么在你的应用里就必定针对puts调用,存在一个R_ARM_JUMP_SLOT重定位信息。

先上一段非常小的程序:

/*
 *  PLT&GOT Resolver
 *  Created on: 2014-6
 *  Author: Chris.Z
 */
#include <stdio.h>
#include <stdlib.h>

/**
 * define the local method
 *
 */
 void local_method_call()
 {
     printf("[+]I'm local method.\n");
 }


int main()
{
    //printf("[+]PLT&GOT Resolver...\n");
    //local_method_call();
    //call puts to check r_arm_jump_slot
    puts("[+]call the method of puts from libc.\n");
    getchar();
    return 0;
}
这个程序没干其他事,就是调用了puts输出一段文本,这里的puts定义在libc.so库中。麻雀虽小,五脏俱全,我们验证下linker是怎么实现对于puts的“动态链接”的。

首先用readelf看下程序生成的二进制文件的elf内容(截取部分):


注意上图中的箭头所指,正是我们所说的R_ARM_JUMP_SLOT重定位,位于.rel.plt区。注意这行信息中的第一列Offset,值是0x9ff8,先记录下来,后面会用到。

我们实际动态运行下上面的小程序,同时用GDB附加到进程进行远程动态调试(GDB实在是Android系统级调试的一大神器!)。

由于我们要查看指令执行的细节,因此需要进行汇编级调试,进入gdb后,输入remote target :port后,进入GDB提示符,我们输入disas main来查看main函数的反汇编代码:

> disas main
Dump of assembler code for function main:
   0x00008378 <+0>:	push	{r3, r4, r11, lr}
   0x0000837c <+4>:	add	r11, sp, #12
=> 0x00008380 <+8>:	ldr	r4, [pc, #124]	; 0x8404 <main+140>,当前PC寄存器所指位置
   0x00008384 <+12>:	add	r4, pc, r4
   0x00008388 <+16>:	ldr	r3, [pc, #120]	; 0x8408 <main+144>
   0x0000838c <+20>:	add	r3, pc, r3
   0x00008390 <+24>:	mov	r0, r3
   0x00008394 <+28>:	bl	0x82b0 ;跳转到puts调用!!!<==============
   0x00008398 <+32>:	ldr	r3, [pc, #108]	; 0x840c <main+148>
   0x0000839c <+36>:	ldr	r3, [r4, r3]
   0x000083a0 <+40>:	ldr	r3, [r3, #4]
   0x000083a4 <+44>:	sub	r2, r3, #1
   0x000083a8 <+48>:	ldr	r3, [pc, #92]	; 0x840c <main+148>
   0x000083ac <+52>:	ldr	r3, [r4, r3]
   0x000083b0 <+56>:	str	r2, [r3, #4]
   0x000083b4 <+60>:	ldr	r3, [pc, #80]	; 0x840c <main+148>
   0x000083b8 <+64>:	ldr	r3, [r4, r3]
   0x000083bc <+68>:	ldr	r3, [r3, #4]
   0x000083c0 <+72>:	cmp	r3, #0
   0x000083c4 <+76>:	bge	0x83dc <main+100>
   0x000083c8 <+80>:	ldr	r3, [pc, #60]	; 0x840c <main+148>
   0x000083cc <+84>:	ldr	r3, [r4, r3]
   0x000083d0 <+88>:	mov	r0, r3
   0x000083d4 <+92>:	bl	0x82bc
   0x000083d8 <+96>:	b	0x83f8 <main+128>
   0x000083dc <+100>:	ldr	r3, [pc, #40]	; 0x840c <main+148>
   0x000083e0 <+104>:	ldr	r3, [r4, r3]
   0x000083e4 <+108>:	ldr	r3, [r3]
   0x000083e8 <+112>:	add	r2, r3, #1
   0x000083ec <+116>:	ldr	r3, [pc, #24]	; 0x840c <main+148>
   0x000083f0 <+120>:	ldr	r3, [r4, r3]
   0x000083f4 <+124>:	str	r2, [r3]
   0x000083f8 <+128>:	mov	r3, #0
   0x000083fc <+132>:	mov	r0, r3
   0x00008400 <+136>:	pop	{r3, r4, r11, pc}
   0x00008404 <+140>:	andeq	r1, r0, r8, asr r12
   0x00008408 <+144>:	andeq	r0, r0, r12, lsr #1
   0x0000840c <+148>:			; <UNDEFINED> instruction: 0xfffffffc
End of assembler dump.
注意我上面的红色部分注释,那个BL指令就是跳转到puts(当然下面会看到,实际上是先跳转到了plt区)。我们执行b *0x8394,在BL指令处下断点,然后c命令继续执行。

然后我们执行x/i $pc查看当前指令是不是执行到我们的断点:

> display/i $pc
> x/i $pc
=> 0x8394 <main+28>:	bl	0x82b0
OK,确实在0x8394断下,到这里,我们停止继续执行,来检查下几个重要信息。

首先我们看下0x00008394 <+28>: bl0x82b0这行汇编,它的意思是带链接跳转到0x82b0执行,那么0x82b0究竟有什么指令呢?

执行disas 0x82b0,0x82c0,输出如下:

> disas 0x82b0,0x82c0
Dump of assembler code from 0x82b0 to 0x82c0:
   0x000082b0:	add	r12, pc, #0
   0x000082b4:	add	r12, r12, #4096	; 0x1000
   0x000082b8:	ldr	pc, [r12, #3392]!	; 0xd40 跳转到libc.so puts入口 <span style="font-family: Arial, Helvetica, sans-serif;"><==============</span>   0x000082bc:	add	r12, pc, #0
End of assembler dump.
前面的两条指令我们忽略过去(主要是计算.got偏移),我们直接执行到0x82b8,即上标的红色指令。

简单解释指令含义:讲r12+0xd40所指向的内存字加载到PC寄存器,实际上就是跳转到那个地址。

我们看看r12+0xd40此时的值是什么,执行p/x $r12+0xd40:

1: x/i $pc
=> 0x82b8:	ldr	pc, [r12, #3392]!	; 0xd40
> p/x $r12+0xd40
$1 = 0x9ff8
看到了吗,这个0x9ff8正是我们上面记录的那个地址!

我们在执行info symbol 0x9ff8命令:

> info symbol 0x9ff8
_GLOBAL_OFFSET_TABLE_ + 20 in section .got

到这里,我们做个简单小结:

elf文件的.rel.plt区里,针对R_ARM_JUMP_SLOT类型的重定位,其offset的内容就是.got区该符号的地址,即位于GOT表基址偏移20个字节(本例中的GOT表基址为0x9fe4)。
这里要说明的是,可执行和.so动态链接库的计算方式有所差别,.so需要加上加载时的模块基址。

下面,我们在看看.got的0x9ff8里是什么内容:

执行p/x *0x9ff8:

> p/x *0x9ff8
$2 = 0x4011a7dc         puts真实的入口地址<====================
我们在对照下进程的maps:



正是libc.so所在的地址,实际上就是puts的函数入口地址!好了,今天就写到这里,Enjoy IT!微笑

转载请注明出处:生活秀