简单介绍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. linux中添加常用应用程序的桌面图标

    在网上随处可以找到怎么样把应用程序的图标放到桌面上,我刚用ubuntu时也是按照网上的做法,一步一步的做的,现将网上的做法复制下来: 桌面配置文件简述\label{sec:desktop file} ...

  2. 2018-3-31-C#-谁改了我的代码

    title author date CreateTime categories C# 谁改了我的代码 lindexi 2018-3-31 21:15:3 +0800 2018-2-13 17:23:3 ...

  3. H3C 根据子网数划分子网

  4. linux 位操作

    atomic_t 类型在进行整数算术时是不错的. 但是, 它无法工作的好, 当你需要以原子方 式操作单个位时. 为此, 内核提供了一套函数来原子地修改或测试单个位. 因为整个操作 在单步内发生, 没有 ...

  5. Hamcrest匹配器框架

    其实在之前的文章中已经使用过 Hamcrest 匹配器框架,本篇文章将系统的介绍它的使用. 为什么要用Hamcrest匹配器框架 Hamcrest是一款软件测试框架, 可以通过现有的匹配器类检查代码中 ...

  6. dotnet core 使用 GBK 编码

    本文告诉大家如何在 .NET Core 中使用 GBK 编码 默认的 .NET Core 框架不包含 GBK 编码,不包含除了代码页为 28591 和 Unicode(utf-8,utf-16) 之外 ...

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

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

  8. Python11_文件的读写

    1.打开和关闭文件(文件对象的方法open,close) file object = open(file_name [, access_mode][, buffering]) 各个参数的细节如下: f ...

  9. c3p0连接池封装

    在处理数据库事物时需要同一个Connection  但是dbcp无法获得  单独工具也显得繁琐,改进成c3p0工具类: package utils; import java.sql.Connectio ...

  10. vagrant在windows下的安装和配置(一)

    记录一下安装和配置过程中的一些坑步骤一分别下载vagrant和VirtualBox,我这里下载的是vagrant_1.9.1.msi 和 VirtualBox-5.1.14-112924-Win.ex ...