(六) Mach-O 文件的動(dòng)態(tài)鏈接、庫、Dyld(含dlopen)

# 動(dòng)態(tài)鏈接
# 庫:靜態(tài)庫和動(dòng)態(tài)庫
  ## 靜態(tài)庫
  ## 動(dòng)態(tài)庫
  ## 非常重要的LibSystem庫
  ## 補(bǔ)充兩個(gè)概念:程序模塊、映像image
  ## .a/.dylib與.framework的區(qū)別
# Mach-O 文件的動(dòng)態(tài)鏈接 —— dyld引入
# dyld工作流程詳解
  ## _dyld_start
  ## dyldbootstrap::start()
  ## dyld::_main()
# 小結(jié)
# 加載動(dòng)態(tài)庫的另一種方式:顯式運(yùn)行時(shí)鏈接dlopen、dlsym
# 參考鏈接

# 動(dòng)態(tài)鏈接

動(dòng)態(tài)鏈接的基本思想是把程序按照模塊拆分成各個(gè)相對獨(dú)立部分,在程序運(yùn)行時(shí)才將它們鏈接在一起形成一個(gè)完整的程序,而不是像靜態(tài)鏈接一樣把所有的程序模塊都鏈接成一個(gè)個(gè)單獨(dú)的可執(zhí)行文件。

動(dòng)態(tài)鏈接涉及運(yùn)行時(shí)的鏈接及多個(gè)文件的裝載,必需要有操作系統(tǒng)的支持,因?yàn)閯?dòng)態(tài)鏈接的情況下,進(jìn)程的虛擬地址空間的分布會比靜態(tài)鏈接情況下更為復(fù)雜,還有一些存儲管理、內(nèi)存共享、進(jìn)程線程等機(jī)制在動(dòng)態(tài)鏈接下也會有一些微妙的變化。目前主流的操作系統(tǒng)幾乎都支持動(dòng)態(tài)鏈接這種方式。

# 庫:靜態(tài)庫和動(dòng)態(tài)庫

庫(Library),是我們在開發(fā)中的重要角色,庫的作用在于代碼共享、模塊分割以及提升良好的工程管理實(shí)踐。說白了就是一段編譯好的二進(jìn)制代碼,加上頭文件就可以供別人使用。

為什么要用庫?一種情況是某些代碼需要給別人使用,但是我們不希望別人看到源碼,就需要以庫的形式進(jìn)行封裝,只暴露出頭文件(靜態(tài)庫和動(dòng)態(tài)庫的共同點(diǎn)就是不會暴露內(nèi)部具體的代碼信息)。另外一種情況是,對于某些不會進(jìn)行大的改動(dòng)的代碼,我們想減少編譯的時(shí)間,就可以把它打包成庫,因?yàn)閹焓且呀?jīng)編譯好的二進(jìn)制了,編譯的時(shí)候只需要 Link 一下,不會浪費(fèi)編譯時(shí)間。

根據(jù)庫在使用的時(shí)候 Link 時(shí)機(jī)或者說方式(靜態(tài)鏈接、動(dòng)態(tài)鏈接),庫分為靜態(tài)庫和動(dòng)態(tài)庫。

## 靜態(tài)庫

靜態(tài)庫即靜態(tài)鏈接庫(Windows 下的 .lib,linux 下的.a,Mac 下的 .a .framework)。之所以叫做靜態(tài),是因?yàn)殪o態(tài)庫在鏈接時(shí)會被完整地拷貝一份到可執(zhí)行文件中(會使最終的可執(zhí)行文件體積增大)。被多個(gè)程序使用就會有多份冗余拷貝。如果更新靜態(tài)庫,需要重新編譯一次可執(zhí)行文件,重新鏈接新的靜態(tài)庫。

## 動(dòng)態(tài)庫

動(dòng)態(tài)庫即動(dòng)態(tài)鏈接庫。與靜態(tài)庫相反,動(dòng)態(tài)庫在編譯時(shí)并不會被拷貝到可執(zhí)行文件中,可執(zhí)行文件中只會存儲指向動(dòng)態(tài)庫的引用(使用了動(dòng)態(tài)庫的符號、及對應(yīng)庫的路徑等)。等到程序運(yùn)行時(shí),動(dòng)態(tài)庫才會被真正加載進(jìn)來,此時(shí),先根據(jù)記錄的庫路徑找到對應(yīng)的庫,再通過記錄的名字符號找到綁定的地址。

動(dòng)態(tài)庫的優(yōu)點(diǎn)是:

  • 減少可執(zhí)行文件體積:相比靜態(tài)鏈接,動(dòng)態(tài)鏈接在編譯時(shí)不需要打進(jìn)去(不需要拷貝到每個(gè)可執(zhí)行文件中),所以可執(zhí)行文件的體積要小很多。
  • 代碼共用:很多程序都動(dòng)態(tài)鏈接了這些 lib,但它們在內(nèi)存和磁盤中中只有一份(因?yàn)檫@個(gè)原因,動(dòng)態(tài)庫也被稱作共享庫)。
  • 易于維護(hù):使用動(dòng)態(tài)庫,可以不重新編譯連接可執(zhí)行程序的前提下,更新動(dòng)態(tài)庫文件達(dá)到更新應(yīng)用程序的目的。

常見的可執(zhí)行文件的形式:

  • Linux系統(tǒng)中,ELF動(dòng)態(tài)鏈接文件被稱為動(dòng)態(tài)共享對象(DSO,Dynamic SharedObjects),簡稱共享對象,一般都是以 .so 為擴(kuò)展名的一些文件;
  • Windows系統(tǒng)中,動(dòng)態(tài)鏈接文件被稱為動(dòng)態(tài)鏈接庫(Dynamical Linking Library),通常就是我們平時(shí)很常見的以 .dll 為擴(kuò)展名的文件;
  • OS X 和其他 UN*X 不同,它的庫不是“共享對象(.so)”,因?yàn)?OS X 和 ELF 不兼容,而且這個(gè)概念在 Mach-O 中不存在。OS 中的動(dòng)態(tài)鏈接文件一般稱為動(dòng)態(tài)庫文件,帶有 .dylib.framework及鏈接符號.tbd。可以在 /usr/lib 目錄下找到(這一點(diǎn)和其他所有的 UN*X 一樣,不過在OS X 和 iOS 中沒有/lib目錄)
  • OS X 與其他 UN*X 另一點(diǎn)不同是:沒有libc。開發(fā)者可能熟悉其他 UN*X 上的C運(yùn)行時(shí)庫(或Windows上的MSVCRT) 。但是在 OS X 上對應(yīng)的庫/usr/lib/libc.dylib只不過是指向libSystem.B.dylib的符號鏈接。
  • 以C語言運(yùn)行庫為例,補(bǔ)充一下運(yùn)行庫的概念:任何一個(gè)C程序,它的背后都有一套龐大的代碼來進(jìn)行支撐,以使得該程序能夠正常運(yùn)行。這套代碼至少包括入口函數(shù),及其所依賴的函數(shù)所構(gòu)成的函數(shù)集合。當(dāng)然,它還理應(yīng)包括各種標(biāo)準(zhǔn)庫函數(shù)的實(shí)現(xiàn)。這樣的一個(gè)代碼集合稱之為運(yùn)行時(shí)庫(Runtime Library)。而C語言的運(yùn)行庫,即被稱為C運(yùn)行庫(CRT)。運(yùn)行庫顧名思義是讓程序能正常運(yùn)行的一個(gè)庫。

## 兩個(gè)非常重要的庫:LibSystem、libobjc

libSystem 提供了 LibC(運(yùn)行庫) 的功能,還包含了在其他 UN*X 上原本由其他一些庫提供的功能,列幾個(gè)熟知的:

  • GCD libdispatch
  • C語言庫 libsystem_c
  • Block libsystem_blocks
  • 加密庫(比如常見的md5函數(shù)) libcommonCrypto

還有些庫(如數(shù)學(xué)庫 libm、線程庫 libpthread)雖然在/usr/lib中看到雖然有這些庫的文件,但都是libSystem.B.dylib的替身/快捷方式,即都是指向libSystem的符號鏈接。

libSystem 庫是系統(tǒng)上所有二進(jìn)制代碼的絕對先決條件,即所有的二進(jìn)制文件都依賴這個(gè)庫,不論是C、C++還是Objective-C的程序。這是因?yàn)檫@個(gè)庫是對底層系統(tǒng)調(diào)用和內(nèi)核服務(wù)的接口,如果沒有這些接口就什么事也干不了。這個(gè)庫還是/usr/ib/system目錄下一些庫的保護(hù)傘庫(通過LC_REEXPORT_LIB加載命令重新導(dǎo)出了符號) 。

總結(jié)來說:libSystem在運(yùn)行庫的基礎(chǔ)上,增加了一些對底層系統(tǒng)調(diào)用和內(nèi)核服務(wù)的抽象接口。所以在下面的流程中,會發(fā)現(xiàn)libSystem是先于其他動(dòng)態(tài)庫初始化的。

libobjc與libsystem一樣,都是默認(rèn)添加的lib,包含iOS開發(fā)天天接觸的objc runtime.

## 補(bǔ)充兩個(gè)概念

  • 程序模塊:從本質(zhì)上講,普通可執(zhí)行程序和動(dòng)態(tài)庫中都包含指令和數(shù)據(jù),這一點(diǎn)沒有區(qū)別。在使用動(dòng)態(tài)庫的情況下,程序本身被分為了程序主要模塊(Program1)和動(dòng)態(tài)鏈接文件(Lib.so Lib.dylib Lib.dll),但實(shí)際上它們都可以看作是整個(gè)程序的一個(gè)模塊,所以當(dāng)我們提到程序模塊時(shí)可以指程序主模塊也可以指動(dòng)態(tài)鏈接庫。
  • 映像(image) ,通常也是指這兩者??蓤?zhí)行文件/動(dòng)態(tài)鏈接文件,在裝載時(shí)被直接映射到進(jìn)程的虛擬地址空間中運(yùn)行,它是進(jìn)程的虛擬空間的映像,所以很多時(shí)候,也被叫做映像/鏡像文件(Image File)。

## .a/.dylib與.framework的區(qū)別

前者是純二進(jìn)制文件,文件不能直接使用,需要有.h文件的配合(我們在使用系統(tǒng)的.dylib動(dòng)態(tài)庫時(shí),經(jīng)常發(fā)現(xiàn)沒有頭文件,其實(shí)這些庫的頭文件都位于一個(gè)已知位置,如usr/include(新系統(tǒng)中這個(gè)文件夾由SDK附帶了,見 [/usr/include missing on macOS Catalina (with Xcode 11)] ),庫文件位于usr/lib,使得這些庫全局可用),后者除了二進(jìn)制文件、頭文件還有資源文件,代碼可以直接導(dǎo)入使用(.a + .h + sourceFile = .framework)。

Framework 是蘋果公司的 Cocoa/Cocoa Touch 程序中使用的一種資源打包方式,可以將代碼文件、頭文件、資源文件(nib/xib、圖片、國際化文本)、說明文檔等集中在一起,方便開發(fā)者使用。Framework 其實(shí)是資源打包的方式,和靜態(tài)庫動(dòng)態(tài)庫的本質(zhì)是沒有什么關(guān)系(所以framework文件可以是靜態(tài)庫也可以是動(dòng)態(tài)庫,iOS 中用到的所有系統(tǒng) framework 都是動(dòng)態(tài)鏈接的)。

在其它大部分平臺上,動(dòng)態(tài)庫都可以用于不同應(yīng)用間共享, 共享可執(zhí)行文件,這就大大節(jié)省了內(nèi)存。但是iOS平臺在 iOS 8 之前,蘋果不允許第三方框架使用動(dòng)態(tài)方式加載,開發(fā)者可以使用的動(dòng)態(tài) Framework 只有蘋果系統(tǒng)提供的 UIKit.Framework,F(xiàn)oundation.Framework 等。開發(fā)者要進(jìn)行模塊化,只能打包成靜態(tài)庫文件:.a + 頭文件、.framework(這時(shí)候的 Framework 只支持打包成靜態(tài)庫的 Framework),前種方式打包不夠方便,使用時(shí)也比較麻煩,沒有后者的便捷性。

iOS 8/Xcode 6 推出之后,允許開發(fā)者有條件地創(chuàng)建和使用動(dòng)態(tài)庫,支持了動(dòng)態(tài) Framework。開發(fā)者打包的動(dòng)態(tài) Framework 和系統(tǒng)的 UIKit.Framework 還是有很大區(qū)別。后者不需要拷貝到目標(biāo)程序中,是一個(gè)鏈接。而前者在打包和提交 app 時(shí)會被放到 app main bundle 的根目錄中,運(yùn)行在沙盒里,而不是系統(tǒng)中。也就是說,不同的 app 就算使用了同樣的 framework,但還是會有多份的框架被分別簽名,打包和加載,因此蘋果又把這種 Framework 稱為 Embedded Framework(可植入性 Framework)。

不過 iOS8 上開放了 App Extension 功能,可以為一個(gè)應(yīng)用創(chuàng)建插件,這樣主app和插件之間共享動(dòng)態(tài)庫還是可行的。

數(shù)量上,蘋果公司建議最多使用6個(gè)非系統(tǒng)動(dòng)態(tài)庫。

然后就是,在上傳App Store打包的時(shí)候,蘋果會對我們的代碼進(jìn)行一次 Code Singing,包括 app 可執(zhí)行文件和所有Embedded 的動(dòng)態(tài)庫,所以如果是動(dòng)態(tài)從服務(wù)器更新的動(dòng)態(tài)庫,是簽名不了的,sandbox驗(yàn)證動(dòng)態(tài)庫的簽名非法時(shí),就會造成crash。因此應(yīng)用插件化、軟件版本實(shí)時(shí)模塊升級等功能在iOS上無法實(shí)現(xiàn)。不過在 in house(企業(yè)發(fā)布) 包和develop 包中可以使用。

# Mach-O 文件的動(dòng)態(tài)鏈接 —— dyld引入

Mach-O 文件的裝載完成,即內(nèi)核加載器做完相關(guān)的工作后,對于需要?jiǎng)討B(tài)鏈接(使用了動(dòng)態(tài)庫)的可執(zhí)行文件(大部分可執(zhí)行文件都是動(dòng)態(tài)鏈接的)來說,控制權(quán)會轉(zhuǎn)交給鏈接器,鏈接器進(jìn)而接著處理文件頭中的其他加載命令。真正的庫加載和符號解析的工作都是通過LC_LOAD_DYLINKER加載命令指定的動(dòng)態(tài)鏈接器在用戶態(tài)完成的。通常情況下,使用的是 /usr/lib/dyld 作為動(dòng)態(tài)鏈接器,不過這條加載命令可以指定任何程序作為參數(shù)。

鏈接器接管剛創(chuàng)建的進(jìn)程的控制權(quán),因?yàn)閮?nèi)核將進(jìn)程的入口點(diǎn)設(shè)置為鏈接器的入口點(diǎn)。

dyld是一個(gè)用戶態(tài)的進(jìn)程。dyld不屬于內(nèi)核的一部分,而是作為一個(gè)單獨(dú)的開源項(xiàng)目由蘋果進(jìn)行維護(hù)的(當(dāng)然也屬于Darwin的一部分) ,點(diǎn)擊查看項(xiàng)目網(wǎng)址。從內(nèi)核的角度看,dyld是一個(gè)可插入的組件,可以替換為第三方的鏈接器。dyld對應(yīng)的二進(jìn)制文件有兩個(gè),分別是/usr/lib/dyld、/urs/lib/system/libdyld.dylib,前者通用二進(jìn)制格式(FAT),filetype為MH_DYLINKER,后者是普通的動(dòng)態(tài)鏈接庫格式(Mach-O)。

從調(diào)用堆棧上看dyld、libdyld.dylib的作用

前者dyld一段可執(zhí)行的程序,內(nèi)核將其映射至進(jìn)程地址空間,將控制權(quán)交給它進(jìn)行執(zhí)行,遞歸加載所需的動(dòng)態(tài)庫,其中也會將動(dòng)態(tài)鏈接器的另一種形式的libdyld.dylib加載,因?yàn)閯?dòng)態(tài)鏈接器dyld其不但在應(yīng)用的裝載階段起作用,在主程序運(yùn)行的時(shí)候,其充當(dāng)一個(gè)庫的角色,還提供了dlopen、dlsym等api,可以讓主程序顯式運(yùn)行時(shí)鏈接(見下文)。(關(guān)于這一點(diǎn),沒有找到明確的文檔說明。如果有人有正確的理解,請一定要評論區(qū)告訴我一下,感激不盡)

Linux中,動(dòng)態(tài)鏈接庫的存在形式稍有不同,Linux動(dòng)態(tài)鏈接器本身是一個(gè)共享對象(動(dòng)態(tài)庫),它的路徑是/lib/ld-linux.so.2,這實(shí)際上是個(gè)軟鏈接,它指向/lib/ld-x.y.z.so, 這個(gè)才是真正的動(dòng)態(tài)連接器文件。共享對象其實(shí)也是ELF文件,它也有跟可執(zhí)行文件一樣的ELF文件頭(包括e_entry、段表等)。動(dòng)態(tài)鏈接器是個(gè)非常特殊的共享對象,它不僅是個(gè)共享對象,還是個(gè)可執(zhí)行的程序,可以直接在命令行下面運(yùn)行。因?yàn)閘d.so是共享對象,又是動(dòng)態(tài)鏈接器,所以本來應(yīng)由動(dòng)態(tài)鏈接器進(jìn)行的共享對象的重定位,就要靠自己來,又稱“自舉”。自舉完成后ld.so以一個(gè)共享對象的角色,來實(shí)現(xiàn)動(dòng)態(tài)鏈接庫的功能。

我們需要了解一下LC_LOAD_DYLIB這個(gè)加載命令,這個(gè)命令會告訴鏈接器在哪里可以找到這些符號,即動(dòng)態(tài)庫的相關(guān)信息(ID、時(shí)間戳、版本號、兼容版本號等)。鏈接器要加載每一個(gè)指定的庫,并且搜尋匹配的符號。每個(gè)被鏈接的庫(Mach-O格式)都有一個(gè)符號表,符號表將符號名稱和地址關(guān)聯(lián)起來。符號表在Mach-O目標(biāo)文件中的地址可以通過LC_SYMTAB加載命令指定的 symoff 找到。對應(yīng)的符號名稱在 stroff, 總共有 nsyms 條符號信息。

下面是LC_SYMTAB的load_command:

struct dylib {
    union lc_str  name;             / library's path name /
    uint32_t timestamp;             / library's build time stamp /
    uint32_t current_version;       / library's current version number /
    uint32_t compatibility_version; / library's compatibility vers number /
};

struct dylib_command {
    uint32_t    cmd;        /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB, LC_REEXPORT_DYLIB */
    uint32_t    cmdsize;    /* includes pathname string */
    struct dylib    dylib;      /* the library identification */
};

在 <mach-o/dyld.h> 動(dòng)態(tài)庫頭文件中,也為我們提供了查詢所有動(dòng)態(tài)庫 image 的方法(也可以使用otool -L 文件路徑命令來查看,但看著沒代碼全):

#include <mach-o/dyld.h>
#include <stdio.h>

void listImages(){
    uint32_t i;
    uint32_t ic = _dyld_image_count();

    printf("Got %d images\n", ic);
    for (i = 0; i < ic; ++ i) {
        printf("%d: %p\t%s\t(slide: %p)\n",
               i,
               _dyld_get_image_header(i),
               _dyld_get_image_name(i),
               _dyld_get_image_vmaddr_slide(i));
    }
}

listImages();  //調(diào)用方法

log: 
  ...
  45: 0x1ab331000   /usr/lib/libobjc.A.dylib    (slide: 0x2b1b8000)
  46: 0x1e1767000   /usr/lib/libSystem.B.dylib  (slide: 0x2b1b8000)
  ...
  70: 0x107220000   /usr/lib/system/introspection/libdispatch.dylib (slide: 0x107220000)
  71: 0x1ab412000   /usr/lib/system/libdyld.dylib   (slide: 0x2b1b8000)
  ...

# dyld工作流程詳解

通過源碼來看一下dyld的工作流程,只是部分片段,詳細(xì)的可以下載源碼。

## __dyld_start

下面的匯編代碼很簡單,如果不清楚,可以看一下這篇匯編入門文章iOS需要了解的ARM64匯編

#if __arm64__
    .text
    .align 2
    .globl __dyld_start
__dyld_start:
; 操作fp棧幀寄存器,sp棧指針寄存器,配置函數(shù)棧幀
    mov     x28, sp
    and     sp, x28, #~15       // force 16-byte alignment of stack
    mov x0, #0
    mov x1, #0
    stp x1, x0, [sp, #-16]! // make aligned terminating frame
    mov fp, sp          // set up fp to point to terminating frame
    sub sp, sp, #16             // make room for local variables
; L(long 64位) P(point),在前面的匯編一文中,我們已經(jīng)知道:r0 - r30 是31個(gè)通用整形寄存器。每個(gè)寄存器可以存取一個(gè)64位大小的數(shù)。 
; 當(dāng)使用 x0 - x30訪問時(shí),它就是一個(gè)64位的數(shù)。
; 當(dāng)使用 w0 - w30訪問時(shí),訪問的是這些寄存器的低32位
#if __LP64__       
    ldr     x0, [x28]               // get app's mh into x0
    ldr     x1, [x28, #8]           // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
    add     x2, x28, #16            // get argv into x2
#else
    ldr     w0, [x28]               // get app's mh into x0
    ldr     w1, [x28, #4]           // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
    add     w2, w28, #8             // get argv into x2
#endif
    adrp    x3,___dso_handle@page
    add     x3,x3,___dso_handle@pageoff // get dyld's mh in to x4
    mov x4,sp                   // x5 has &startGlue
; 從上面的匯編代碼可以看到,主要是在設(shè)置dyldbootstrap::start函數(shù)調(diào)用棧的配置,在前面的匯編一文中,我們已經(jīng)知道函數(shù)的參數(shù),主要通過x0-x7幾個(gè)寄存器來傳遞
; 可以看到函數(shù)需要的幾個(gè)參數(shù)app_mh,argc,argv,dyld_mh,&startGlue分別被放置到了x0 x1 x2 x4 x5寄存器上
    ; call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
    bl  __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
    mov x16,x0                  // save entry point address in x16

## dyldbootstrap::start()

//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//  主要做的是dyld的引導(dǎo)工作,一般這個(gè)工作通常由 dyld 和 crt(C運(yùn)行時(shí)庫 C Run-Time Libray )來完成。但dyld自身加載的時(shí)候,只能由自己來做。
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
    // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
    // 如果有slide,那么需要重定位,必須在使用任何全局變量之前,進(jìn)行該操作
    rebaseDyld(dyldsMachHeader); 
    // kernel sets up env pointer to be just past end of agv array
    const char** envp = &argv[argc+1];  
    // kernel sets up apple pointer to be just past end of envp array
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;
    // 為stack canary設(shè)置一個(gè)隨機(jī)值
    // stack canary:棧的警惕標(biāo)志(stack canary),得名于煤礦里的金絲雀,用于探測該災(zāi)難的發(fā)生。具體辦法是在棧的返回地址的存儲位置之前放置一個(gè)整形值,該值在裝入程序時(shí)隨機(jī)確定。棧緩沖區(qū)攻擊時(shí)從低地址向高地址覆蓋??臻g,因此會在覆蓋返回地址之前就覆蓋了警惕標(biāo)志。返回返回前會檢查該警惕標(biāo)志是否被篡改。
    __guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
    // 執(zhí)行 dyld 中所有的C++初始化函數(shù)。run all C++ initializers inside dyld
    runDyldInitializers(argc, argv, envp, apple);
#endif
    // 完成所有引導(dǎo)工作,調(diào)用dyld::main(). now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = appsMachHeader->getSlide();
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

## dyld::_main()

dyld也是Mach-O文件格式的,文件頭中的 filetype 字段為MH_DYLINKER,區(qū)別與可執(zhí)行文件的 MH_EXECUTE,所以dyld也是有main()函數(shù)的(默認(rèn)名稱是mian(),也可以自己修改入口地址的)。

因?yàn)檫@個(gè)函數(shù)太長,寫在一起不好閱讀,所以按照流程功能點(diǎn),自上而下分為一個(gè)個(gè)代碼片段。關(guān)鍵的函數(shù)會在代碼中注釋說明

### 方法名及說明

// dyld的入口指針,內(nèi)核加載dyld,跳轉(zhuǎn)到__dyld_start函數(shù):進(jìn)行了一些寄存器設(shè)置,然后就調(diào)用了該函數(shù)。Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which sets up some registers and call this function.
// 返回主程序模塊的mian()函數(shù)地址,__dyld_start中會跳到該地址。Returns address of main() in target program which __dyld_start jumps to
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{

### 第一步 配置上下文信息,設(shè)置運(yùn)行環(huán)境,處理環(huán)境變量

    #pragma mark -- 第一步,設(shè)置運(yùn)行環(huán)境
    // Grab the cdHash of the main executable from the environment
    uint8_t mainExecutableCDHashBuffer[20];
    const uint8_t* mainExecutableCDHash = nullptr;
    if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
        // 獲取主程序的hash
        mainExecutableCDHash = mainExecutableCDHashBuffer;

#if !TARGET_OS_SIMULATOR
    // Trace dyld's load
    notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
    // Trace the main executable's load
    notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif

    uintptr_t result = 0;
    // 獲取主程序的macho_header結(jié)構(gòu)
    sMainExecutableMachHeader = mainExecutableMH;
    // 獲取主程序的slide值
    sMainExecutableSlide = mainExecutableSlide;
    ......
    CRSetCrashLogMessage("dyld: launch started");
    // 傳入Mach-O頭部以及一些參數(shù)設(shè)置上下文信息
    setContext(mainExecutableMH, argc, argv, envp, apple);

    // Pickup the pointer to the exec path.
    // 獲取主程序路徑
    sExecPath = _simple_getenv(apple, "executable_path");

    // <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
    if (!sExecPath) sExecPath = apple[0];
    ......
    if ( sExecPath[0] != '/' ) {
        // have relative path, use cwd to make absolute
        char cwdbuff[MAXPATHLEN];
        if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
            // maybe use static buffer to avoid calling malloc so early...
            char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
            strcpy(s, cwdbuff);
            strcat(s, "/");
            strcat(s, sExecPath);
            sExecPath = s;
        }
    }

    // Remember short name of process for later logging
    // 獲取進(jìn)程名稱
    sExecShortName = ::strrchr(sExecPath, '/');
    if ( sExecShortName != NULL )
        ++sExecShortName;
    else
        sExecShortName = sExecPath;

    // 配置進(jìn)程受限模式
    configureProcessRestrictions(mainExecutableMH, envp);
    ......
    // 檢測環(huán)境變量
    checkEnvironmentVariables(envp);
    // 在DYLD_FALLBACK為空時(shí)設(shè)置默認(rèn)值
    defaultUninitializedFallbackPaths(envp);
    ......
    // 如果設(shè)置了DYLD_PRINT_OPTS則調(diào)用printOptions()打印參數(shù)
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    // 如果設(shè)置了DYLD_PRINT_ENV則調(diào)用printEnvironmentVariables()打印環(huán)境變量
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    ......
    // 獲取當(dāng)前程序架構(gòu)
    getHostInfo(mainExecutableMH, mainExecutableSlide);

### 第二步 加載共享緩存

在iOS系統(tǒng)中,UIKit,F(xiàn)oundation等基礎(chǔ)庫是每個(gè)程序都依賴的,需要通過dyld(位于/usr/lib/dyld)一個(gè)一個(gè)加載到內(nèi)存,然而如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次,勢必造成運(yùn)行緩慢,為了優(yōu)化啟動(dòng)速度和提高程序性能,共享緩存機(jī)制就應(yīng)運(yùn)而生。iOS的dyld采用了一個(gè)共享庫預(yù)鏈接緩存,蘋果從iOS 3.0開始將所有的基礎(chǔ)庫都移到了這個(gè)緩存中,合并成一個(gè)大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下(OS X中是在/private/var/db/dyld目錄),按不同的架構(gòu)保存分別保存著,如dyld_shared_cache_armv7。而且在OS X中還有一個(gè)輔助的.map文件,而iOS中沒有。

如果在iOS上搜索大部分常見的庫,比如所有二進(jìn)制文件都依賴的libSystem,是搜索不到的,這個(gè)庫的文件不在文件系統(tǒng)中,而是被緩存文件包含。關(guān)于如何從共享緩存中提取我們想看的庫,可以參考鏈接dyld詳解第一部分

    #pragma mark -- 第二步,加載共享緩存 // load shared cache
    // 檢查共享緩存是否開啟,iOS必須開啟
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
      /*
       * mapSharedCache加載共享緩存庫,其中調(diào)用loadDyldCache函數(shù),展開loadDyldCache,有這么幾種情況:
         * 僅加載到當(dāng)前進(jìn)程mapCachePrivate(模擬器僅支持加載到當(dāng)前進(jìn)程)
         * 共享緩存是第一次被加載,就去做加載操作mapCacheSystemWide
         * 共享緩存不是第一次被加載,那么就不做任何處理
       */
      mapSharedCache();
    }
    ......

    try {
        // add dyld itself to UUID list
        addDyldImageToUUIDList();

### 第三步 實(shí)例化主程序

ImageLoader:前面已經(jīng)提到image(映像文件)常見的有可執(zhí)行文件、動(dòng)態(tài)鏈接庫。ImageLoader 作用是將這些文件加載進(jìn)內(nèi)存,且每一個(gè)文件對應(yīng)一個(gè)ImageLoader實(shí)例來負(fù)責(zé)加載。

從下面可以看到大概的順序:先將動(dòng)態(tài)鏈接的 image 遞歸加載,再依次進(jìn)行可執(zhí)行文件的鏈接。

        #pragma mark -- 第三步 實(shí)例化主程序,會實(shí)例化一個(gè)主程序ImageLoader
        // instantiate ImageLoader for main executable
        /*
         * 展開 instantiateFromLoadedImage 函數(shù), 可以看到主要分三步:
         *  isCompatibleMachO():檢查mach-o的subtype是否是當(dāng)前cpu可以支持;
         *  instantiateMainExecutable(): 就是實(shí)例化可執(zhí)行文件,這個(gè)期間會解析LoadCommand,這個(gè)之后會發(fā)送 dyld_image_state_mapped 通知;
         *  addImage(): 添加到 allImages中
         */
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

        // Now that shared cache is loaded, setup an versioned dylib overrides
    #if SUPPORT_VERSIONED_PATHS
        checkVersionedPaths();
    #endif

        // dyld_all_image_infos image list does not contain dyld
        // add it as dyldPath field in dyld_all_image_infos
        // for simulator, dyld_sim is in image list, need host dyld added
#if TARGET_OS_SIMULATOR
        // get path of host dyld from table of syscall vectors in host dyld
        void* addressInDyld = gSyscallHelpers;
#else
        // get path of dyld itself
        void*  addressInDyld = (void*)&__dso_handle;
#endif
        char dyldPathBuffer[MAXPATHLEN+1];
        int len = proc_regionfilename(getpid(), (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
        if ( len > 0 ) {
            dyldPathBuffer[len] = '\0'; // proc_regionfilename() does not zero terminate returned string
            if ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath) != 0 )
                gProcessInfo->dyldPath = strdup(dyldPathBuffer);
        }

### 第四步 加載插入的動(dòng)態(tài)庫

通過遍歷 DYLD_INSERT_LIBRARIES 環(huán)境變量,調(diào)用 loadInsertedDylib 加載。

在三方App的Mach-O文件中通過修改DYLD_INSERT_LIBRARIES的值來加入我們自己的動(dòng)態(tài)庫,從而注入代碼,hook別人的App。

        #pragma mark -- 第四步 加載插入的動(dòng)態(tài)庫
        // load any inserted libraries
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
        // record count of inserted libraries so that a flat search will look at 
        // inserted libraries, then main, then others.
        // 記錄插入的動(dòng)態(tài)庫數(shù)量
        sInsertedDylibCount = sAllImages.size()-1;

### 第五步 鏈接主程序

實(shí)例化之后就是動(dòng)態(tài)鏈接的過程。link 這個(gè)過程就是將加載進(jìn)來的二進(jìn)制變?yōu)榭捎脿顟B(tài)的過程。簡單來說就是:rebase => binding

  • rebase:就是針對 “mach-o在加載到虛擬內(nèi)存中不是固定的首地址” 這一現(xiàn)象做數(shù)據(jù)修正的過程。一般可執(zhí)行文件在沒有ASLR造成的首地址不固定的情況下, 裝載進(jìn)虛擬地址中的首地址都是固定的, 比如:Linux下一般都是0x08040000,Windows下一般都是0x0040000,Mach-O的TEXT地址在__PageZero之后的0x100000000地址.
  • binding:就是將這個(gè)二進(jìn)制調(diào)用的外部符號進(jìn)行綁定的過程。 比如我們objc代碼中需要使用到NSObject,即符號_OBJC_CLASS_$_NSObject,但是這個(gè)符號又不在我們的二進(jìn)制中,在系統(tǒng)庫 Foundation.framework中,因此就需要binding這個(gè)操作將對應(yīng)關(guān)系綁定到一起。
  • lazyBinding:就是在加載動(dòng)態(tài)庫的時(shí)候不會立即binding, 當(dāng)時(shí)當(dāng)?shù)谝淮握{(diào)用這個(gè)方法的時(shí)候再實(shí)施binding。 做到的方法也很簡單: 通過dyld_stub_binder這個(gè)符號來做。 lazy binding的方法第一次會調(diào)用到dyld_stub_binder, 然后dyld_stub_binder負(fù)責(zé)找到真實(shí)的方法,并且將地址bind到樁上,下一次就不用再bind了。
  • weakBinding:下方還有一步weakBinding
        #pragma mark -- 第五步 鏈接主程序
        // link main executable
        gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
        if ( mainExcutableAlreadyRebased ) {
            // previous link() on main executable has already adjusted its internal pointers for ASLR 
            // work around that by rebasing by inverse amount
            sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
        }
#endif
        /*
        link() 函數(shù)的遞歸調(diào)用函數(shù)堆棧形式
          ▼ ImageLoader::link() //啟動(dòng)主程序的連接進(jìn)程   —— ImageLoader.cpp,ImageLoader類中可以發(fā)現(xiàn)很多由dyld調(diào)用來實(shí)現(xiàn)二進(jìn)制加載邏輯的函數(shù)。
            ▼ recursiveLoadLibraries() //進(jìn)行所有需求動(dòng)態(tài)庫的加載
              ?? //確定所有需要的庫
              ▼ context.loadLibrary() //來逐個(gè)加載。context對象是一個(gè)簡單的結(jié)構(gòu)體,包含了在方法和函數(shù)之間傳遞的函數(shù)指針。這個(gè)結(jié)構(gòu)體的loadLibrary成員在libraryLocator()函數(shù)(dyld.cpp)中初始化,它完成的功能也只是簡單的調(diào)用load()函數(shù)。
                ▼ load() // 源碼在dyld.cpp,會調(diào)用各種幫助函數(shù)。
                  ?? loadPhase0() → loadPhase1() → ... → loadPhase5() → loadPhase5load() → loadPhase5open() → loadPhase6() 遞歸調(diào)用  //每一個(gè)函數(shù)都負(fù)責(zé)加載進(jìn)程工作的一個(gè)具體任務(wù)。比如,解析路徑或者處理會影響加載進(jìn)程的環(huán)境變量。
                  ▼ loadPhase6() // 該函數(shù)從文件系統(tǒng)加載需求的dylib到內(nèi)存中。然后調(diào)用一個(gè)ImageLoaderMachO類的實(shí)例對象。來完成每個(gè)dylib對象Mach-O文件具體的加載和連接邏輯。
         */
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        sMainExecutable->setNeverUnloadRecursive();
        if ( sMainExecutable->forceFlat() ) {
            gLinkContext.bindFlat = true;
            gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
        }

### 第六步 鏈接插入的動(dòng)態(tài)庫

        #pragma mark -- 第六步 鏈接插入的動(dòng)態(tài)庫
        // link any inserted libraries
        // do this after linking main executable so that any dylibs pulled in by inserted 
        // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
                image->setNeverUnloadRecursive();
            }
            // only INSERTED libraries can interpose
            // register interposing info after all inserted libraries are bound so chaining works
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                image->registerInterposing(gLinkContext);
            }
        }

        // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
        for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
            ImageLoader* image = sAllImages[i];
            if ( image->inSharedCache() )
                continue;
            image->registerInterposing(gLinkContext);
        }
        ......

        // apply interposing to initial set of images
        for(int i=0; i < sImageRoots.size(); ++i) {
            sImageRoots[i]->applyInterposing(gLinkContext);
        }
        gLinkContext.notifyBatch(dyld_image_state_bound, false);

        // Bind and notify for the inserted images now interposing has been registered
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
            }
        }

### 第七步 弱符號綁定

        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        #pragma mark -- 第七步 執(zhí)行弱符號綁定。weakBind: 從代碼中可以看出這一步會對所有含有弱符號的鏡像合并排序進(jìn)行bind。OC中沒發(fā)現(xiàn)應(yīng)用場景,可能是C++的吧
        sMainExecutable->weakBind(gLinkContext);
        gLinkContext.linkingMainExecutable = false;

        sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);

        CRSetCrashLogMessage("dyld: launch, running initializers");
        ......  

### 第八步 執(zhí)行初始化方法

dyld會優(yōu)先初始化動(dòng)態(tài)庫,然后初始化主程序。

        #pragma mark -- 第八步 執(zhí)行初始化方法initialize() 
        // run all initializers
        //attribute((constructor)) 修飾的函數(shù)就是在這一步執(zhí)行的, 即在主程序的main()函數(shù)之前。__DATA中有個(gè)Section __mod_init_func就是記錄這些函數(shù)的。
        //與之對應(yīng)的是attribute((destructor))修飾的函數(shù), 是主程序 main() 執(zhí)行之后的一些全局函數(shù)析構(gòu)操作, 也是記錄在一個(gè)Section __mod_term_func中.
        /*
        initializeMainExecutable()函數(shù)的遞歸調(diào)用函數(shù)堆棧形式:
          ?? 先初始化動(dòng)態(tài)庫,for(size_t i=1; i < rootCount; ++i) { sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]); }  // run initialzers for any inserted dylibs
          ▼ 再初始化可執(zhí)行文件 sMainExecutable->runInitializers()  // run initializers for main executable and everything it brings up 
            ▼ ImageLoader::processInitializers()
              ▼ ImageLoader::recursiveInitialization()  // 循環(huán)遍歷images list中所有的imageloader,recursive(遞歸)初始化。Calling recursive init on all images in images list
                ▼ ImageLoaderMachO::doInitialization()  // 初始化這個(gè)image. initialize this image
                  ▼ ImageLoaderMachO::doImageInit()  //解析LC_ROUTINES_COMMAND 這個(gè)加載命令,可以參考loader.h中該命令的說明,這個(gè)命令包含了動(dòng)態(tài)共享庫初始化函數(shù)的地址,該函數(shù)必須在庫中任意模塊初始化函數(shù)(如C++ 靜態(tài)構(gòu)造函數(shù)等)之前調(diào)用
                  ▼ ImageLoaderMachO::doModInitFunctions()  // 內(nèi)部會調(diào)用C++全局對象的構(gòu)造函數(shù)、__attribute__((constructor))修飾的C函數(shù)
                  // 以上兩個(gè)函數(shù)中,libSystem相關(guān)的都是要首先執(zhí)行的,而且在上述遞歸加載動(dòng)態(tài)庫過程,libSystem是默認(rèn)引入的,所以棧中會出現(xiàn)libSystem_initializer的初始化方法
          ?? (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
        */
        initializeMainExecutable(); 

        // 通知所有的監(jiān)視進(jìn)程,本進(jìn)程要進(jìn)入main()函數(shù)了。 notify any montoring proccesses that this process is about to enter main()
        notifyMonitoringDyldMain();
        ......

在上面的doImageInit、doModInitFunctions函數(shù)中,會發(fā)現(xiàn)都有判斷libSystem庫是否已加載的代碼,即libSystem要首先加載、初始化。在上文中,我們已經(jīng)強(qiáng)調(diào)了這個(gè)庫的重要性。之所以在這里又提到,是因?yàn)檫@個(gè)庫也起到了將dyld與objc關(guān)聯(lián)起來的作用:

可以從上面的調(diào)用堆棧中看到,從dyld到objc的流程,下面來插一段objc的源碼objc-os.mm_object_init函數(shù)的實(shí)現(xiàn):

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // 各種初始化
    environ_init();
    tls_init();
    static_init();
    lock_init();
    // 看了一下exception_init是空實(shí)現(xiàn)??!就是說objc的異常是完全采用c++那一套的。
    exception_init();
   // 注冊dyld事件的監(jiān)聽,該方法是dyld提供的,內(nèi)部調(diào)用了dyld::registerObjCNotifiers這個(gè)方法,記錄了這三個(gè)分別對應(yīng)map,init,unmap事件的回調(diào)函數(shù),會在相應(yīng)時(shí)機(jī)觸發(fā)
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

這三個(gè)函數(shù)就很熟悉了,位于objc-runtime-new.mm中,objc運(yùn)行時(shí)老生常談的幾個(gè)方法(關(guān)于OBJC的部分,內(nèi)容太多,這里簡單介紹,下篇細(xì)談),每次有新的鏡像加載時(shí)都會在指定時(shí)機(jī)觸發(fā)這幾個(gè)方法:

  • map_images : 每當(dāng) dyld 將一個(gè) image 加載進(jìn)內(nèi)存時(shí) , 會觸發(fā)該函數(shù)進(jìn)行image的一些處理:如果是首次,初始化執(zhí)行環(huán)境等,之后_read_images進(jìn)行讀取,進(jìn)行類、元類、方法、協(xié)議、分類的一些加載。
  • load_images : 每當(dāng) dyld 初始化一個(gè) image 會觸發(fā)該方法,會對該 image 進(jìn)行+load的調(diào)用
  • unmap_image : 每當(dāng) dyld 將一個(gè) image 移除時(shí) , 會觸發(fā)該函數(shù)
引圖自 https://juejin.im/post/6844904068867948552#heading-0

值得說明的是,這個(gè)初始化的過程遠(yuǎn)比寫出來的要復(fù)雜,這里只提到了 runtime 這個(gè)分支,還有像 GCD、XPC 等重頭的系統(tǒng)庫初始化分支沒有提及(當(dāng)然,有緩存機(jī)制在,也不會重復(fù)初始化),總結(jié)起來就是 main 函數(shù)執(zhí)行之前,系統(tǒng)做了非常多的加載和初始化工作,但都被很好的隱藏了,我們無需關(guān)心。

然后,從上面最后的代碼(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL); 以及注釋register cxa_atexit() handler to run static terminators in all loaded images when this process exits可以看出注冊了cxa_atexit()函數(shù),當(dāng)此進(jìn)程退出時(shí),該處理程序會運(yùn)行所有加載的image中的靜態(tài)終止程序(static terminators)。

### 第九步 查找主程序入口點(diǎn)并返回,__dyld_start會跳轉(zhuǎn)進(jìn)入

        #pragma mark -- 第九步 查找入口點(diǎn) main() 并返回,調(diào)用 getEntryFromLC_MAIN,從 Load Command 讀取LC_MAIN入口,如果沒有LC_MAIN入口,就讀取LC_UNIXTHREAD,然后跳到主程序的入口處執(zhí)行
        // find entry point for main executable
        result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
        if ( result != 0 ) {
            // main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
            if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
                *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
            else
                halt("libdyld.dylib support not present for LC_MAIN");
        }
        else {
            // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
            result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
            *startGlue = 0;
        }
    ......

    catch(const char* message) {
        syncAllImages();
        halt(message);
    }
    catch(...) {
        dyld::log("dyld: launch failed\n");
    }
    ......
    return result;
}

# 小結(jié)

引自iOS 程序 main 函數(shù)之前發(fā)生了什么一文中的片段,《 Mike Ash 這篇 blog 》對 dyld 作用順序的概括:

  1. 從 kernel 留下的原始調(diào)用棧引導(dǎo)和啟動(dòng)自己
  2. 將程序依賴的動(dòng)態(tài)鏈接庫遞歸加載進(jìn)內(nèi)存,當(dāng)然這里有緩存機(jī)制
  3. non-lazy 符號立即 link 到可執(zhí)行文件,lazy 的存表里
  4. Runs static initializers for the executable
  5. 找到可執(zhí)行文件的 main 函數(shù),準(zhǔn)備參數(shù)并調(diào)用
  6. 程序執(zhí)行中負(fù)責(zé)綁定 lazy 符號、提供 runtime dynamic loading services、提供調(diào)試器接口
  7. 程序main函數(shù) return 后執(zhí)行 static terminator
  8. 某些場景下 main 函數(shù)結(jié)束后調(diào) libSystem 的 _exit 函數(shù)

然后,使用調(diào)用堆棧,來看下dyld的工作流程,只注釋了認(rèn)為重要的部分。

#pragma mark -- 內(nèi)核XNU加載Mach-O
#pragma mark -- 從 XNU內(nèi)核態(tài) 將控制權(quán)轉(zhuǎn)移到 dyld用戶態(tài)
▼ dyld
  ▼ __dyld_start   // 源碼在dyldStartup.s這個(gè)文件,用匯編實(shí)現(xiàn)
    ▼ dyldbootstrap::start()   //dyldInitialization.cpp,負(fù)責(zé)dyld的引導(dǎo)工作
      ▼ dyld::_main()   // dyld.cpp
        ?? // 第一步,設(shè)置運(yùn)行環(huán)境
        ?? // 第二步,加載共享緩存
        ?? // 第三步 實(shí)例化主程序,會實(shí)例化一個(gè)主程序ImageLoader
        ▼ instantiateFromLoadedImage()  
          ?? isCompatibleMachO()  // 檢查mach-o的subtype是否是當(dāng)前cpu可以支持;
          ?? instantiateMainExecutable()  // 實(shí)例化可執(zhí)行文件,這個(gè)期間會解析LoadCommand,這個(gè)之后會發(fā)送 dyld_image_state_mapped 通知;
          ?? addImage()  // 將可執(zhí)行文件這個(gè)image,添加到 allImages中
        ?? // 第四步,循環(huán)調(diào)用該函數(shù),加載插入的動(dòng)態(tài)庫
        ?? loadInsertedDylib()  
        ?? // 第五步,調(diào)用link()函數(shù),鏈接主程序
        ▼ link()  
          ▼ ImageLoader::link() //啟動(dòng)主程序的連接進(jìn)程   —— ImageLoader.cpp,ImageLoader類中可以發(fā)現(xiàn)很多由dyld調(diào)用來實(shí)現(xiàn)二進(jìn)制加載邏輯的函數(shù)。
            ▼ recursiveLoadLibraries() //進(jìn)行所有需求動(dòng)態(tài)庫的加載
              ?? //確定所有需要的庫
              ▼ context.loadLibrary() //來逐個(gè)加載。context對象是一個(gè)簡單的結(jié)構(gòu)體,包含了在方法和函數(shù)之間傳遞的函數(shù)指針。這個(gè)結(jié)構(gòu)體的loadLibrary成員在libraryLocator()函數(shù)(dyld.cpp)中初始化,它完成的功能也只是簡單的調(diào)用load()函數(shù)。
                ▼ load() // 源碼在dyld.cpp,會調(diào)用各種幫助函數(shù)。
                  ?? loadPhase0() → loadPhase1() → ... → loadPhase5() → loadPhase5load() → loadPhase5open() → loadPhase6() 遞歸調(diào)用  //每一個(gè)函數(shù)都負(fù)責(zé)加載進(jìn)程工作的一個(gè)具體任務(wù)。比如,解析路徑或者處理會影響加載進(jìn)程的環(huán)境變量。
                  ▼ loadPhase6() // 該函數(shù)從文件系統(tǒng)加載需求的dylib到內(nèi)存中。然后調(diào)用一個(gè)ImageLoaderMachO類的實(shí)例對象。來完成每個(gè)dylib對象Mach-O文件具體的加載和連接邏輯。
        ?? // 第六步,調(diào)用link()函數(shù),鏈接插入的動(dòng)態(tài)庫
        ?? // 第七步,對主程序進(jìn)行弱符號綁定weakBind
        ?? sMainExecutable->weakBind(gLinkContext);
        ?? // 第八步,執(zhí)行初始化方法 initialize。attribute((constructor)) 修飾的函數(shù)就是在這一步執(zhí)行的, 即在主程序的main()函數(shù)之前。__DATA中有個(gè)Section __mod_init_func就是記錄這些函數(shù)的。
        ▼ initializeMainExecutable()  // dyld會優(yōu)先初始化動(dòng)態(tài)庫,然后初始化主程序。
          ▼ sMainExecutable->runInitializersrunInitializers()  // run initializers for main executable and everything it brings up 
            ▼ ImageLoader::processInitializers()
              ▼ ImageLoader::recursiveInitialization()  // 循環(huán)遍歷images list中所有的imageloader,recursive(遞歸)初始化。Calling recursive init on all images in images list
                ▼ ImageLoaderMachO::doInitialization()  // 初始化這個(gè)image. initialize this image
                  ▼ ImageLoaderMachO::doImageInit()  //解析LC_ROUTINES_COMMAND 這個(gè)加載命令,可以參考loader.h中該命令的說明,這個(gè)命令包含了動(dòng)態(tài)共享庫初始化函數(shù)的地址,該函數(shù)必須在庫中任意模塊初始化函數(shù)(如C++ 靜態(tài)構(gòu)造函數(shù)等)之前調(diào)用
                  ▼ ImageLoaderMachO::doModInitFunctions()  // 內(nèi)部會調(diào)用C++全局對象的構(gòu)造函數(shù)、__attribute__((constructor))修飾的C函數(shù)
                  // 以上兩個(gè)函數(shù)中,libSystem相關(guān)的都是要首先執(zhí)行的,而且在上述遞歸加載動(dòng)態(tài)庫過程,libSystem是默認(rèn)引入的,所以棧中會出現(xiàn)libSystem_initializer的初始化方法
          ?? (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
        ?? // 第九步,查找入口點(diǎn) main() 并返回,調(diào)用 getEntryFromLC_MAIN,從 Load Command 讀取LC_MAIN入口,如果沒有LC_MAIN入口,就讀取LC_UNIXTHREAD,然后跳到主程序的入口處執(zhí)行
        ?? (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
網(wǎng)絡(luò)引圖,勘誤:右下角libSystemInitialized是在doModInitFunctions中調(diào)用

關(guān)于更多的理論知識,可以閱讀下iOS程序員的自我修養(yǎng)-MachO文件動(dòng)態(tài)鏈接(四)、實(shí)踐篇—fishhook原理(:程序運(yùn)行期間通過修改符號表(nl_symbol_ptr和la_symbol_ptr),來替換要hook的符號對應(yīng)的地址),將《程序員的自我修養(yǎng)》中的理論結(jié)合iOS系統(tǒng)中的實(shí)現(xiàn)機(jī)制做了個(gè)對比介紹。

# 加載動(dòng)態(tài)庫的另一種方式:顯式運(yùn)行時(shí)鏈接dlopen

上面的這種動(dòng)態(tài)鏈接,其實(shí)還可以稱為裝載時(shí)鏈接,與靜態(tài)鏈接相比,其實(shí)都是屬于在程序運(yùn)行之前進(jìn)行的鏈接。還有另一種動(dòng)態(tài)鏈接稱為顯式運(yùn)行時(shí)鏈接(Explicit Runtime Linking)。

裝載時(shí)鏈接:是在程序開始運(yùn)行時(shí)(前)通過dyld動(dòng)態(tài)加載。通過dyld加載的動(dòng)態(tài)庫需要在編譯時(shí)進(jìn)行鏈接,鏈接時(shí)會做標(biāo)記,綁定的地址在加載后再?zèng)Q定。

顯式運(yùn)行時(shí)鏈接:即在運(yùn)行時(shí)通過動(dòng)態(tài)鏈接器dyld提供的API dlopen 和 dlsym 來加載。這種方式,在編譯時(shí)是不需要參與鏈接的。

  • dlopen會把共享庫載入運(yùn)行進(jìn)程的地址空間,載入的共享庫也會有未定義的符號,這樣會觸發(fā)更多的共享庫被載入。
  • dlopen也可以選擇是立刻解析所有引用還是滯后去做。
  • dlopen打開動(dòng)態(tài)庫后返回的是模塊的指針(句柄/文件描述符(FD))
  • dlsym的作用就是通過dlopen返回的動(dòng)態(tài)庫指針和函數(shù)的符號,得到函數(shù)的地址然后使用。

不過,通過這種運(yùn)行時(shí)加載遠(yuǎn)程動(dòng)態(tài)庫的 App,蘋果公司是不允許上線 App Store 的,所以只能用于線下調(diào)試環(huán)節(jié)。

# 參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容