本文為L_Ares個人寫作,以任何形式轉(zhuǎn)載請表明原文出處。
想探索dyld的加載流程,還是需要一些比較常識性的東西,我們就從庫這個東西開始說。
一、關(guān)于庫
平常我們經(jīng)常會掛在嘴邊的CoreFoundation、UIKit、OpenGL等等,這些都是我們開發(fā)的過程中可能需要依賴的庫。
我們會把這些必用或者常用的內(nèi)容封裝成一個庫,準備在某個位置,有需要的時候就會直接調(diào)取,這樣更加的方便。
那么,
1. 庫是什么?
庫是一份可執(zhí)行的代碼的二進制。它們可以被操作系統(tǒng)載入到內(nèi)存,并且被識別。
2. 庫的常見分類
- 靜態(tài)庫
比如.a、.lib等
- 動態(tài)庫(共享庫)
比如.framework、.so(安卓的應該很熟吧)、.dll
3. 動態(tài)庫和靜態(tài)庫的區(qū)別
想了解他們的區(qū)別,就必須知道一個源文件變成可執(zhí)行文件的過程。這個其實大家都是知道的,而且在之前的章節(jié)說運行時的時候也說過了。
這里再啰嗦一次編譯流程 :
源文件--->預編譯--->編譯--->匯編--->鏈接--->可執(zhí)行文件
那么庫文件一般都是在鏈接的時候開始介入和源文件發(fā)生一些事情。
來看,

圖1中是一個項目的兩個代碼段,1和2分處不同的模塊,但是同時都需要庫文件B和庫文件D。
如果是靜態(tài)庫的話,B和D就必須在那個位置坐好,而且就需要為1和2每個段都編譯一次庫文件。也就是說使用靜態(tài)庫的時候,會將靜態(tài)庫的信息直接編譯到可執(zhí)行文件中
再看,

圖2中也是一個項目的兩個代碼段,1和2也分處不同的模塊,也都需要庫文件B和D。
這時候B和D如果是靜態(tài)庫的話,就會根據(jù)實際的需求來進行插入,需要的時候就添加,不需要的時候不會參與。也就是說加載動態(tài)庫時,操作系統(tǒng)會先檢查動態(tài)庫是否因為其它程序已經(jīng)將這個動態(tài)庫信息加載到了內(nèi)存中。不需要多次的加載庫文件。
特點總結(jié) :
- 靜態(tài)庫 :
- 信息直接編譯到可執(zhí)行文件中
- 優(yōu)點 : 靜態(tài)庫被刪除,對可執(zhí)行文件不會造成影響。
- 缺點 : 浪費內(nèi)存空間,如果靜態(tài)庫需要修改,可執(zhí)行文件需要重新編譯。
- 動態(tài)庫 :
- 優(yōu)點 :
- 動態(tài)庫只被夾在到內(nèi)存中一次,不會多次加載。共享內(nèi)存,節(jié)約資源。
- 編譯程序并不會鏈接到目標代碼中,而是在運行時才被載入,不需要每次修改庫文件就要重新編譯。
- 缺點 : 因為運行時載入,所以運行時必然有性能損失,而且程序會依賴外部環(huán)境,一旦動態(tài)庫修改出錯,程序可能會發(fā)生問題。
- 常見的 :
UIKit、libdispatch、libobjc.dyld
二、思路和準備工作
1. 思路和概念
為了確定思路和探索順序,就先來看app的啟動流程圖 :

整個虛線框里面的就是我們要探索的dyld。
那么首先說好概念問題,
什么叫dyld?
dyld
- 英文全稱 :
the dynamic link editor- 中文名叫
蘋果動態(tài)鏈接器。- 它是
iOS中非常重要的部分。- 在
app被打包成mach-o可執(zhí)行文件后,dyld將對其進行鏈接(link),加載可執(zhí)行文件(load)等后續(xù)操作。
2. 探索準備工作
準備條件 : dyld源碼750.6,請自行下載。objc4-781。請看教程,里面有配置好的文件。
libdispatch和libSystem都在這里,請自行搜索這兩個字,下載喜歡的版本。
新建一個我們熟悉的
Project--->iOS--->App。后面我會把它叫Project,一說Project就是說這個東西。在
main.m中給main函數(shù)上斷點,正常情況下,main函數(shù)是程序的入口吧,所以找它,然后執(zhí)行。
我們看Debug Navigator信息,如下圖2.1所示 :

- 在
main函數(shù)前面還有一個start的存在,所以在main函數(shù)的前面,一定還有操作。 - 那么看
start函數(shù),畫紅框的那里,發(fā)現(xiàn)start函數(shù)屬于libdyld。 - 所以在進入
main函數(shù)前,dyld就已經(jīng)介入了。
- 還需要一個臨界點。
因為已經(jīng)發(fā)現(xiàn)main函數(shù)不是最早執(zhí)行的函數(shù)了,那么想要探索dyld,就必須找到一個相對更早的dyld的入口。于是在ViewController中實現(xiàn)它的一個更早的機制——load。可以證明一下,ViewController的load是早于main函數(shù)調(diào)用的。
斷點全部不要,在main函數(shù)里面隨意NSLog內(nèi)容,然后在ViewController的load中隨意打印內(nèi)容。明顯發(fā)現(xiàn)load比main還早。

- 實現(xiàn)
load,并掛上斷點,繼續(xù)Run。

- 在
lldb中輸入bt,查看堆棧信息的調(diào)用。

很明顯,在Debug Nav和堆棧信息中,找到了同一個更早調(diào)用的_dyld_start。
- 打開上面準備的dyld源碼750.6,搜索
__dyld_start。
三、 dyld加載流程
上面已經(jīng)找到了_dyld_start這個入口,那么就從_dyld_start開始流程探索。
主線 : dyld加載流程
1. dyldbootstrap::start

一定是
arm64架構(gòu),并且看_dyld_start都做了什么,所以看bl跳轉(zhuǎn)。找到dyldbootstrap::start。搜空格 + start(,因為這是一個函數(shù),按照函數(shù)格式規(guī)范來搜。找
return。進入dyld::_main

2. dyld::_main的流程
就看一些有注釋的,然后看主要流程。不用全都看得懂,主要是捋清楚這條線。按照經(jīng)驗來看,無論做什么都要先搞清楚環(huán)境,所以從環(huán)境入手,搜索env,找到如下圖所示
- 【第一步 : 環(huán)境變量配置】 : 根據(jù)環(huán)境變量進行對應的值的配置。獲取當前的運行架構(gòu)


- 【第二步 : 設置共享緩存】 : iOS中必須開啟共享緩存,為動態(tài)庫的使用做環(huán)境準備,并且共享緩存必須映射到共享區(qū)域,否則可能出現(xiàn)動態(tài)庫無法使用的情況。

- 【第三步 : 嘗試將dyld本身放入
UUID列表】

- 【第四步 : 主可執(zhí)行程序的實例化】 :
instantiateFromLoadedImage會為主可執(zhí)行程序生成鏡像

- 【第五步 : 插入動態(tài)庫】 :
loadInsertedDylib加載所有插入的動態(tài)庫

- 【第六步 : 鏈接主可執(zhí)行程序】

- 【第七步 : 鏈接動態(tài)庫】

- 【第八步 : 弱符號綁定】

- 【第九步 : 執(zhí)行初始化方法】

- 【第十步 : 通知所有監(jiān)聽,該進程馬上進入
main()】

- 【第十一步 : 尋找主可執(zhí)行程序入口】
從Load Commond讀取LC_MAIN入口,如果沒有就讀取LC_UNIXTHREAD。最后進入的就是我們見到的main()函數(shù)

四、關(guān)于主程序的初始化和執(zhí)行初始化
上面是main()函數(shù)啟動前的準備過程,這里挑重點的說,先說主程序的初始化。因為后面所有的步驟流程都是圍繞著這個主程序來進行的。

先來看一個串起了整條線的變量
sMainExecutable : 主程序。在所有的步驟線上,它都是中心點。
那么就找它的初始化方法instantiateFromLoadedImage進行探索。
1. instantiateFromLoadedImage主程序的初始化
//內(nèi)核在dyld獲得控制之前在,需要在主可執(zhí)行文件中進行映射。
//我們需要為主可執(zhí)行文件創(chuàng)建一個ImageLoader*
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
- 注釋寫了,主可執(zhí)行文件并不是主程序,主程序是
ImageLoader *的對象image,也即是鏡像。 - 這個主程序最后會被強轉(zhuǎn)成
ImageLoaderMachO類型的指針對象,也就是我們的mach-o可執(zhí)行文件。
然后我們進入創(chuàng)建ImageLoader的創(chuàng)建方法instantiateMainExecutable。
// create image for main executable
//為主可執(zhí)行文件創(chuàng)建鏡像
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
//段的數(shù)量
unsigned int segCount;
//庫的數(shù)量
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
//sniffLoadCommands用于確定此mach-o文件是否有正常的或者壓縮的LINKEDIT,以及它所擁有的段數(shù)
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
//根據(jù)load commands的內(nèi)容,實例化具體的類
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}
- 也就是說創(chuàng)建主鏡像的重點就是
sniffLoadCommands,它是獲得mach-o可執(zhí)行文件的代碼,并且也可以獲取load commods加載命令的信息。
說白了,就是鏡像文件進到這里,主要就是被加載到load commonds里面,那么之后我們寫的所有的代碼都會被編譯進來,也就是說我們寫的東西都會被變成mach-o的形式。
比如,最上面我創(chuàng)建的那個Project打開它的可執(zhí)行文件

這里面的動態(tài)庫都會被變成mach-o文件,都變成了這種數(shù)據(jù)段的形式存在于load commonds里面。
2. initializeMainExecutable執(zhí)行初始化
void initializeMainExecutable()
{
// record that we've reached this step
//記錄一下我們已經(jīng)到達了主可執(zhí)行程序的初始化
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
//對所有插入的動態(tài)庫做初始化
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
//獲取鏡像根文件的數(shù)量
const size_t rootCount = sImageRoots.size();
//如果鏡像根文件的數(shù)量比1多
if ( rootCount > 1 ) {
//循環(huán)執(zhí)行初始化,這里的初始化和下面的主可執(zhí)行文件及其所有關(guān)聯(lián)文件的初始化函數(shù)是同一個
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
//運行主可執(zhí)行文件和所有關(guān)聯(lián)文件的初始化
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
//注冊cxa_atexit()處理程序,在進程退出時在所有加載的鏡像(image)中運行靜態(tài)終止符
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
//如果需要轉(zhuǎn)存信息
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
還是看一下注釋
- 執(zhí)行初始化的真正函數(shù)是
runInitializers。
runInitializers我就不貼出來了,因為我們就需要那一句processInitializers函數(shù)。
- (1) processInitializers
//這里不用翻譯這么多英文了,大概意思就是向上的鏈接全都放到向下的鏈接的處理后面
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
//在鏡像列表中的所有鏡像都利用遞歸進行init,建立一個沒有進行初始化向上依賴關(guān)系的列表
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
//如果還存在向上的依賴關(guān)系,把它們?nèi)砍跏蓟? if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
就是對鏡像列表中的鏡像全部初始化,并且把依賴關(guān)系全部變成向下的。
- (2) recursiveInitialization
代碼太多,截圖看,因為我們最主要的是讓dyld知道了我需要用到你了。
看畫紅框的,一個通知notifySingle和一個初始化doInitialization。

- (3) notifySingle
全局搜索notifySingle(
然后找到獲取鏡像文件的真實地址的那句代碼

一個函數(shù)指針,那么繼續(xù)跟這個函數(shù)指針??纯催@個指針是在哪里賦值的,

注冊Notify。再看誰注冊了通知。接著搜索registerObjCNotifiers。找到了

然后去objc4-781中搜索_dyld_objc_notify_register。

本身
*sNotifyObjCInit就是一個函數(shù)回調(diào),這就是數(shù)據(jù)在這dyld和objc中的傳遞。
本來
dyld里面的sNotifyObjCInit是init,相當于是一個nil的,只有看objc_init什么時候調(diào)用_dyld_objc_notify_register,并給他這個load_images,sNotifyObjCInit才能執(zhí)行起來。
所以下面接著看load_images在objc-781里面又干了什么,是不是真的調(diào)起了+(void)load。
- (3.1) load_images
下面都是跟著紅框往里面進。



這里就行了,因為看到了@selector(load)。
然后我們對比一下xcode的堆棧信息。

和上面的流程一模一樣。
總結(jié) :
+(void)load的源碼鏈條 :
_dyld_start--->dyldbootstrap::start--->dyld::_main--->
dyld::initializeMainExecutable--->ImageLoader::runInitializers--->
ImageLoader::processInitializers--->ImageLoader::recursiveInitialization--->
dyld::notifySingle(函數(shù)指針,利用回調(diào))--->
sNotifyObjCInit--->load_images(libobjc.A.dylib)
- (4) doInitialization
現(xiàn)在知道它是執(zhí)行初始化的真正步驟,那么看dyld的源碼

兩種初始化方法,一個-init、一個靜態(tài)構(gòu)造器。分別進去看一下。
(4.1)doImageInit()

兩點 :
-
doImageInit()是通過-init做的初始化,并且是通過初始化函數(shù)的移動完成鏡像的初始化。 -
libSystem必須是第一個進行初始化的庫。 - for循環(huán)加載方法的調(diào)用
(4.2)doModInitFunctions()

你會發(fā)現(xiàn)和doImageInit差不多的內(nèi)容,但是doModInitFunctions加載大多都是c++的文件。
做個驗證 :

這是最開始的Project,在main.m添加c++代碼
__attribute__((constructor)) void jdFunc() {//掛上斷點
printf("%s",__func__);
}
上圖3.15就是它的結(jié)果,畫紅框的就是doModInitFunctions。
另外,可以下載libSystem的官方源碼,搜索_initializer,會發(fā)現(xiàn)
(1). libSystem也是通過_dyld_initializer進行的初始化。然后就會進行libdispatch_init(void);,這一步libdispatch_init的初始化就通過了libdispatch.dyld的庫。
(2). 其實還可以繼續(xù)的,這里我就多說一下,因為的確圖太多了,我就不全部貼了,感興趣的可以再下載一份libdispatch的庫,然后搜索libdispatch_init,會發(fā)現(xiàn)它的實際實現(xiàn)是通過_os_object_init,再進入_os_object_init,你就會發(fā)現(xiàn)_objc_init()。
結(jié)論 :
庫的初始化順序 :
dyld--->libSystem init--->libdispatch init--->objc init
objc_init的源碼鏈 :
_dyld_start--->dyldbootstrap::start--->dyld::_main--->
dyld::initializeMainExecutable--->ImageLoader::runInitializers--->
ImageLoader::processInitializers--->ImageLoader::recursiveInitialization--->
doInitialization--->
libSystem_initializer(libSystem.B.dylib)--->_os_object_init(libdispatch.dylib)
--->_objc_init(libobjc.A.dylib)
附:
一張dyld的加載流程圖。
