Android Linker简介
简单介绍Android linker的基础知识,基于Android 10分支。
linker的作用
考虑简单的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 [arguments...]
linker64 path.zip!/program [arguments...]
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内部实现影响。查找顺序的规则如下。
如果环境变量
LD_LIBRARY_PATH=/path/to/dir1/:/path/to/dir2/
被设置,则首先在环境变量指定的目录中查找;如果库文件编译时使用了
-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/]
在linker指定的默认路径中查找。不同的操作系统或者不同的linker实现,有不同的配置。Android 10系统上如果没有配置命名空间规则(实际都会配置,这里只是举个简单例子),则默认的查找路径如下:
/system/lib64
/odm/lib64
/vendor/lib64
Android Linker 命名空间(namespace)
Android linker namespace从Android 7开始引入,到Android 10不断修改完善,主要用来解决两个需求:
- 禁止应用程序(apk)访问非公开的NDK库,改善Android碎片化导致的应用兼容问题。Android应用程序可以通过JNI使用native库函数,以前没有限制的时候,很多开发者为了实现各种需求,经常会使用不在NDK中的系统库。而这些库实际属于Android系统的私有库,其API/ABI会随着Android版本不断变化,不保证向后兼容,而Android系统碎片化又非常严重,导致严重的应用兼容性问题;
- 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/etc/ld.config.<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开始查找。查找顺序如下。
- 首先在该namespace中查找,查找顺序如前所述,先在
ld_library_paths
中查找, 对应LD_LIBRARY_PATH
环境变量,然后查找库文件RUN_PATH
指定的目录,最后在default_library_paths
中查找。如果在RUN_PATH
中找到,或者找到的库文件是符号链接,则进一步检查实际的库文件是否在white_listed
,ld_library_paths
,default_library_paths
,permitted_paths
这几个目录中,如果不在则不允许加载 - 如果1中没有找到,则在关联的namespace中查找,查找顺序同1. 可以指定在关联的namespace中做完整的查找,或者只在一个库文件列表中查找
- 如果以上两步都没有找到,则返回失败,即不会递归查找关联namespace的关联namespace。
符号解析
Linker将所有依赖涉及的库文件全部加载到进程的内存地址空间之后,开始解析符号。这个过程就比较直观了,大致过程如下:从可执行程序或者dlopen
要加载的库开始,按照BFS顺序遍历每个加载的库文件;对于每个库文件,遍历所有的重定向表,对于每个表项,在依赖的库中查找器符号,将符号地址写入表项指定的地址,完成符号解析工作。
代码浏览
Android linker代码实现位于Android源码的bionic/linker目录。推荐Google最近发布的代码浏览工具:cs.android.com
libdl, namespace等相关代码主要在 bionic/libdl, art/libnativeloader(master分支)等工程目录下。
64位arm平台上,Linker入口函数在bionic/linker/arch/arm64/begin.S
find_libraries函数实现了linker加载库函数,解析符号的主要过程,是linker中极为重要的一个函数,也是理解linker运行原理的关键之一。
init_default_namespaces, CreateClassLoaderNamespace是创建linker namespace的代码逻辑。
Resources
阅读以下文档和代码,可以对Android linker有一个更好的理解。
- ELF
- cs.android.com
- vndk linker namespace
- man page of tools: readelf, gcc, ld, android-ndk, etc.
- Android Linker Namespace: Security Flaws
Android Linker简介的更多相关文章
- Android.mk简介:
Android.mk简介: Android.mk文件用来告知NDK Build 系统关于Source的信息. Android.mk将是GNU Makefile的一部分,且将被Build System解 ...
- Android Studio 简介及导入 jar 包和第三方开源库方[转]
原文:http://blog.sina.com.cn/s/blog_693301190102v6au.html Android Studio 简介 几天前的晚上突然又想使用 Android Studi ...
- "浅谈Android"第一篇:Android系统简介
近来,看了一本书,名字叫做<第一行代码>,是CSDN一名博主写的,一本Android入门级的书,比较适合新手.看了书之后,有感而发,想来进行Android开发已经有一年多了,但欠缺系统化的 ...
- 【译】Android系统简介—— Activity
续上一篇,继续介绍Android系统.上一篇: [译]Android系统简介 本文主要介绍构建Android应用的一些主要概念: Activity Activity是应用程序中一个单独的有UI的页面( ...
- 被遗忘的Android mipmaps简介
被遗忘的 Android mipmaps 简介 [导读]已经发布的 Android Studio1.1 版本是一个 bug 修复版本.在这个版本中,当你创建工程时一项改变将会吸引你的眼球.工程创建登陆 ...
- Android系统简介(中):系统架构
Android的系统架构栈分为4层,从上往下分别是Applications.Application framework.Libraries & Android Runtime.Linux ...
- Android系统简介(上):历史渊源
上个月,看到微信的一系列文章,讲到Linux的鼻祖-李纳斯的传记<Just for Fun>, 其人神乎其能, 其人生过程非常有趣,值得每个程序员细细品味. 而实际上,对我而已,虽然做软件 ...
- Android ART简介
一. Android ART简介 Android DEX/ODEX/OAT文件
- Android插件简介
/** * @actor Steffen.D * @time 2015.02.06 * @blog http://www.cnblogs.com/steffen */ Android插件简介 Andr ...
随机推荐
- python模块之random模块
random模块 随机模块,用于处理随机问题. import random # 随机整数 print(random.randint(0, 9)) # 0到9之间随机一个整数 print(random. ...
- vue组件中data是个函数
当我们const vm = new Vue({ el : '#app', data : { msg:‘hello World’ } })用习惯了,data是一个对象,可到了vue组件 Vue.co ...
- 深入理解Jvm--Java静态分配和动态分配完全解析
jvm中分配Dispatch的概念 分派是针对方法而言的,指的是方法确定的过程,通常发生在方法调用的过程中.分派根据方法选择的发生时机可以分为静态分派和动态分派,其中对于动态分派,根据宗量种数又可以分 ...
- linux 后备缓存
一个设备驱动常常以反复分配许多相同大小的对象而结束. 如果内核已经维护了一套相同 大小对象的内存池, 为什么不增加一些特殊的内存池给这些高容量的对象? 实际上, 内核 确实实现了一个设施来创建这类内存 ...
- linux 运行处理者
如同前面建议的, 当内核收到一个中断, 所有的注册的处理者被调用. 一个共享的处理者 必须能够在它需要的处理的中断和其他设备产生的中断之间区分. 使用 shared=1 选项来加载 short 安装了 ...
- C语言 屏幕截图 (GDI)
截取全屏幕 #include <windows.h> void echo(CHAR *str); int CaptureImage(HWND hWnd, CHAR *dirPath, ...
- 【k8s】kubeadm快速部署Kubernetes
1.Kubernetes 架构图 kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具. 这个工具能通过两条指令完成一个kubernetes集群的部署: # 创建一个 Mast ...
- CodeTypeDeclaration,CodeMemberProperty动态生成代码
由于是CodeDom些列,所以先介绍几个CodeDom表达式: :CodeConditionStatement:判断语句即是if(condition){} else{},看最全的那个构造函数: pub ...
- 在win64上使用bypy进行百度网盘文件上传
阿里云服务器的带宽为2M,网站每日的备份包都3G多了,离线下载太费时间了,打算每日将备份包自动上传到自己的百度云盘里.1.先安装Python 执行python -V ,发现没安装python2.去py ...
- Spring Boot中路径及配置文件读取问题
编译时src/main/java中*.java文件会被编译成*.class文件,在classpath中创建对应目录及class文件 src/main/resources目录中的文件 ...