****************************************
linux 下动态库函数调用--反汇编知识。
author: hjjdebug
date: 2016年 09月 07日 星期三 14:41:49 CST
****************************************
#include #include 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 函数框架。 ---------------------------------------- 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 ---------------------------------------- 3. 如何访问常量数据?例如: "hello android" ---------------------------------------- printf("hello android\n"); 2bc: e59f3090 ldr r3, [pc, #144] ; 354 2c0: e08f3003 add r3, pc, r3 2c4: e1a00003 mov r0, r3 2c8: ebffffe9 bl 274 ...... 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.... ---------------------------------------- 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 通过frame pointer 可以获得局部变量的地址。 ---------------------------------------- 5. 如何调用动态链接函数, 找到函数入口地址。 ---------------------------------------- 还以printf 为例, printf 是c 库函数, 编译期并不知道其地址。 我们不看354了, 看bl 274 2bc: e59f3090 ldr r3, [pc, #144] ; 354 2c0: e08f3003 add r3, pc, r3 2c4: e1a00003 mov r0, r3 2c8: ebffffe9 bl 274 .... 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 .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 .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过程连接表,属于微代码区,每项由3行代码组成,用于从.got表中取得数据,并跳转执行。 ---------------------------------------- 扩展知识: ---------------------------------------- 可重定位代码(windows->dll) 和 位置无关代码(linux->so) 可重定位代码:(windows) 生成动态库时假定它被加载在地址 0 处。加载时它会被加载到一个地址(base), 这时要根据代码重定位(relocation)信息,对代码进行定址,so 才能正确寻址。 缺点: 不同的进程会把so加载到不同的地址, 而这些地址是不同的,重定位后的代码也是不同的, 所以这些代码没有办法共享。内存中有多份。 这样失掉了共享库的优势,跟不共享没多少差别。 除非进程能把共享库加载到同一个地址(但这个要求是过分的). 位置无关代码:(linux) linux so文件使用 -fPIC 来生成位置无关代码。这些代码可以被加载到内存的任何位置都可以运行。 怎样做到? 不管是程序地址还是数据地址,都是通过pc值加上一个偏移量来获得,就实现了位置无关。 例如访问外部函数,将外部函数地址全部放入.got table, 通过 [pc+offset] 获取。 优点: 虽然不同的进程会把so 映射到不同的地址空间,但操作系统将把它们映射到相同的物理地址, 节省了代码空间。 缺点: 代码执行效率上有一点损失。 但是,没有了重定位,加载也变快了。