AndroidLinker简介
简单介绍Android linker的基础知识,基于Android 10分⽀。
linker的作⽤android最新版
考虑简单的HelloWorld程序。
$ tree .
.
|-- jni
|  |-- Android.mk
|  `-- helloworld.c
...
$ cat jni/helloworld.c
#include <stdio.h>
int main() {
puts("hello, world\n");
return 0;
}
$ ndk-build
install        : helloworld => libs/arm64-v8a/helloworld
我们只需要调⽤puts库函数来打印字符串到标准输出,不需要⾃⼰实现打印的功能。⼯具链(⽐如Android ndk,包括编译器和链接编辑器等)将源⽂件编译成动态可执⾏程序。puts的代码在libc库中实现,不会编译到我们的HelloWorld程序当中,所以当运⾏HelloWorld程序的时候,libc库需要同时被加载到进程地址空间,这样main函数才能调⽤puts函数,这个⼯作由linker完成。现代操作系统⼤多默认配置ASLR,程序每次执⾏,libc库在内存地址空间中的加载地址是不固定的,即puts函数的实际地址也是不固定的,所以编译器编译main函数时不能直接引⽤puts函数的地址,只能通过重定向机制来
间接引⽤,可以简单理解成,main函数通过⼀个指针来间接调⽤puts函数,⽽linker负责在运⾏时查puts的实际加载地址,修改这个指针,使其指向正确的地址。
所以linker主要作⽤:加载可执⾏程序依赖的库;查修改被引⽤的符号(称为符号解析或者重定向)。
实际上动态链接涉及⾮常多的细节,linker需要处理这些细节,⽐如调⽤每个库的初始化函数,处理符号的版本,库内部符号的解析等等,这⾥不做讨论。
Android linker程序
64位系统上,Android linker程序位于/system/bin/linker64路径。其本⾝是⼀个动态可执⾏程序,能够直接运⾏。
$ file linker64
linker64: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[md5/uuid]=22c1b90f715b68a629bd2c0113c02dae, not stripped $ adb shell linker64
Usage: linker64 program []
linker64 path.zip!/program []
A helper program for linking dynamic executables. Typically, the kernel loads
this program because it's the PT_INTERP of a dynamic executable.
This program can also be run directly to load and run a dynamic executable. The
executable can be inside a zip file if it's stored uncompressed and at a
page-aligned offset.
如上描述,⼀般linker不是作为独⽴可执⾏程序运⾏,⽽是由kernel在运⾏其他可执⾏程序时调⽤。Android 可执⾏程序为ELF格式,ELF可执⾏程序有⼀个INTERP类型的program header,指定linker程序的路径。当在命令⾏中运⾏⼀个ELF可执⾏程序的时候,⽐如我们在命令⾏shell中执⾏helloworld程序时adb shell /data/local/tmp/helloworld,内核同时将helloworld和linker程序加载到内存,然后跳转到linker程序的⼊⼝函数执⾏,由linker负责完成动态连接过程:加载helloworld依赖的库libc等,查puts等函数的实际地址,修改main函数对puts的引⽤(重定向)。最后linker程序跳转到helloworld程序的⼊⼝处开始执⾏。看上去就像helloworld程序直接运⾏⼀样。
$ aarch64-linux-android-readelf -l libs/arm64-v8a/helloworld
...
Program Headers:
Type          Offset                      VirtAddr                  PhysAddr
FileSiz                      MemSiz                  Flags  Align
...
INTERP      0x0000000000000238  0x0000000000000238  0x0000000000000238
0x0000000000000015  0x0000000000000015  R      1
[Requesting program interpreter: /system/bin/linker64]
...
除了⽤于链接可执⾏程序,Android linker还提供了dlopen系列函数的实现。Android系统上libdl.so中的dlopen函数只是⼀个wrapper,实际功能实现在linker程序中。
// bionic/libdl/libdl.cpp, libdl中的wrapper函数
__attribute__((__weak__))
void* dlopen(const char* filename, int flag) {
const void* caller_addr = __builtin_return_address(0);
return __loader_dlopen(filename, flag, caller_addr);
}
// bionic/linker/dlfcn.cpp,linker中的实现
void* __loader_dlopen(const char* filename, int flags, const void* caller_addr) {
return dlopen_ext(filename, flags, nullptr, caller_addr);
}
查并加载库
可执⾏程序依赖的库⽂件记录在ELF⽂件动态段中类型为NEEDED的表项中,如下图。
$ aarch64-linux-android-readelf -d libs/arm64-v8a/helloworld
Dynamic section at offset 0xd88 contains 30 entries:
Tag                        Type                    Name/Value
0x0000000000000001 (NEEDED)            Shared library: [libc.so]
0x0000000000000001 (NEEDED)            Shared library: [libm.so]
0x0000000000000001 (NEEDED)            Shared library: [libdl.so]
这⾥helloworld程序依赖三个库⽂件,分别是libc.so, libm.so, libdl.so。
被依赖的库⽂件,也可能依赖其他的库⽂件,Linker⾸先按照BFS顺序,加载这些库⽂件到进程的内存地址空间。但是这⾥NEEDED表项记录的是⽂件名,没有包含完整路径,那么在哪⾥到这些⽂件呢?另外,dlopen函数参数指定要加载的库⽂件可以是绝对路径,也可以是不带路径的⽂件名,后者如何查呢?Linker按照⼀定的顺序查⼀些指定的⽬录,在这些⽬录中寻库⽂件。Android linker在Android N版本上引⼊了⼀个命名空间的概念,使库⽂件的查变得稍微复杂⼀下,但是基本的查原则是⼀致的。这⾥先介绍引⼊命名空间之前的查规则,然后讨论命名空间的概念,引⼊的原因,以及完整的查规则。
Linker按照顺序在指定的⼀些⽬录中查依赖的库⽂件,这个顺序受运⾏时的环境变量、编译时的参数,以及linker内部实现影响。查顺序的规则如下。
1. 如果环境变量LD_LIBRARY_PATH=/path/to/dir1/:/path/to/dir2/被设置,则⾸先在环境变量指定的⽬录中查;
2. 如果库⽂件编译时使⽤了-rpath=/path/to/dir1:/path/to/dir2, 则在rpath参数指定的⽬录中查。rpath指定的路径保存在ELF⽂件的动态段中
的RUNTPATH表项:
$ cat jni/Android.mk
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := testlib.c
LOCAL_LDFLAGS := -Wl,-rpath=/data/local/tmp/:/data/
include $(BUILD_SHARED_LIBRARY)
$ ndk-build
...
$ aarch64-linux-android-readelf -d libs/arm64-v8a/libtest.so
Dynamic section at offset 0xdd8 contains 27 entries:
Tag                          Type              Name/Value
...
0x000000000000001d  (RUNPATH)      Library runpath: [/data/local/tmp/:/data/]
3. 在linker指定的默认路径中查。不同的操作系统或者不同的linker实现,有不同的配置。Android 10系统上如果没有配置命名空间规则
(实际都会配置,这⾥只是举个简单例⼦),则默认的查路径如下:
/system/lib64
/odm/lib64
/vendor/lib64
Android Linker 命名空间(namespace)
Android linker namespace从Android 7开始引⼊,到Android 10不断修改完善,主要⽤来解决两个需求:
1. 禁⽌应⽤程序(apk)访问⾮公开的NDK库,改善Android碎⽚化导致的应⽤兼容问题。Android应⽤程序可以通过JNI使⽤native库函数,
以前没有限制的时候,很多开发者为了实现各种需求,经常会使⽤不在NDK中的系统库。⽽这些库实际属于Android系统的私有库,其API/ABI会随着Android版本不断变化,不保证向后兼容,⽽Android系统碎⽚化⼜⾮常严重,导致严重的应⽤兼容性问题;
2. system与vendor分区的解耦,减少Android系统的碎⽚化。Android 8引⼊treble架构,将system分区与vendor分区解耦,这样在
Android版本升级时,可以单独升级system分区,⽽不需要重新适配vendor分区,减少OEM⼚商在Android⼤版本升级时的适配⼯作,加快Android⼤版本的升级速度。
⼀个namespace定义了⼀个范围,每个可执⾏程序或者库⽂件都属于⼀个namespace,linker查依赖的库⽂件时,只在被依赖的可执⾏程序或库⽂件所属的namespace(及其直接关联的namespace)中查。下图是namespace数据结构的⼀部分,ld_library_paths对应前⾯所述的LD_LIBRARY_PATH环境变量,default_library_path对应前⾯所述linker默认路径。Linker在namespace中的查顺序同之前我们介绍的顺序⼀致,即先在ld_library_paths中查,然后在RUN_PATH指定的⽬录中查,最后在default_library_paths中查。
当运⾏⼀个可执⾏程序的时候,系统根据⼀个配置⽂件(/system/fig.<vndk_version>.txt),为该程序创建对应的namespace。该配置⽂件分别定义了/system/bin/、/vendor/bin/等⽬录下可执⾏程序在运⾏时进程内的namespace配置。例如运⾏/system/bin/⽬录下的程序时,可执⾏程序所在的namespace的default_library_path被设置为/system/lib64/, /product/lib64,即先从这两个⽬录开始查依赖的库;⽽运⾏/vendor/bin/⽬录下的程序时,可执⾏程序所在的namespace的default_library_path被设置为/odm/lib64, /vendor/lib64,即先从这两个⽬录查依赖的库。
⼀个namespace可以关联多个其他namespace,当在这个namespace中不到库⽂件的时候,可以在其直接关联的namespace中查,如果仍然不到,则不再继续。如果⼀个库⽂件在其调⽤者的namespace中到,则该库也属于调⽤者的namespace,如果⼀个库⽂件在其调⽤者namespace的关联的某个namespace中到,则该库属于关联的namespace。
system分区和vendor分区可执⾏程序运⾏时的namespace配置如下图所⽰(来源于Android官⽹)。
当执⾏⼀个可执⾏程序的时候,linker在可执⾏程序所属的namespace中开始查;或者当调⽤dlopen加载⼀个库⽂件的时候,linker在调⽤函数所属可执⾏程序或库所在的namespace开始查。查顺序如下。
1. ⾸先在该namespace中查,查顺序如前所述,先在ld_library_paths中查,对应LD_LIBRARY_PATH环境变量,然后查库⽂
件RUN_PATH指定的⽬录,最后在default_library_paths中查。如果在RUN_PATH中到,或者到的库⽂件是符号链接,则进⼀步检查实际的库⽂件是否在white_listed, ld_library_paths, default_library_paths, permitted_paths这⼏个⽬录中,如果不在则不允许加载
2. 如果1中没有到,则在关联的namespace中查,查顺序同1. 可以指定在关联的namespace中做完整的查,或者只在⼀个库⽂
件列表中查
3. 如果以上两步都没有到,则返回失败,即不会递归查关联namespace的关联namespace。
符号解析
Linker将所有依赖涉及的库⽂件全部加载到进程的内存地址空间之后,开始解析符号。这个过程就⽐较直观了,⼤致过程如下:从可执⾏程序或者dlopen要加载的库开始,按照BFS顺序遍历每个加载的库⽂件;对于每个库⽂件,遍历所有的重定向表,对于每个表项,在依赖的库中查器符号,将符号地址写⼊表项指定的地址,完成符号解析⼯作。
代码浏览
Android linker代码实现位于Android源码的bionic/linker⽬录。推荐Google最近发布的代码浏览⼯具:
libdl, namespace等相关代码主要在 bionic/libdl, art/libnativeloader(master分⽀)等⼯程⽬录下。
64位arm平台上,Linker⼊⼝函数在
函数实现了linker加载库函数,解析符号的主要过程,是linker中极为重要的⼀个函数,也是理解linker运⾏原理的关键之⼀。
, 是创建linker namespace的代码逻辑。
Resources
阅读以下⽂档和代码,可以对Android linker有⼀个更好的理解。
man page of tools: readelf, gcc, ld, android-ndk, etc.
Google

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。