版本比较

密钥

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。
目录
minLevel1
maxLevel4
include
outlinefalse
indent
stylenone
exclude
typelist
printablefalse
class

Dynamic Linker

动态链接器是在程序执行过程中对ELF二进制文件进行解释,将多个ELF文件组合成一个完整的程序镜像,也称解释器。

...

ELF Header

ELF头用于识别ELF可执行格式。

...

Field

Name

Value

Meaning

e_type

ET_NONE

0

No file type

ET_REL

1

Relocatable file

ET_EXEC

2

Executable file

ET_DYN

3

Shared object file

ET_CORE

4

Core file

e_ident[EI_CLASS]

ELFCLASSNONE

0

Invalid class

ELFCLASS32

1

32-bit objects

ELFCLASS64

2

64-bit objects

e_ident[EI_DATA]

ELFDATANONE

 Invalid data encoding

ELFDATA2LSB

little endian

ELFDATA2MSB

big endian

共有3种类型的ELF,分别为.o文件,共享对象文件(.so文件和包含main的二进制文件)和静态链接的可执行文件。
以一个简单的C程序为例:

...

$ gcc -static -o main-static main.o
$ readelf -h main-static

...

共享对象文件的ELF头

$ gcc -o main-dynamic main.o
$ readelf -h main-dynamic

...

分节

...

Field

Name

Value

Meaning

sh_type

SHT_NULL

0

SHT_PROGBITS

1

程序比特流,程序运行时需要的数据

SHT_SYMTAB

2

符号表,协助调试

SHT_STRTAB

3

字符串表

SHT_RELA

4

新版重定位表

SHT_HASH

5

哈希表,用于符号比较查找

SHT_DYNAMIC

6

动态表

SHT_NOTE

7

注释信息

SHT_NOBITS

8

不占文件空间,但需内存空间,如.bss

SHT_REL

9

旧版重定位表

SHT_SHLIB

10

预留

SHT_DYNSYM

11

符号表,需重定位符号,导出符号等

SHT_INIT_ARRAY

14

Array of constructors

SHT_FINI_ARRAY

15

Array of destructors

SHT_PREINIT_ARRAY

16

Array of pre-constructors

SHT_LOOS

0x60000000

Start OS-specific

SHT_HIOS

0x6fffffff

End OS-specific type

SHT_LOPROC

0x70000000

Start of processor-specific

SHT_HIPROC

0x7fffffff

End of processor-specific

SHT_LOUSER

0x80000000

Start of application-specific

SHT_HIUSER

0x8fffffff

End of application-specific

sh_flags

SHF_WRITE

0x1

Writable

SHF_ALLOC

0x2

Occupies memory during execution

SHF_EXECINSTR

0x4

Executable

SHF_TLS

0x400

Section hold thread-local data

SHF_MASKOS

0x0ff00000

OS-specific

SHF_MASKPROC

0xf0000000

Processor-specific

$ readelf -W -S main-dynamic 

...

Dynamic表

代码块
languagejava
typedef struct {
Elf64_Sxwordd_tag; /* Dynamic entry type */
Union {
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
Elf64_Dyn _DYNAMIC[];
$ readelf -W -d main-dynamic

...

  • st_name:符号名,值为动态字符串表的索引

  • st_info:分两部分,高4位表示符号绑定方式(ELF64_ST_BIND),低4位表示符号类型(ELF64_ST_TYPE)。其定义见下表。

  • st_shndx:分节索引,其中包含特殊的分节索引(SHN_UNDEF/ SHN_ABS/ SHN_COMMON)。

  • st_value:与符号关联的值,描述符号所在的位置,可能是绝对值,相对虚拟地址等

    • 可重定位ELF:值表示在st_shndex指向的分节中的偏移,即符号做在分区的位置,比如:函数定义在.text代码分节中,该值指向该函数在.text中的偏移;若st_shndex为SHN_COMMON,表示符号的对齐字节数,该符号未分配空间,无具体位置

    • 共享对象或可执行程序ELF:值表示虚拟地址,指向符号所在位置的相对虚拟地址,一般不需要参照st_shndx

    • SHN_UNDEF:表示符号未定义,值为0

    • SHN_ABS:表示值为绝对虚拟地址

  • st_size:符号代表的数据大小。若为函数,则表示函数二进制机器指令大小。

Field

Name

Value

Meaning

ELF64_ST_BIND

STB_LOCAL

0

文件域全局符号,作用域在可重定位文件中

STB_GLOBAL

1

全局符号,作用域为整个程序

STB_WEAK

2

全局符号,不同共享对象文件中可重复定义

ELF64_ST_TYPE

STT_NOTYPE

0

未指定类型

STT_OBJECT

1

数据对象,如:变量、数组等

STT_FUNC

2

函数或者可执行代码段(如:汇编代码)

STT_SECTION

3

与分节关联的符号,主要跟重定位相关

STT_FILE

4

文件符号

注:符号查找顺序为STB_LOCAL-> STB_GLOBAL->STB_WEAK。

...

程序段

程序段是将属性相同的分节按序整合在一个段中。

...

Field

Name

Value

Meaning

p_type

PT_NULL

0

Program header table entry unused

PT_LOAD

1

Loadable program segment

PT_DYNAMIC

2

Dynamic linking information

PT_INTERP

3

Program interpreter

PT_NOTE

4

Auxiliary information

PT_SHLIB

5

Reserved

PT_PHDR

6

Entry for header table itself

PT_TLS

7

Thread-local storage segment

PT_LOOS

0x60000000

Start of OS-specific

PT_HIOS

0x6fffffff

End of OS-specific

PT_LOPROC

0x70000000

Start of processor-specific

PT_HIPROC

0x7fffffff

End of processor-specific

p_flags

PF_X

0x1

Segment is executable

PF_W

0x2

Segment is writable

PF_R

0x4

Segment is readable

PF_MASKOS

0x0ff00000

OS-specific

PF_MASKPROC

0xf0000000

Processor-specific

...

从程序头的信息可知,由于段有对齐的要求,段间的内容可能有重叠。LOAD段必须按虚拟地址顺序排列,且地址相邻,其基本顺序为代码段,只读数据段,可读写数据段。这是相对寻址的规范要求。
INTERP代表动态链接器,由该链接器初始化动态库信息,包括符号地址的初始化,构建一个完整的可执行程序镜像。
LOAD类型的段需要加载至内存,程序执行时需要用到的代码和数据,其中一个段为代码段,有的段为只读的数据段,有的为可读写的数据段,有的为未初始化的数据段。一般来说未初始化的数据段和可读写的有初始化值的数据段整合至一个可读写数据段内。
DYNAMIC描述动态加载时需要的所有数据,包括符号表,可重定位表,依赖库,符号版本信息表,初始化代码段,结束代码段,初始胡数组数据段,结束数据数据段等。与可读写数据段重叠。
其他段主要在程序加载过程中做初始化使用,初始化完成后,可以丢弃。
GNU_EH_FRAME表示异常处理段,与只读数据段重叠。
GNU_RELRO表示该段在加载初始化阶段重定位后数据无需在修改,在运行阶段可只读,由于数据量少,从段信息可以看到,其与可读写数据段重叠。
在ELF头中程序入口地址为0x610,其在可执行代码段(0x0000 ~ 0x01f8)内。

...

动态库不支持Local Exec访问方式,只支持包含main函数的程序。

$ gcc -O3 -fPIC -mtls-dialect=trad -ftls-model=initial-exec -o tls-ie tls.c
从重定位表中,没有__tls_get_addr的重定位,也没有TLS变量的重定位。其使用段寄存器和TLS变量在TLS程序段的偏移进行寻址。
$ readelf -W -r tls-le

...

从汇编中看出,var和var2在TLS程序段的偏移增加了0x10,这是aarch64的glibc使用变种1 TLS数据结构的原因。
$ objdump -j .text --disassemble=tls tls-le

...

       C/C++ TLS variables are declared with a specifier:

Specifier

Notes

__thread

  • non-standard, but ubiquitous in GCC and Clang

  • cannot have dynamic initialization or destruction

_Thread_local

  • a keyword standardized in C11

  • cannot have dynamic initialization or destruction

thread_local

  • C11: a macro for _Thread_local via threads.h

  • C++11: a keyword, allows dynamic initialization and/or destruction

glibc aarch64 TLS

根据libc.so的程序段和重定位信息可知,libc.so使用Initial Exec访问TLS模式。任何共享对象都依赖libc.so,在程序加载过程中,都会加载libc.so,静态链接libc.so,因此使用Initial Exec符合其使用场景。即使有共享对象动态加载(dlopen)libc.so,由于libc.so已经静态加载至内存,动态加载的libc.so时,会复用静态加载的libc.so
使用dlopen无法使用这些TLS变量,因为每个线程创建的Dynamic TLS block与TP的偏移不一致。(glibc支持该情况,对static TLS块预留了144字节,用于处理IE访问模式的动态加载情况,一般来说,框架库只会使用8字节的TLS变量,指向一个数据结构,这样减少的访问TLS的开销,也减少TLS变量占用空间,glibc可支持更多的动态库。但bionic未预留static TLS块空间)

...

有3个有符号的TLS变量,共占用16字节。
$ readelf -W -s /lib/aarch64-linux-gnu/libc.so.6 

...

glibc使用TLS变量汇总

作用域

所在文件

size

说明

errno

errno.c

4

标准c语言错误码

strerror/strsignal

tls-internal.c

16

错误码转换成字符串,字符串buffer为TLS变量

dlerror

libc_dlerror_result.c

8

共享对象动态加载错误字符串

herror/h_errno

herrno.c

4

Inet网络处理错误码

inet_ntoa

inet_ntoa.c

18

Inet将ip地址转换成字符串格式

locale

global-locale.c

8

本地化字符串、时间、货币等,如:strerror

malloc

arena.c/malloc.c

20

内存管理,本地内存池

resolv

res_libc.c/resolv_context.c

16

域名解析,资源状态和上下文均为TLS变量

pthread

cxa_thread_atexit_impl.c

24

线程退出时,回收线程资源

runrpc

rpc_thread.c

8

远端过程调用服务以及rpc错误使用TLS变量

ctype

ctype_info.c

24

ctype中需要buffer的字符串转换

  • 错误码:包括标准C错误码,动态库操作错误,网络错误码等使用TLS变量,错误码字符串化时需要缓冲,需要本地化,这些都使用TLS变量来支持线程安全接口。

  • 网络/域名解析:域名解析IP地址等线程安全接口,都使用TLS变量来实现线程安全

  • 本地化:本地化涉及字符串编码、语言,时间、货币、数字等格式,使用TLS变量缓冲数据,实现线程安全接口

  • 堆管理:优化多线程内存分配等效率,采用TLS变量来管理本地内存池

  • 网络名字解析:为实现涉及字符串返回接口的线程安全,均采用TLS变量作为缓冲。

  • C语言类型处理:字符串类型转换,如:toupper/tolower,根据本地化配置转换

  • 远端过程调用:rpc服务使用所有的TLS变量,rpc客户端使用错误码的TLS变量。

...

  • 提供linux环境下的bionic动态库链接器。初始化bionic libc运行环境,动态加载bionic动态库以及其依赖库,并进行重定位。Bionic动态库链接器支持bionic特定的ELF格式。

  • Bionic libc功能适配。逐个对bionic libc接口进行适配,对于不能正常执行的接口进行重构,实现缺失的接口功能。

  • 逐一适配bionic框架动态库。对每个框架动态库提供一个 Linux适配动态库,且对接口逐一适配,对不能正常的接口进行重构,对正常的接口进行透传。框架动态库依赖的动态库使用原生的bionic动态库

  • 采用动态加载(dlopen)bionic动态库,每个函数都采用显示调用(dlsym)方式查找函数地址。

  • Linux gnu解释器延迟解析函数符号。第一次调用bionic框架动态库函数时,对动态库进行动态加载,然后再显示解析该函数符号。未使用的函数不会被解析,可降低程序启动延时,减少符号解析开销。

适配接口统计

Libc适配统计

直接转发

间接转发

重构

透传

210

211

15

906

  • 直接转发:转发调用gnu libc的接口

  • 间接转发:进行接口适配,然后再调用gnu libc接口

  • 重构:进行功能适配,后再调用gnu libc接口或者全新实现,如:_pthread_gettid、_errno

  • 透传:不做任何处理,直接调用bionic libc接口。

Libc适配转发和重构分类:

类别

直接转发

间接转发

重构

字符串操作

44

6

0

内存管理

9

2

0

pthread

29

56

3

Stdio

14

73

5

网络相关

4

2

0

动态链接操作

0

8

0

目录操作

8

7

4

时间操作

10

0

0

系统日志操作

4

0

0

文件操作

2

0

0

Unix标准接口

11

0

0

stdlib

2

0

0

C++ ABI

2

1

0

环境变量操作

3

0

0

Locale

6

0

0

宽字符

6

6

0

信号处理

0

20

0

文件系统挂载操作

1

3

0

安卓特有

0

11

0

System properties

0

13

1

其它

9

4

1

框架动态库适配统计

库名

数量

转义

直通

未实现

安卓库数量

libEGL.so

44

16

24

4

79

libGLESv1_CM.so

145

2

272

0

278

libGLESv2.so

358

2

358

0

846

libvulkan.so

210

3

436

0

182

libOpenCL.so

129

10

130

0

数量统计方法:
readelf -W --dyn-syms libEGL.so | awk '{print $8}' | grep ^egl | wc -l
readelf -W --dyn-syms libGLESv1_CM.so | awk '{print $8}' | grep ^gl | wc -l
readelf -W --dyn-syms libGLESv2.so | awk '{print $8}' | grep ^gl | wc -l
readelf -W --dyn-syms libvulkan.so | awk '{print $8}' | grep ^vk | wc -l
框架动态库版本号

库名

Libhybris

Linux

Android

libEGL.so

1.0.0

1.1.0

N/A

libGLESv1_CM.so

1.0.1

1.2.0

N/A

libGLESv2.so

2.0.0

2.1.0

N/A

libvulkan.so

1.2.183

1.2.131

N/A

libOpenCL.so

1.0.0

1.0.0

注:

  1. libvulkan.so.1.3.204达到1000个接口。

  2. libhybris对vulkan的封装,采用IFUNC间接函数链接功能实现函数转发。

...

在gcc编译生成的程序中,动态表包含直接依赖的动态库信息;在clang编译生成的程序中,不但包含直接依赖的动态库信息,也包含间接依赖的动态库信息。
在一个示例中,程序调用了libxab.so和libxc.so中的函数,而libxc.so调用了libxe.so中的函数。分别使用gcc和clang编译,可以看到,clang编译的程序动态表中包含了堆libxe.so的依赖。

$ gcc -L. -o demo-gcc ../../main.c -lxab -lxc -lxe

$ readelf -W -d demo-gcc

...

$ clang -L. -o demo-clang ../../main.c -lxab -lxc -lxe

...

Libc与ld-linux版本一一对应,不同版本不能组合在一起使用。Libc依赖ld-linux,glibc-2.38的aarch64版本,libc有18个符号未定义,这些符号在ld-linux中定义。

类型

数量

符号

操作动态库操作相关,包括异常接口,审计库处理接口

8

_dl_argv@GLIBC_PRIVATE
_dl_audit_preinit@GLIBC_PRIVATE
_dl_audit_symbind_alt@GLIBC_PRIVATE
_dl_find_dso_for_object@GLIBC_PRIVATE
_dl_rtld_di_serinfo@GLIBC_PRIVATE
_dl_catch_exception@GLIBC_PRIVATE
_dl_signal_error@GLIBC_PRIVATE
_dl_signal_exception@GLIBC_PRIVATE

大多与操作动态库相关接口,用于更改操作的实现,加载时需初始化数据结构

2

_rtld_global@GLIBC_PRIVATE
_rtld_global_ro@GLIBC_PRIVATE

栈操作接口,加载时初始化栈范围

2

__libc_stack_end@GLIBC_2.2.5
__nptl_change_stack_perm@GLIBC_PRIVATE

操作TLS相关操作

4

_dl_allocate_tls@GLIBC_PRIVATE
_dl_allocate_tls_init@GLIBC_PRIVATE
_dl_deallocate_tls@GLIBC_PRIVATE
__tls_get_addr@GLIBC_2.3

安全相关

1

__libc_enable_secure@GLIBC_PRIVATE

调优参数

1

__tunable_get_val@GLIBC_PRIVATE

其中_rtld_global数据结构大小为4328,_rtld_global_ro为904,这两个数据结构大小与CPU体系结构相关。

...

该方式在程序编译时,在引用TLS变量时,使用__tls_get_addr函数获得其地址。该函数接收两个参数,分别是共享对象编号和相对于tp的偏移值。

$ gcc -O3 -shared -fPIC -ftls-model=global-dynamic -o tls-gd.so tls.c
重定位表,var, var2重定位位置为GOT表,每个变量占用两个表项,分别为编号和偏移值。
$ readelf -W -r tls-gd.so

...

从以下汇编代码看出,访问var, var2变量时,分别调用__tls_get_addr获取,参数地址为var和var2在GOT表项的起始地址,如:0x3fd8和0x3fc8所在地址。
$ objdump -j .text --disassemble=tls tls-gd.so

...

与Global Dynamic类似,由于只在内部共享对象引用,只需调用__tls_get_addr获得第一个TLS变量的地址即可,其余TLS变量地址使用相对偏移获得。

$ gcc -O3 -shared -fPIC -ftls-model=local-dynamic -o tls-ld.so tls.c
从重定位表中看出,只使用了一个R_X86_64_DTPMOD64表项记录编号,预留了8字节空间默认偏移为0,也即只需通过__tls_get_addr获得tp的首地址即可。
$ readelf -W -r tls-ld.so

...

从汇编看出,通过__tls_get_addr获取tp地址后,使用偏移量访问var,var2等。当TLS变量数量较多时,该访问模式性能优于Generic Dynamic。
$ objdump -j .text --disassemble=tls tls-ld.so

...

与Local Dynamic相比,去掉了对__tls_get_addr的函数调用,直接从偏移获取TLS变量值。由于无法提前获知共享对象在程序执行时的加载顺序,也即无法确切其与tp的偏移量,因此每个TLS变量都需要重定位,加载时复制给GOT表项。

$ gcc -O3 -shared -fPIC -ftls-model=initial-exec -o tls-ie.so tls.c
var和var2存在一个R_X86_64_TPOFF64重定位项,用于在加载过程中,计算变量与tp的偏移量。
$ readelf -W -r tls-ie.so

...

var的偏移量保存在GOT表项的0xefe0位置,var2的偏移量保存在GOT表项的0x3fd8。tp地址保存在%fs寄存器中。
$ objdump -j .text --disassemble=tls tls-ie.so

...

动态库不支持Local Exec访问方式,只支持包含main函数的程序。

$ gcc -O3 -fPIC -ftls-model=initial-exec -o tls-ie tls.c
从重定位表中,没有__tls_get_addr的重定位,也没有TLS变量的重定位。其使用段寄存器和TLS变量在TLS程序段的偏移进行寻址。
$ readelf -W -r tls-le

...

X86_64采用变种2的TLS数据结构,从指令看到var的地址偏移为-8,var2的地址偏移为-4,tp地址放在%fs寄存器中。
$ objdump -j .text --disassemble=tls tls-le

...

TLS Descrptors

$ gcc -O3 -shared -fPIC -mtls-dialect=gnu2 -o tls-desc.so tls.c

$ readelf -W -r tls-desc

...

汇编代码寻址TLS变量的GOT表项,并使用函数调用指令执行GOT表项中的函数,同时将表项地址保存%rax寄存器作为参数传递,函数返回TLS变量与TP的偏移。
$ objdump -j .text --disassemble=tls tls-desc

...