动态库重定向
每个进程都能够具有一个独立的虚拟地址空间,所以关于可履行文件,他能够有一个固定的虚拟基地址,可是关于动态库,为每个动态库区分固定的虚拟地址规模会十分费事,而且或许无法做到:比方库的巨细变了、要添加新的库、某个库抛弃不用了,等等状况都要从头区分。别的当以相似插件的形式运行时要添加一个新库时,无法找到一个固定可用的虚拟地址规模。因为无法预知所以无法事先预留,因而需求动态链接器在运行时进行重定向。本文后面评论的都是针对position independent code
。
GOT
GOT表简介
GOT:Global Offset Table,是用于拜访大局变量的。
GOT表中存的是变量的虚拟地址,当拜访大局变量时,会先从相应got表项中获取变量的地址,然后再从该地址中读出值,或许向该地址中写入新值。
引进GOT表的优缺点
优点
- 引进got表后,重定向时,文本段不需求修正,因而文本段能够在多个进程中同享,可削减内存运用
- 引进got表后,可明显削减重定位项的数目(假如是对文本段重定位,每处拜访变量的当地都需求一个重定位项,而引进got表后,仅每个方针变量一个重定位项),可削减动态衔接器重定位的耗时
缺点
- 拜访变量时多了一次直接操作(需求先从got项中加载变量的地址),速度稍有影响
看个拜访外部库变量的比如
object file 中文本段重定向
extern int value;
int readValue() {
return value;
}
反编译后的指令:
Disassembly of section .text:
0000000000000000 <readValue>:
0: 90000000 adrp x0, 0
4: f9400000 ldr x0, [x0]
8: b9400000 ldr w0, [x0]
c: d65f03c0 ret
- 第一条和第二条指令用于加载
value
变量的地址到x0
寄存器中。(补白:因为 aarch64 是定长指令集,每条指令固定4字节,运用这两条指令,能够拜访PC +/-4GB的规模) - 第三条指令从
x0
所表示的地址中读出数据放到w0
寄存器中。(补白:int是32位,所以保存到w0
,x0
的高32位会自动清0)
上面第一条,第二条加载地址的指令是“不完整”的,实践状况应该是:
adrp x0, pageAddr
ldr x0, [x0, pageOffset]
可是因为外部符号value
的地址在其库运行时被加载后才干确认,编译时是无法知晓的,因而编译的时分pageAddr
和 pageOffset
都留空(填0),等待重定向,能够看下重定向信息:
Relocation section '.rela.text' at offset 0x1e0 contains 2 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000000 0000000b00000137 R_AARCH64_ADR_GOT_PAGE 0000000000000000 value + 0
0000000000000004 0000000b00000138 R_AARCH64_LD64_GOT_LO12_NC 0000000000000000 value + 0
从offset能够看到这两条重定位项分别是针对上面第一条、第二条指令的。
关于文本段(.text section)重定向很直接,也好了解,可是这意味着会改动文本段,那么就不能在多个进程中同享了,内存占用会添加。
动态库GOT表重定向
将上面的 object file(.o)链接(program linker,差异于dynamic linker)一下,反编译看看:
Disassembly of section .text:
0000000000000238 <readValue>:
238: f00000e0 adrp x0, 1f000
23c: f947f000 ldr x0, [x0, #4064]
240: b9400000 ldr w0, [x0]
244: d65f03c0 ret
早年两条指令能够看到是从相对pc 0x1ffe0
的方位读取value
的地址,来看下重定向信息:
Relocation section '.rela.dyn' at offset 0x220 contains 1 entry:
Offset Info Type Symbol's Value Symbol's Name + Addend
000000000001ffe0 0000000200000401 R_AARCH64_GLOB_DAT 0000000000000000 value + 0
从上面重定位项能够看到0x1ffe0
处重定向后正是存入value
的地址,一起文本段不需求重定位。从section header table
中能够看出0x1ffe0
坐落.got
section。
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
...
[ 9] .got PROGBITS 000000000001ffd8 00ffd8 000010 08 WA 0 0 8
...
从上面可知.got section
是由链接器(program linker)生成的,那么链接器是怎样知道要创立 got 项的呢?回忆一下上面 object file 的重定位信息:
Relocation section '.rela.text' at offset 0x1e0 contains 2 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000000 0000000b00000137 R_AARCH64_ADR_GOT_PAGE 0000000000000000 value + 0
能够看到object file中有一个R_AARCH64_ADR_GOT_PAGE
类型的重定位项,正是这个类型的重定位项奉告链接器要创立相应的 got 项,以及对应该 got 项的 R_AARCH64_GLOB_DAT
类型的重定位项。
PLT
PLT表简介
PLT(Procedure Linkage Table),用于调用非static函数的。PLT中的代码也会用到GOT表,对应于PLT的GOT表通常会独自一个section:.got.plt
调用非static函数时,会先跳转到对应的PLT项,然后PLT项中的跳板代码会跳转到对应的.got.plt
项中的地址,而这个地址是动态链接器填入的方针函数的地址。
推迟绑定
因为库中很多函数运行时或许用不到,比方崩溃处理的函数多数状况下是用不到的,别的像libc中提供了很多的函数,可是app或许只运用其中很少的一部分,因而加载动态库时绑定一切的函数就有点浪费了,因而就引进了推迟绑定:当第一次调用函数时才触发动态链接器去查找方针函数地址。
引进PLT的优缺点
优点
- 引进plt表(
.got.plt
)后,重定向时,文本段不需求修正,因而文本段能够在多个进程中同享,可削减内存运用 - 引进plt表(
.got.plt
)后,可明显削减重定位项(跟上面GOT相似) - 引进plt表后,便利实现推迟绑定
缺点
- 调用函数时多了一层直接,功能稍有影响
补白:其实上面说到的优缺点首要都是 .got.plt
带来的,.plt
section 自身并不一定要存在,编译时甚至能够经过 -fno-plt
来禁止生成 .plt
section。仅仅将跳板代码(尤其是支持推迟绑定的状况)抽出来放到独自的 .plt
section ,指令体积会减小,而且.got.plt
中的初始值核算更便利。
看个调用外部库函数的比如
object file文本段重定向
#include<stdlib.h>
void* malloc_proxy(size_t size) {
return malloc(size);
}
反编译后的指令:
Disassembly of section .text:
0000000000000000 <malloc_proxy>:
0: a9bf7bfd stp x29, x30, [sp, #-16]!
4: 910003fd mov x29, sp
8: 94000000 bl 0
c: a8c17bfd ldp x29, x30, [sp], #16
10: d65f03c0 ret
bl
的方针地址 0 是因为编译时不知道malloc
函数的地址,需求重定向:
Relocation section '.rela.text' at offset 0x1f8 contains 1 entry:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000008 0000000b0000011b R_AARCH64_CALL26 0000000000000000 malloc + 0
跟上面相同,在object file中是不存在.got
.plt
这些section的,重定向是直接针对文本段来的,.plt
.got.plt
section 是由链接器(program linker)创立的。
动态库PLT GOTPLT表
将object file链接后反编译如下:
Disassembly of section .plt:
0000000000000250 <.plt>:
250: a9bf7bf0 stp x16, x30, [sp, #-16]!
254: f00000f0 adrp x16, 1f000
258: f947fe11 ldr x17, [x16, #4088]
25c: 913fe210 add x16, x16, #0xff8
260: d61f0220 br x17
264: d503201f nop
268: d503201f nop
26c: d503201f nop
0000000000000270 <malloc@plt>:
270: 90000110 adrp x16, 20000
274: f9400211 ldr x17, [x16]
278: 91000210 add x16, x16, #0x0
27c: d61f0220 br x17
Disassembly of section .text:
0000000000000280 <malloc_proxy>:
280: a9bf7bfd stp x29, x30, [sp, #-16]!
284: 910003fd mov x29, sp
288: 97fffffa bl 270
28c: a8c17bfd ldp x29, x30, [sp], #16
290: d65f03c0 ret
从反编译能够看到:
- malloc_proxy 改为调用 malloc@plt 的跳板代码(
bl 270
,偏移 0x270 处的符号:malloc@plt) - malloc@plt 中前两条指令将 PC 偏移 0x20000 处的值读入
x17
寄存器中,第4条指令挑转到x17
对应的地址处。(先疏忽x16
)
来看下重定向信息:
Relocation section '.rela.plt' at offset 0x230 contains 1 entry:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000020000 0000000100000402 R_AARCH64_JUMP_SLOT 0000000000000000 malloc@GLIBC_2.17 + 0
从重定向信息能够看到,动态链接器会在偏移0x20000
处存入 malloc
函数的地址。
(补白:malloc 后面的 @GLIBC_2.17
是symbol versioning,本文疏忽)
因而 br x17
就跳转到了 malloc
函数中,实现了对 malloc 的调用。
(补白:因为 plt 中的跳板代码是经过 br
而不是 blr
,没有修正 lr
寄存器,因而方针函数回来后能回到开始调用方位的下一条指令)
- 从
section header table
中能够看到偏移 0x20000 处于.got.plt
section中。
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
...
[13] .got.plt PROGBITS 000000000001ffe8 00ffe8 000020 08 WA 0 0 8
...
别的咱们能够看下 .got.plt
section 中,0x20000处的值(动态库中静态的值,动态链接器绑定该符号前):
<.got.plt>:
20000: 0000000000000250
从这个地址能够看到:.got.plt
项(除了前3个项)中的初始值对应的是 .plt
section中的第一个项:
0000000000000250 <.plt>:
250: a9bf7bf0 stp x16, x30, [sp, #-16]!
254: f00000f0 adrp x16, 1f000
258: f947fe11 ldr x17, [x16, #4088]
25c: 913fe210 add x16, x16, #0xff8
260: d61f0220 br x17
-
adrp
&ldr
是将 PC 偏移0x1fff8
处的值(也便是.got.plt
的第3项)加载到x17
寄存器中 -
.got.plt
中的第3项是动态链接器存入的用来查找符号的函数的地址 -
stp
保存x16
lr
到栈上,因为动态链接器符号查找办法内部会修正lr寄存器,所以先保存到栈上 -
br x17
便是跳转到动态链接器符号查找函数去解析方针办法地址,并填入.got.plt
对应的项中,从栈上康复寄存器(lr
),并跳入该地址履行方针办法
回忆一下上面1,2,3,可知经过plt调用非static办法的流程如下:
- 调用 plt 中方针办法对应的跳板办法:xxx@plt
- xxx@plt 办法跳转到
.got.plt
对应项中的地址 - 假如方针符号现已绑定过
.got.plt
中对应项的地址便是方针函数的地址,调用过程完结 - 假如方针符号未绑定过,
.got.plt
中对应项的地址指向.plt
中的另一段跳板代码,该代码跳转到动态链接器的符号查找代码,会将查找到的符号地址填入.got.plt
的对应项中,并跳转到方针符号地址以完结办法履行
动态库 BIND_NOW
Android 平台(arm架构)动态库默许是 当即绑定(bind now)的。但Linux平台上默许是推迟绑定的(上面现已说到过推迟绑定的优缺点),关于一个指定的动态库怎么判别是否是当即绑定的呢?
- 假如动态库
.dynamic
section中存在DT_BIND_NOW
项,那么会当即绑定 - 假如动态库
.dynamic
section中DT_FLAGS
value设置了DT_BIND_NOW
或许DT_FLAGS_1
value设置了DF_1_NOW
的话,会当即绑定 - 假如运行时环境变量包括
LD_BIND_NOW
,会当即绑定
推迟绑定时获取函数地址
上面讲过了关于推迟绑定的状况下,比及第一次调用函数时才会调用动态链接器的办法去查找符号。那么假如在调用函数之前,先要获取函数地址怎样办呢?其实这个很简单,获取函数地址跟变量拜访相同,会在.got
表中有一个独自的表项存储对应函数的地址,也会有一个独自的重定位项,关于变量地址,动态链接器都是当即绑定的。因而关于同一个函数,在同一个库中或许会一起存在 plt(.got.plt) 项 & got 项。
看一个获取外部函数地址的比如:
#include<stdlib.h>
#include<stdio.h>
void f() {
printf("%pn", malloc);
}
看下他的重定位项:
Relocation section '.rela.dyn' at offset 0x3b8 contains 8 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
...
000000000001ffd0 0000000500000401 R_AARCH64_GLOB_DAT 0000000000000000 malloc@GLIBC_2.17 + 0
...
大局符号介入
前文说到,拜访大局变量、非static函数需求从相应的got项中取出符号的地址,关于外部库的符号这个好了解,关于同一个库中的符号,拜访代码与被拜访的符号之间的距离是确认的,为什么也要从got中获取地址呢?
原因便是“大局符号介入”的存在:当向大局符号表加入符号时,假如同名符号已存在,则疏忽后面的符号。因而拜访同一个库中的大局变量或许非static的函数,运行时实践拜访的或许是先加载的库中的同名符号。
因而假如某个符号,规划上并不期望外部运用的话,应该将其设置为 static 的,或许放到C++匿名命名空间中,或许将他的 visibility 设置为 hidden。这样还能够优化功能。