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。这样还可以优化性能。

相关推荐
uwvwko2 小时前
BUUCTF——web刷题第一页题解
android·前端·数据库·php·web·ctf
fzxwl3 小时前
隆重推荐(Android 和 iOS)UI 自动化工具—Maestro
android·ui·ios
LittleLoveBoy5 小时前
踩坑:uiautomatorviewer.bat 打不开
android
居然是阿宋5 小时前
Android核心系统服务:AMS、WMS、PMS 与 system_server 进程解析
android
CGG928 小时前
【单例模式】
android·java·单例模式
kp000009 小时前
PHP弱类型安全漏洞解析与防范指南
android·开发语言·安全·web安全·php·漏洞
编程乐学(Arfan开发工程师)14 小时前
06、基础入门-SpringBoot-依赖管理特性
android·spring boot·后端
androidwork14 小时前
使用 Kotlin 和 Jetpack Compose 开发 Wear OS 应用的完整指南
android·kotlin
繁依Fanyi15 小时前
Animaster:一次由 CodeBuddy 主导的 CSS 动画编辑器诞生记
android·前端·css·编辑器·codebuddy首席试玩官
奔跑吧 android17 小时前
【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
android·bluetooth·bt·aosp13·storagemodule