ELF中.got、.plt section的作用、lazy binding的实现及全局符号介入的影响

动态库重定向

每个进程都可以拥有一个独立的虚拟地址空间,所以对于可执行文件,他可以有一个固定的虚拟基地址,但是对于动态库,为每个动态库划分固定的虚拟地址范围会非常麻烦,而且可能无法做到:比如库的大小变了、要增加新的库、某个库废弃不用了,等等情况都要重新划分。另外当以类似插件的形式运行时要增加一个新库时,无法找到一个固定可用的虚拟地址范围。因为无法预知所以无法事先预留,因此需要动态链接器在运行时进行重定向。本文后面讨论的都是针对position independent code

GOT

GOT表简介

GOT:Global Offset Table,是用于访问全局变量的。

GOT表中存的是变量的虚拟地址,当访问全局变量时,会先从相应got表项中获取变量的地址,然后再从该地址中读出值,或者向该地址中写入新值。

引入GOT表的优缺点

优点
  1. 引入got表后,重定向时,文本段不需要修改,因此文本段可以在多个进程中共享,可减少内存使用
  2. 引入got表后,可显著减少重定位项的数目(如果是对文本段重定位,每处访问变量的地方都需要一个重定位项,而引入got表后,仅每个目标变量一个重定位项),可减少动态连接器重定位的耗时
缺点
  1. 访问变量时多了一次间接操作(需要先从got项中加载变量的地址),速度稍有影响

看个访问外部库变量的例子

object file 中文本段重定向
c 复制代码
extern int value;

int readValue() {
	return value;
}

反编译后的指令:

yaml 复制代码
Disassembly of section .text:

0000000000000000 <readValue>:
   0:	90000000 	adrp	x0, 0 
   4:	f9400000 	ldr	x0, [x0]
   8:	b9400000 	ldr	w0, [x0]
   c:	d65f03c0 	ret
  1. 第一条和第二条指令用于加载 value 变量的地址到 x0寄存器中。(备注:因为 aarch64 是定长指令集,每条指令固定4字节,使用这两条指令,可以访问PC +/-4GB的范围)
  2. 第三条指令从x0所表示的地址中读出数据放到w0寄存器中。(备注:int是32位,所以保存到w0x0的高32位会自动清0)

上面第一条,第二条加载地址的指令是"不完整"的,实际情况应该是:

css 复制代码
adrp	x0, pageAddr
ldr     x0, [x0, pageOffset]

但是因为外部符号value的地址在其库运行时被加载后才能确定,编译时是无法知晓的,因此编译的时候pageAddrpageOffset都留空(填0),等待重定向,可以看下重定向信息:

sql 复制代码
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)一下,反编译看看:

yaml 复制代码
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的地址,来看下重定向信息:

sql 复制代码
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。

less 复制代码
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 的重定位信息:

sql 复制代码
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的优缺点

优点
  1. 引入plt表(.got.plt)后,重定向时,文本段不需要修改,因此文本段可以在多个进程中共享,可减少内存使用
  2. 引入plt表(.got.plt)后,可显著减少重定位项(跟上面GOT类似)
  3. 引入plt表后,方便实现延迟绑定
缺点
  1. 调用函数时多了一层间接,性能稍有影响

备注:其实上面提到的优缺点主要都是 .got.plt 带来的,.plt section 本身并不一定要存在,编译时甚至可以通过 -fno-plt 来禁止生成 .plt section。只是将跳板代码(尤其是支持延迟绑定的情况)抽出来放到单独的 .plt section ,指令体积会减小,并且.got.plt中的初始值计算更方便。

看个调用外部库函数的例子

object file文本段重定向
c 复制代码
#include<stdlib.h>

void* malloc_proxy(size_t size) {
	return malloc(size);
}

反编译后的指令:

yaml 复制代码
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 函数的地址,需要重定向:

sql 复制代码
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链接后反编译如下:

yaml 复制代码
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

从反编译可以看到:

  1. malloc_proxy 改为调用 malloc@plt 的跳板代码(bl 270,偏移 0x270 处的符号:malloc@plt)
  2. malloc@plt 中前两条指令将 PC 偏移 0x20000 处的值读入 x17寄存器中,第4条指令挑转到x17对应的地址处。(先忽略x16) 来看下重定向信息:
sql 复制代码
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 寄存器,因此目标函数返回后能回到最初调用位置的下一条指令)

  1. section header table 中可以看到偏移 0x20000 处于 .got.plt section中。
less 复制代码
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处的值(动态库中静态的值,动态链接器绑定该符号前):

makefile 复制代码
<.got.plt>:
20000:     0000000000000250

从这个地址可以看到:.got.plt 项(除了前3个项)中的初始值对应的是 .plt section中的第一个项:

yaml 复制代码
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方法的流程如下:

  1. 调用 plt 中目标方法对应的跳板方法:xxx@plt
  2. xxx@plt 方法跳转到 .got.plt 对应项中的地址
  3. 如果目标符号已经绑定过 .got.plt 中对应项的地址就是目标函数的地址,调用过程完成
  4. 如果目标符号未绑定过,.got.plt中对应项的地址指向 .plt 中的另一段跳板代码,该代码跳转到动态链接器的符号查找代码,会将查找到的符号地址填入 .got.plt的对应项中,并跳转到目标符号地址以完成方法执行
动态库 BIND_NOW

Android 平台(arm架构)动态库默认是 立即绑定(bind now)的。但Linux平台上默认是延迟绑定的(上面已经提到过延迟绑定的优缺点),对于一个指定的动态库如何判断是否是立即绑定的呢?

  1. 如果动态库 .dynamic section中存在 DT_BIND_NOW 项,那么会立即绑定
  2. 如果动态库 .dynamic section中DT_FLAGSvalue设置了DT_BIND_NOW或者DT_FLAGS_1value设置了DF_1_NOW的话,会立即绑定
  3. 如果运行时环境变量包含LD_BIND_NOW,会立即绑定
延迟绑定时获取函数地址

上面讲过了对于延迟绑定的情况下,等到第一次调用函数时才会调用动态链接器的方法去查找符号。那么如果在调用函数之前,先要获取函数地址怎么办呢?其实这个很简单,获取函数地址跟变量访问一样,会在.got表中有一个单独的表项存储对应函数的地址,也会有一个单独的重定位项,对于变量地址,动态链接器都是立即绑定的。因此对于同一个函数,在同一个库中可能会同时存在 plt(.got.plt) 项 & got 项。

看一个获取外部函数地址的例子:

c 复制代码
#include<stdlib.h>
#include<stdio.h>

void f() {
        printf("%p\n", malloc);
}

看下他的重定位项:

sql 复制代码
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。这样还可以优化性能。

相关推荐
CYRUS_STUDIO37 分钟前
使用 Dex2C 加壳保护 Android APK 代码
android·安全·逆向
alexhilton1 小时前
理解Jetpack Compose中副作用函数的内部原理
android·kotlin·android jetpack
恋猫de小郭5 小时前
腾讯 Kuikly 正式开源,了解一下这个基于 Kotlin 的全平台框架
android·前端·ios
贫道绝缘子6 小时前
【Android】四大组件之Activity
android
人生游戏牛马NPC1号6 小时前
学习Android(四)
android·kotlin
_祝你今天愉快6 小时前
安卓触摸事件分发机制分析
android
fyr897576 小时前
Ubuntu 下编译goldfish内核并使用模拟器运行
android·linux
心之所向,自强不息7 小时前
关于Android Studio的Gradle各项配置
android·ide·gradle·android studio
隐-梵7 小时前
Android studio学习之路(八)---Fragment碎片化页面的使用
android·学习·android studio
百锦再7 小时前
Kotlin学习基础知识大全(上)
android·xml·学习·微信·kotlin·studio·mobile