目录 | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
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 |
...
分节
...
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表
代码块 | ||
---|---|---|
| ||
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 |
...
从汇编中看出,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 |
|
_Thread_local |
|
thread_local |
|
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 |
注:
libvulkan.so.1.3.204达到1000个接口。
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 |
大多与操作动态库相关接口,用于更改操作的实现,加载时需初始化数据结构 | 2 | _rtld_global@GLIBC_PRIVATE |
栈操作接口,加载时初始化栈范围 | 2 | __libc_stack_end@GLIBC_2.2.5 |
操作TLS相关操作 | 4 | _dl_allocate_tls@GLIBC_PRIVATE |
安全相关 | 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变量时,分别调用__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 |
...
从汇编看出,通过__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的偏移量保存在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 |
...
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
...