NDK开发、Native Hook、Android性能优化必知:动态链接
The following article is from 程序员江同学 Author 程序员江同学
最近在读《程序员的自我修养:链接,装载与库》,其实这本书跟 Android 开发的联系还挺紧密的,无论是 NDK 开发,或者是性能优化中一些常用的 Native Hook 手段,都需要了解一些链接,装载相关的知识点。本文为读书笔记。
空间浪费问题
版本更新问题
动态链接是什么?
动态链接将程序链接的过程由装载前推迟到了装载时,这当然会带来一些性能上的损失,但也可以通过延迟绑定(Lazy Binding)等方式进行优化,使性能损失达到最小。据估算,动态链接与静态链接相比,性能损失大约在5%以下。当然经过实践的证明,这点性能损失用来换取程序在空间上的节省和程序构建和升级时的灵活性,是相当值得的。
/** Program1.c **/
#include "Lib.h"
int main()
{
foobar(1);
return 0;
}
/** Program2.c **/
#include "Lib.h"
int main()
{
foobar(2);
return 0;
}
/** Lib.c **/
#include <stdio.h>
void foobar(int i)
{
printf("Printing from Lib.so %d\n", i);
sleep(-1);
}
/** Lib.h **/
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif
gcc -fPIC -shared -o Lib.so Lib.c // -shared 表示生成动态共享库,-fPIC 后面再解释
gcc -o Program1 Program1.c ./Lib.so // 编译链接生成 Program1
gcc -o Program2 Program2.c ./Lib.so // 编译链接生成 Program2
动态链接地址空间分布
$ ./Program1 &
[3] 11067
Printing from Lib.so 1
$ cat /proc/11067/maps
55b2e50da000-55b2e50db000 r--p 00000000 07:03 126060 /workspaces/programmer-training/Program1
55b2e50db000-55b2e50dc000 r-xp 00001000 07:03 126060 /workspaces/programmer-training/Program1
55b2e50dc000-55b2e50dd000 r--p 00002000 07:03 126060 /workspaces/programmer-training/Program1
55b2e50dd000-55b2e50de000 r--p 00002000 07:03 126060 /workspaces/programmer-training/Program1
55b2e50de000-55b2e50df000 rw-p 00003000 07:03 126060 /workspaces/programmer-training/Program1
55b2e558d000-55b2e55ae000 rw-p 00000000 00:00 0 [heap]
7f7d4f34a000-7f7d4f34d000 rw-p 00000000 00:00 0
7f7d4f34d000-7f7d4f36f000 r--p 00000000 00:2e 525226 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7d4f36f000-7f7d4f4e7000 r-xp 00022000 00:2e 525226 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7d4f4e7000-7f7d4f535000 r--p 0019a000 00:2e 525226 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7d4f535000-7f7d4f539000 r--p 001e7000 00:2e 525226 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7d4f539000-7f7d4f53b000 rw-p 001eb000 00:2e 525226 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f7d4f53b000-7f7d4f53f000 rw-p 00000000 00:00 0
7f7d4f549000-7f7d4f54a000 r--p 00000000 07:03 126046 /workspaces/programmer-training/Lib.so
7f7d4f54a000-7f7d4f54b000 r-xp 00001000 07:03 126046 /workspaces/programmer-training/Lib.so
7f7d4f54b000-7f7d4f54c000 r--p 00002000 07:03 126046 /workspaces/programmer-training/Lib.so
7f7d4f54c000-7f7d4f54d000 r--p 00002000 07:03 126046 /workspaces/programmer-training/Lib.so
7f7d4f54d000-7f7d4f54e000 rw-p 00003000 07:03 126046 /workspaces/programmer-training/Lib.so
7f7d4f54e000-7f7d4f550000 rw-p 00000000 00:00 0
7f7d4f550000-7f7d4f551000 r--p 00000000 00:2e 525204 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7d4f551000-7f7d4f574000 r-xp 00001000 00:2e 525204 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7d4f574000-7f7d4f57c000 r--p 00024000 00:2e 525204 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7d4f57d000-7f7d4f57e000 r--p 0002c000 00:2e 525204 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7d4f57e000-7f7d4f57f000 rw-p 0002d000 00:2e 525204 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f7d4f57f000-7f7d4f580000 rw-p 00000000 00:00 0
7ffc08748000-7ffc0876a000 rw-p 00000000 00:00 0 [stack]
7ffc087b5000-7ffc087b9000 r--p 00000000 00:00 0 [vvar]
7ffc087b9000-7ffc087bb000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
Lib.so 与 Program1 一样,它们都是被操作系统用同样的方法映射至进程的虚拟地址空间,只是它们占据的虚拟地址和长度不同。 Program1 除了使用 Lib.so 以外,它还用到了动态链接形式的 C 语言运行库 libc-2.31.so。 另外还有一个很值得关注的共享对象就是ld-2.31.so,它实际上是Linux下的动态链接器。动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行Program1 之前,首先会把控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给 Program1,然后开始执行。
$ readelf -l Lib.so
Elf file type is DYN (Shared object file)
Entry point 0x1080
There are 11 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000560 0x0000000000000560 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x000000000000017d 0x000000000000017d R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x00000000000000dc 0x00000000000000dc R 0x1000
LOAD 0x0000000000002e10 0x0000000000003e10 0x0000000000003e10
0x0000000000000220 0x0000000000000228 RW 0x1000
DYNAMIC 0x0000000000002e20 0x0000000000003e20 0x0000000000003e20
0x00000000000001c0 0x00000000000001c0 RW 0x8
Section to Segment mapping:
Segment Sections...
00 .note.gnu.property .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
01 .init .plt .plt.got .plt.sec .text .fini
02 .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .dynamic .got .got.plt .data .bss
那么问题来了,为什么要这样做,为什么不将每个共享对象在进程中的地址固定?
固定装载地址的问题
装载时重定位
地址无关代码
第一种是模块内部的函数调用、跳转等。 第二种是模块内部的数据访问,比如模块中定义的全局变量、静态变量。 第三种是模块外部的函数调用、跳转等。 第四种是模块外部的数据访问,比如其他模块中定义的全局变量。
#include "pic.h"
static int a;
extern int b;
extern void ext();
void bar()
{
a = 1; // 模块内数据访问
b = 2; // 模块间数据访问
}
void main()
{
bar(); // 模块内指令调用
ext(); // 模块间指令调用
}
类型一 模块内部调用或跳转
类型二 模块内部数据访问
类型三 模块间数据访问
类型四 模块间调用、跳转
小结
动态链接下对于全局和静态的数据访问都要进行复杂的 GOT 定位,然后间接寻址,对于模块间的调用也要先定位GOT,然后再进行间接跳转。 动态链接的链接工作在运行时完成,即程序开始执行时,动态链接器会寻找并装载所需要的共享对象,然后进行符号查找地址重定位等工作。
延迟绑定
PLT 基本原理
bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dl_runtime_resolve
bar@plt 的第一条指令是一条通过 GOT 间接跳转的指令。bar@GOT 表示 GOT 中保存 bar() 这个函数相应的项。如果链接器在初始化阶段已经初始化该项,并且将 bar() 的地址填入该项,那么这个跳转指令就会跳转到 bar(), 实现函数正确调用。但是为了实现延迟绑定,链接器在初始化阶段并没有将 bar() 的地址填入到该项,那么这条指令相当于没有进行任何操作,继续下面的指令。 第二条指令将一个数字 n 压入堆栈中,这个数字是 bar 这个符号引用在重定位表“.rel.plt”中的下标。 接着又是一条push指令将模块的ID压入到堆栈。 最后跳转到 _dl_runtime_resolve:这条指令先将所需要决议符号的下标压入堆栈,再将模块 ID 压入堆栈,然后调用动态链接器的 _dl_runtime_resolve() 函数来完成符号解析和重定位工作 。_dl_runtime_resolve() 在进行一系列工作以后将 bar() 的真正地址填入到 bar@GOT 中。 一旦 bar() 这个函数被解析完毕,当我们再次调用 bar@plt 时,第一条jmp指令就能够跳转到真正的 bar() 函数中, bar() 函数返回的时候会根据堆栈里面保存的 EIP 直接返回到调用者,而不会再继续执行后面的指令。
PLT 具体实现
第一项保存的是“.dynamic”段的地址,这个段描述了本模块动态链接相关的信息。 第二项保存的是本模块的ID。 第三项保存的是 _dl_runtime_resolve() 的地址。
PLT0:
push *(GOT + 4)
jump *(GOT + 8)
...
bar@plt:
jmp *(bar@GOT)
push n
jump PLT0 ”
.intrp 段
$ objdump -s PicTest
PicTest: file format elf64-x86-64
Contents of section .interp:
0318 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0328 7838362d 36342e73 6f2e3200 x86-64.so.2.
.dynamic 段
$ readelf -d pic.so
Dynamic section at offset 0x2e18 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x1140
0x0000000000000019 (INIT_ARRAY) 0x3e08
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3e10
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x2f0
0x0000000000000005 (STRTAB) 0x3d8
0x0000000000000006 (SYMTAB) 0x318
0x000000000000000a (STRSZ) 120 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x4000
0x0000000000000002 (PLTRELSZ) 24 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x540
0x0000000000000007 (RELA) 0x480
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x460
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x450
0x000000006ffffff9 (RELACOUNT) 3
$ ldd PicTest
linux-vdso.so.1 (0x00007ffcfc9bd000)
./pic.so (0x00007f4fc64b5000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4fc62b9000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4fc64c1000)
动态符号表
$ readelf -s pic.so
Symbol table '.dynsym' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
6: 0000000000001119 39 FUNC GLOBAL DEFAULT 14 ext
7: 0000000000004028 4 OBJECT GLOBAL DEFAULT 24 b
动态链接重定位表
$ readelf -r Lib.so
Relocation section '.rela.dyn' at offset 0x488 contains 7 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000003e10 000000000008 R_X86_64_RELATIVE 1130
000000003e18 000000000008 R_X86_64_RELATIVE 10f0
000000004028 000000000008 R_X86_64_RELATIVE 4028
000000003fe0 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000003fe8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003ff0 000400000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000003ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x530 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000004018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000004020 000500000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
R_X86_64_JUMP_SLO: 该类型表示被修正的位置只需要直接填入符号的地址即可,它所修正的位置位于“.got.plt” R_X86_64_GLOB_DAT: 与 R_X86_64_JUMP_SLO 类似,表示被修正的位置只需要直接填入符号的地址即可,但是它所修正的位置位于“.got” R_X86_64_RELATIVE: 这种类型的重定位实际上就是基址重置,适用于共享对象的数据段这种无法做到地址无关的符号,它可能会包含绝对地址的引用,对于这种绝对地址的引用,我们必须在装载时将其重定位。
动态链接器自举
装载共享对象
完成基本自举以后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,我们可以称它为全局符号表(Global Symbol Table)。 “.dynamic”段中,有一种类型的入口是DT_NEEDED,它所指出的是该可执行文件(或共享对象)所依赖的共享对象,由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中。 链接器开始从集合里取出一个所需要的共享对象的名字,找到相应的文件后打开该文件,读取相应的ELF文件头和“.dynamic”段,然后将它相应的代码段和数据段映射到进程空间中。 如果这个ELF共享对象还依赖于其他共享对象,那么将所依赖的共享对象的名字放到装载集合中。如此循环直到所有依赖的共享对象都被装载进来为止。 这个装载顺序类似图的遍历过程,链接器可能会使用深度优先或者广度优先或者其他的顺序来遍历整个图。 当一个新的共享对象被装载进来的时候,它的符号表会被合并到全局符号表中,所以当所有的共享对象都被装载进来的时候,全局符号表里面将包含进程中所有的动态链接所需要的符号。
符号的优化级
“/* a1.c */
#include <stdio.h>
void a()
{
printf("a1.c\n");
}
/* a2.c */
#include <stdio.h>
void a()
{
printf("a2.c\n");
}
/* b1.c */
void a();
void b1()
{
a();
}
/* b2.c */
void a();
void b2()
{
a();
}
全局符号介入与地址无关代码
重定位与初始化
当完成了重定位和初始化之后,所有的准备工作就宣告完成了,所需要的共享对象也都已经装载并且链接完成了,这时候动态链接器的工作也就完成了,将进程的控制权转交给程序的入口并且开始执行。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!