linux 下动态库函数调用--反汇编知识(arm)。

来源:互联网 发布:wifi限制网速软件 编辑:程序博客网 时间:2024/06/15 17:43
****************************************
linux 下动态库函数调用--反汇编知识。
author: hjjdebug
date:   2016年 09月 07日 星期三 14:41:49 CST

****************************************

#include <stdio.h>
#include <string.h>
void test_so()
{
    char buffer[256];
    printf("---------- hello android ------------\n");
    FILE * fp=fopen("1.txt","rt");
    if(fp)
    {
        memset(buffer,0,sizeof(buffer));
        fread(buffer,sizeof(buffer),1,fp);
        printf("%s\n",buffer);
        fclose(fp);
    }    
    else
    {
        printf("error open 1.txt\n");
    }
}
----------------------------------------

1. c 函数框架。

以下分析代码为arm 指令, 由arm-linux-androideabi-gcc  编译

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

c 函数会使用堆栈,故而会使用sp寄存器, 但函数执行完后要恢复sp寄存器,
所以框架通常会保留原来的寄存器,我们把它叫old-sp寄存器。 arm 用fp寄存器
充当这个角色。 下面是它的框架示意: frame pointer 在函数生命期内其值是不变的。
 2b0:    e92d4800     push    {fp, lr}
 2b4:    e28db004     add    fp, sp, #4
 2b8:    e24ddf42     sub    sp, sp, #264    ; 0x108
......

 34c:    e24bd004     sub    sp, fp, #4
 350:    e8bd8800     pop    {fp, pc}

----------------------------------------
2.如何调用其它函数, 参数传递方法。
----------------------------------------
arm 的传参,小于4个的用寄存器, r0,r1,r2,r3. 大于4个用堆栈。

返回值用r0

这个变化,相比x86用堆栈传参,区别是参数不在栈中了, 返回地址也不在栈中了,

函数调用栈不明显了。函数调用参数也不明显了。 好处是不花力气维护栈了。 逆向会稍困难一点。

由于r0 做返回值,r0,r1,r2,r3 常用寄存器比较忙, 如果需要保存函数参数,需把r0,r1,r2,r3 保存到别处,包括栈内存或高位寄存器。

所以你会经常见到r0,r1,r2,r3 等(尤其是r0)寄存器传来传去。


----------------------------------------
3. 如何访问常量数据?例如: "hello android"
----------------------------------------
  printf("hello android\n");

 2bc:    e59f3090     ldr    r3, [pc, #144]    ; 354 <test_so+0xa4>
 2c0:    e08f3003     add    r3, pc, r3
 2c4:    e1a00003     mov    r0, r3
 2c8:    ebffffe9     bl    274 <test_so-0x3c>
......

 354:    0000009c     .word    0x0000009c
 358:    000000b4     .word    0x000000b4
 35c:    000000b0     .word    0x000000b0
 360:    00000050     .word    0x00000050
在程序区造一个表, 表中存放数据地址,这个地址与当前pc值相加才是真实数据地址。
下面看看数据段内容:
pc值2c0+9c = 36c 正好是"hello android"的的地址

Contents of section .rodata:
 0364 2d2d2d2d 2d2d2d2d 2d2d2068 656c6c6f  ---------- hello
 0374 20616e64 726f6964 202d2d2d 2d2d2d2d   android -------
 0384 2d2d2d2d 2d000000 312e7478 74000000  -----...1.txt...
 0394 72740000 6572726f 72206f70 656e2031  rt..error open 1

 03a4 2e747874 00000000                    .txt.... 

这种访问全局变量的技巧,对thumb 指令尤其重要。 thumb 指令是16为, 而全局变量地址是32位,

如何加载32位地址到寄存器中? 那就是在程序区见一个数据表,用16位指令可以从手边的数据表中取到地址,

这个地址加上pc 值构成真正的数据地址(位置无关),然后从真实地址中再拿到数据,这样就访问了全局变量。   


----------------------------------------
4. 如何访问局部变量?
----------------------------------------
局部变量是保存在堆栈中的,函数退出即丢弃。
例如:        memset(buffer,0,sizeof(buffer));
 2f8:    e24b3f42     sub    r3, fp, #264    ; 0x108
 2fc:    e1a00003     mov    r0, r3
 300:    e3a01000     mov    r1, #0
 304:    e3a02c01     mov    r2, #256    ; 0x100
 308:    ebffffdf     bl    28c <test_so-0x24>
通过frame pointer 可以获得局部变量的地址。

----------------------------------------
5. 如何调用动态链接函数, 找到函数入口地址。
----------------------------------------
还以printf 为例, printf 是c 库函数, 编译期并不知道其地址。
我们不看354了(字符串地址表), 看bl 274(过程连接表)

 2bc:    e59f3090     ldr    r3, [pc, #144]    ; 354 <test_so+0xa4>
 2c0:    e08f3003     add    r3, pc, r3
 2c4:    e1a00003     mov    r0, r3
 2c8:    ebffffe9     bl    274 <test_so-0x3c>
....
274 处是另外一个表项,该表名称叫plt (procedure leakage table)
翻译为过程连接表
00000260 <.plt>:
 260:    e52de004     .word    0xe52de004
 264:    e59fe004     .word    0xe59fe004
 268:    e08fe00e     .word    0xe08fe00e
 26c:    e5bef008     .word    0xe5bef008
 270:    000011d4     .word    0x000011d4
 274:    e28fc600     .word    0xe28fc600
 278:    e28cca01     .word    0xe28cca01
 27c:    e5bcf1d4     .word    0xe5bcf1d4
 280:    e28fc600     .word    0xe28fc600
 284:    e28cca01     .word    0xe28cca01
 288:    e5bcf1cc     .word    0xe5bcf1cc
过程连接表是小微代码区,看看ida-pro中的显示,如下:

.plt:00000274 ; =============== S U B R O U T I N E =======================================
.plt:00000274
.plt:00000274 ; Attributes: thunk
.plt:00000274
.plt:00000274 ; int puts(const char *s)
.plt:00000274 puts                                    ; CODE XREF: test_so+18p
.plt:00000274                                         ; test_so+7Cp ...
.plt:00000274                 ADR             R12, 0x27C
.plt:00000278                 ADD             R12, R12, #0x1000
.plt:0000027C                 LDR             PC, [R12,#(puts_ptr - 0x127C)]! ; __imp_puts

.plt:0000027C ; End of function puts

解释: 这个ADR R12,0x27c, 真实指令是 add ip, pc, #const, 就是说是基于pc 值的位置无关代码。

结果是从某一固定偏移的内存中取到一个数值付给PC.

对应此例,这个偏移是#puts_ptr -> 1450, 里面存的数要靠连接器加载后填入数据才能得到, 这个数据是puts 的地址。

加载器是根据导入表信息填写这个地址的。

.plt:0000027C
.plt:00000280
.plt:00000280 ; =============== S U B R O U T I N E =======================================
.plt:00000280
.plt:00000280 ; Attributes: thunk
.plt:00000280
.plt:00000280 ; FILE *fopen(const char *filename, const char *modes)
.plt:00000280 fopen                                   ; CODE XREF: test_so+34p
.plt:00000280                 ADR             R12, 0x288
.plt:00000284                 ADD             R12, R12, #0x1000
.plt:00000288                 LDR             PC, [R12,#(fopen_ptr - 0x1288)]! ; __imp_fopen
.plt:00000288 ; End of function fopen

274处是一个三条指令组成的小代码区。它首先计算出一个地址,这个地址在数据区,然后从那里取得数值,
转去执行。
可以感受到,取到的地址就是动态连接库的函数地址。这个地址要由加载器把数值填充好。
这个表位于一个数据区内,叫.got 区(global offset table). 显然,等价于pe 格式文件的导入地址表。

extern:00001468 ; Segment type: Externs
extern:00001468 ; int puts(const char *s)
extern:00001468                 IMPORT __imp_puts       ; CODE XREF: puts+8j
extern:00001468                                         ; DATA XREF: .got:puts_ptro
extern:0000146C ; FILE *fopen(const char *filename, const char *modes)
extern:0000146C                 IMPORT __imp_fopen      ; CODE XREF: fopen+8j
extern:0000146C                                         ; DATA XREF: .got:fopen_ptro

----------------------------------------
由此总结一下,动态跳转的过程是:
----------------------------------------
1. 加载器把外部so文件的调用函数地址都填充好了, 这个导入地址表叫.got 全局偏移表。
2. 有一个plt过程连接表,属于微代码区,每项由很短的代码组成,用于从.got表中取得数据,并跳转执行。


----------------------------------------
扩展知识:
----------------------------------------
可重定位代码(windows->dll) 和 位置无关代码(linux->so)
可重定位代码:(windows)
生成动态库时假定它被加载在地址 0 处。加载时它会被加载到一个地址(base),
这时要根据代码重定位(relocation)信息,对代码进行定址,dll 才能正确寻址。
缺点: 不同的进程会把dll加载到不同的地址, 而这些地址是不同的,重定位后的代码也是不同的,
所以这些代码没有办法共享。内存中有多份。 这样失掉了共享库的优势,跟不共享没多少差别。
除非进程能把共享库加载到同一个地址(但这个要求是过分的).

位置无关代码:(linux)
linux so文件使用 -fPIC 来生成位置无关代码。这些代码可以被加载到内存的任何位置都可以运行。
怎样做到?
不管是程序地址还是数据地址,都是通过pc值加上一个偏移量来获得,就实现了位置无关。
例如访问外部函数,将外部函数地址全部放入.got table, 通过 [pc+offset] 获取。
优点: 虽然不同的进程会把so 映射到不同的地址空间,但操作系统将把它们映射到相同的物理地址,
节省了代码空间。
缺点: 代码执行效率上有一点损失。 但是,没有了重定位,加载也变快了。


0 0
原创粉丝点击