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內部實現影響。查找順序的規則如下。

  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有一個更好的理解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM