简单介绍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内部实现影响。查找顺序的规则如下。

  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/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开始查找。查找顺序如下。

  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最近发布的代码浏览工具: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有一个更好的理解。

Android Linker简介的更多相关文章

  1. Android.mk简介:

    Android.mk简介: Android.mk文件用来告知NDK Build 系统关于Source的信息. Android.mk将是GNU Makefile的一部分,且将被Build System解 ...

  2. Android Studio 简介及导入 jar 包和第三方开源库方[转]

    原文:http://blog.sina.com.cn/s/blog_693301190102v6au.html Android Studio 简介 几天前的晚上突然又想使用 Android Studi ...

  3. "浅谈Android"第一篇:Android系统简介

    近来,看了一本书,名字叫做<第一行代码>,是CSDN一名博主写的,一本Android入门级的书,比较适合新手.看了书之后,有感而发,想来进行Android开发已经有一年多了,但欠缺系统化的 ...

  4. 【译】Android系统简介—— Activity

    续上一篇,继续介绍Android系统.上一篇: [译]Android系统简介 本文主要介绍构建Android应用的一些主要概念: Activity Activity是应用程序中一个单独的有UI的页面( ...

  5. 被遗忘的Android mipmaps简介

    被遗忘的 Android mipmaps 简介 [导读]已经发布的 Android Studio1.1 版本是一个 bug 修复版本.在这个版本中,当你创建工程时一项改变将会吸引你的眼球.工程创建登陆 ...

  6. Android系统简介(中):系统架构

    Android的系统架构栈分为4层,从上往下分别是Applications.Application framework.Libraries  & Android Runtime.Linux  ...

  7. Android系统简介(上):历史渊源

    上个月,看到微信的一系列文章,讲到Linux的鼻祖-李纳斯的传记<Just for Fun>, 其人神乎其能, 其人生过程非常有趣,值得每个程序员细细品味. 而实际上,对我而已,虽然做软件 ...

  8. Android ART简介

    一.    Android ART简介 Android DEX/ODEX/OAT文件

  9. Android插件简介

    /** * @actor Steffen.D * @time 2015.02.06 * @blog http://www.cnblogs.com/steffen */ Android插件简介 Andr ...

随机推荐

  1. python模块之random模块

    random模块 随机模块,用于处理随机问题. import random # 随机整数 print(random.randint(0, 9)) # 0到9之间随机一个整数 print(random. ...

  2. vue组件中data是个函数

    当我们const vm = new Vue({ el : '#app',   data : { msg:‘hello World’ } })用习惯了,data是一个对象,可到了vue组件 Vue.co ...

  3. 深入理解Jvm--Java静态分配和动态分配完全解析

    jvm中分配Dispatch的概念 分派是针对方法而言的,指的是方法确定的过程,通常发生在方法调用的过程中.分派根据方法选择的发生时机可以分为静态分派和动态分派,其中对于动态分派,根据宗量种数又可以分 ...

  4. linux 后备缓存

    一个设备驱动常常以反复分配许多相同大小的对象而结束. 如果内核已经维护了一套相同 大小对象的内存池, 为什么不增加一些特殊的内存池给这些高容量的对象? 实际上, 内核 确实实现了一个设施来创建这类内存 ...

  5. linux 运行处理者

    如同前面建议的, 当内核收到一个中断, 所有的注册的处理者被调用. 一个共享的处理者 必须能够在它需要的处理的中断和其他设备产生的中断之间区分. 使用 shared=1 选项来加载 short 安装了 ...

  6. C语言 屏幕截图 (GDI)

    截取全屏幕 #include <windows.h>   void echo(CHAR *str); int CaptureImage(HWND hWnd, CHAR *dirPath, ...

  7. 【k8s】kubeadm快速部署Kubernetes

    1.Kubernetes 架构图 kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具. 这个工具能通过两条指令完成一个kubernetes集群的部署: # 创建一个 Mast ...

  8. CodeTypeDeclaration,CodeMemberProperty动态生成代码

    由于是CodeDom些列,所以先介绍几个CodeDom表达式: :CodeConditionStatement:判断语句即是if(condition){} else{},看最全的那个构造函数: pub ...

  9. 在win64上使用bypy进行百度网盘文件上传

    阿里云服务器的带宽为2M,网站每日的备份包都3G多了,离线下载太费时间了,打算每日将备份包自动上传到自己的百度云盘里.1.先安装Python 执行python -V ,发现没安装python2.去py ...

  10. Spring Boot中路径及配置文件读取问题

    编译时src/main/java中*.java文件会被编译成*.class文件,在classpath中创建对应目录及class文件           src/main/resources目录中的文件 ...