第十三節(jié)—dyld加載流程

本文為L_Ares個人寫作,以任何形式轉(zhuǎn)載請表明原文出處。

想探索dyld的加載流程,還是需要一些比較常識性的東西,我們就從這個東西開始說。

一、關(guān)于庫

平常我們經(jīng)常會掛在嘴邊的CoreFoundation、UIKitOpenGL等等,這些都是我們開發(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.png

圖1中是一個項目的兩個代碼段,1和2分處不同的模塊,但是同時都需要庫文件B和庫文件D。

如果是靜態(tài)庫的話,BD就必須在那個位置坐好,而且就需要為1和2每個段都編譯一次庫文件。也就是說使用靜態(tài)庫的時候,會將靜態(tài)庫的信息直接編譯到可執(zhí)行文件中

再看,

圖2.png

圖2中也是一個項目的兩個代碼段,1和2也分處不同的模塊,也都需要庫文件BD

這時候BD如果是靜態(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ā)生問題。
    • 常見的 : UIKitlibdispatch、libobjc.dyld

二、思路和準備工作

1. 思路和概念

為了確定思路和探索順序,就先來看app的啟動流程圖 :

app啟動流程圖.png

整個虛線框里面的就是我們要探索的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。請看教程,里面有配置好的文件。libdispatchlibSystem都在這里,請自行搜索這兩個字,下載喜歡的版本。

  1. 新建一個我們熟悉的Project--->iOS--->App后面我會把它叫Project,一說Project就是說這個東西。

  2. main.m中給main函數(shù)上斷點,正常情況下,main函數(shù)是程序的入口吧,所以找它,然后執(zhí)行。

我們看Debug Navigator信息,如下圖2.1所示 :

圖2.2.1.png
  • main函數(shù)前面還有一個start的存在,所以在main函數(shù)的前面,一定還有操作。
  • 那么看start函數(shù),畫紅框的那里,發(fā)現(xiàn)start函數(shù)屬于libdyld。
  • 所以在進入main函數(shù)前,dyld就已經(jīng)介入了。
  1. 還需要一個臨界點。
    因為已經(jīng)發(fā)現(xiàn)main函數(shù)不是最早執(zhí)行的函數(shù)了,那么想要探索dyld,就必須找到一個相對更早的dyld的入口。于是在ViewController中實現(xiàn)它的一個更早的機制——load。可以證明一下,ViewControllerload是早于main函數(shù)調(diào)用的。

斷點全部不要,在main函數(shù)里面隨意NSLog內(nèi)容,然后在ViewControllerload中隨意打印內(nèi)容。明顯發(fā)現(xiàn)loadmain還早。

圖2.2.2.png
  1. 實現(xiàn)load,并掛上斷點,繼續(xù)Run。
圖2.2.3.png
  1. lldb中輸入bt,查看堆棧信息的調(diào)用。
圖2.2.4.png

很明顯,在Debug Nav和堆棧信息中,找到了同一個更早調(diào)用的_dyld_start。

  1. 打開上面準備的dyld源碼750.6,搜索__dyld_start

三、 dyld加載流程

上面已經(jīng)找到了_dyld_start這個入口,那么就從_dyld_start開始流程探索。

主線 : dyld加載流程

1. dyldbootstrap::start

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

  • return。進入dyld::_main

圖2.3.2.png

2. dyld::_main的流程

就看一些有注釋的,然后看主要流程。不用全都看得懂,主要是捋清楚這條線。按照經(jīng)驗來看,無論做什么都要先搞清楚環(huán)境,所以從環(huán)境入手,搜索env,找到如下圖所示

  • 【第一步 : 環(huán)境變量配置】 : 根據(jù)環(huán)境變量進行對應的值的配置。獲取當前的運行架構(gòu)

圖2.3.3.png
  • 【第二步 : 設置共享緩存】 : iOS中必須開啟共享緩存,為動態(tài)庫的使用做環(huán)境準備,并且共享緩存必須映射到共享區(qū)域,否則可能出現(xiàn)動態(tài)庫無法使用的情況。
圖2.3.4.png
  • 【第三步 : 嘗試將dyld本身放入UUID列表】
圖2.3.5.png
  • 【第四步 : 主可執(zhí)行程序的實例化】 : instantiateFromLoadedImage會為主可執(zhí)行程序生成鏡像
圖2.3.6.png
  • 【第五步 : 插入動態(tài)庫】 : loadInsertedDylib加載所有插入的動態(tài)庫
圖2.3.7.png
  • 【第六步 : 鏈接主可執(zhí)行程序】
圖2.3.8.png
  • 【第七步 : 鏈接動態(tài)庫】
圖2.3.9.png
  • 【第八步 : 弱符號綁定】
圖2.3.10.png
  • 【第九步 : 執(zhí)行初始化方法】
圖2.3.11.png
  • 【第十步 : 通知所有監(jiān)聽,該進程馬上進入main()
圖2.3.12.png
  • 【第十一步 : 尋找主可執(zhí)行程序入口】

Load Commond讀取LC_MAIN入口,如果沒有就讀取LC_UNIXTHREAD。最后進入的就是我們見到的main()函數(shù)

圖2.3.13.png

四、關(guān)于主程序的初始化和執(zhí)行初始化

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

圖3.1.png

先來看一個串起了整條線的變量

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í)行文件

圖3.2.png

這里面的動態(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.3.png
  • (3) notifySingle

全局搜索notifySingle(

然后找到獲取鏡像文件的真實地址的那句代碼

圖3.4.png

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

圖3.5.png

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

圖3.6.png

然后去objc4-781中搜索_dyld_objc_notify_register。

圖3.7.png

本身*sNotifyObjCInit就是一個函數(shù)回調(diào),這就是數(shù)據(jù)在這dyldobjc中的傳遞。

本來dyld里面的sNotifyObjCInitinit,相當于是一個nil的,只有看objc_init什么時候調(diào)用_dyld_objc_notify_register,并給他這個load_images,sNotifyObjCInit才能執(zhí)行起來。

所以下面接著看load_imagesobjc-781里面又干了什么,是不是真的調(diào)起了+(void)load

  • (3.1) load_images

下面都是跟著紅框往里面進。

圖3.8.png
圖3.9.png
圖3.10.png

這里就行了,因為看到了@selector(load)。

然后我們對比一下xcode的堆棧信息。

圖3.11.png

和上面的流程一模一樣。

總結(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的源碼

圖3.12.png

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

(4.1)doImageInit()

圖3.13.png

兩點 :

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

(4.2)doModInitFunctions()

圖3.14.png

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

做個驗證 :

圖3.15.png

這是最開始的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的加載流程圖。

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

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