dyld加載應用啓動原理詳解

我們都知道APP的入口函數是main(),而在main()函數調用之前,APP的加載過程是怎樣的呢?接下來我們一起來分析APP的加載流程。

一、利用斷點進行追蹤

  • 首先我們創建一個工程,什麼代碼都不寫,在main()函數處進行斷點,會看到情況如下圖:

     

    01

  1. 通過上圖我們可以看到,在調用堆棧中,我們只看到了star和main,並開啓了主線程,其它的什麼都看不到。那要怎麼才能看到調用堆棧詳細點的信息了?我們都知道,有一個方法比main()函數調用更早,那就是load()函數,此時在控制器中寫一個load函數,並斷點運行,如下圖:

02

  1. 通過上圖,我們看到了比較詳細的函數調用順序,從第13行的_dyld_start到第3行的dyld:notifySingle,頻率出現最多的就是這個dyld的傢伙,那麼dyld是什麼?它在做什麼?簡單來說dyld是一個動態鏈接器,用來加載所有的庫和可執行文件。接下來我們將通過圖2的調用關係,去追蹤dyld到底在什麼?

二、 dyld加載流程分析

1. 首先下載dyld源碼

2. 打開dyld源碼工程,根據圖2的第12行dyldbootstrap:start爲關鍵字搜索dyldbootstrap中調用的start方法,如下圖:

 

3. 該方法源碼如下,接下來我們對該方法的重點部分進行分析:

uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], intptr_t slide)
{
    // 讀取macho文件的頭部信息
    const struct macho_header* dyldsMachHeader =  (const struct macho_header*)(((char*)&_mh_dylinker_header)+slide);
    
    // 滑塊,設置偏移量,用於重定位
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }
    
    uintptr_t appsSlide = 0;
        
    // 針對偏移異常的監測
    dyld_exceptions_init(dyldsMachHeader, slide);
    
    // 初始化machO文件
    mach_init();

    // 設置分段保護,這裏的分段下面會介紹,屬於machO文件格式
    segmentProtectDyld(dyldsMachHeader, slide);
    
    //環境變量指針
    const char** envp = &argv[argc+1];
    
    // 環境變量指針結束的設置
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;

    // 在dyld中運行所有c++初始化器
    runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
    
    // 如果主可執行文件被鏈接-pie,那麼隨機分配它的加載地址
    if ( appsMachHeader->flags & MH_PIE )
        appsMachHeader = randomizeExecutableLoadAddress(appsMachHeader, envp, &appsSlide);
    
    // 傳入頭文件信息,偏移量等。調用dyld的自己的main函數(這裏並不是APP的main函數)。
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);
}

  • 3.1 函數的參數中我們看到有一個macho_header的參數,這是一個什麼東西呢?Mach-O其實是Mach Object文件格式的縮寫,是mac以及iOS中的可執行文件格式,並且有自己的文件格式目錄,蘋果給出的mach文件如下圖:

     

    04

  • 3.2 首先我們點擊進入macho_header這個結構體看它的定義如下:

struct mach_header_64 {
    uint32_t    magic;      /* 區分系統架構版本 */
    cpu_type_t  cputype;    /*CPU類型 */
    cpu_subtype_t   cpusubtype; /* CPU具體類型 */
    uint32_t    filetype;   /* 文件類型 */
    uint32_t    ncmds;      /* loadcommands 條數,即依賴庫數量*/
    uint32_t    sizeofcmds; /* 依賴庫大小 */
    uint32_t    flags;      /* 標誌位 */
    uint32_t    reserved;   /* 保留字段,暫沒有用到*/
};

 

2. 配置一些環境變量

 

3. 實例化主程序,即macho可執行文件。

4. 加載共享緩存庫。

5. 插入動態緩存庫。

6. 鏈接主程序。

7. 初始化函數。

 

8. 返回主程序的入口函數,開始進入主程序的main()函數。

 

2. 配置一些環境變量

 

3. 實例化主程序,即macho可執行文件。

4. 加載共享緩存庫。

5. 插入動態緩存庫。

6. 鏈接主程序。

7. 初始化函數。

 

8. 返回主程序的入口函數,開始進入主程序的main()函數。

 

  • 3.3 這裏macho_header就是讀取macho文件的頭部信息,header裏面會包含該二進制文件的一些信息:如字節順序、架構類型、加載指令的數量等。可以用來快速確認一些信息,比如當前文件用於32位還是64位、文件的類型等。那麼macho文件在哪裏可以找到了呢?如下圖,我們找到macho,並用MachOView來查看:

     

    05

  • 3.4 上面那個黑不溜秋的就是macho文件,是一個可執行文件,我們來看下它加載的頭部信息有哪些?這些信息將會被傳到下一個函數中。這裏簡單說下Number of Load Commands數字爲22,代表22個庫文件,在LoadCommands有加載庫的對應關係,Section中就是我們的數據DATA,包含了代碼,常量等數據。

     

    06

  • 3.5 小結:star函數主要就是先讀取macho文件的頭部信息,設置虛擬地址偏移,這裏的偏移主要用於重定向。接下來就是初始化macho文件,用於後續加載庫文件和DATA數據,再運行C++的初始化器,最後進入dyly的主函數。

  • 4. 接下來我們繼續追蹤,根據圖2的調用堆棧,我們知道在dyldbootstrap:star方法中調用了dyld::_main方法,也就是我們上面說到的進入dyld的主程序,如下圖:

    07

  • 4.1 我們進入方法繼續追蹤,截取部分源如下圖,我們發現這裏有幾個if判斷,此處是在設置環境變量,也就是如果設置了這些環境變量,Xcode就會在控制檯打印相關的詳細信息:
    if ( sProcessIsRestricted )
            pruneEnvironmentVariables(envp, &apple);
        else
            checkEnvironmentVariables(envp, ignoreEnvironmentVariables);
        if ( sEnv.DYLD_PRINT_OPTS ) 
            printOptions(argv);
        if ( sEnv.DYLD_PRINT_ENV ) 
            printEnvironmentVariables(envp);
        getHostInfo();  
    
  • 4.2 當我們設置了相關的環境變量,此時Xcode就會打印程序相關的目錄、用戶級別、插入的動態庫、動態庫的路徑等,演示圖下圖:
  • 08

  • 4.3 設置環境變量之後,接下來會調用getHostInfo()來獲取machO頭部獲取當前運行架構的信息,函數代碼如下:
  • static void getHostInfo()
    {
    #if 1
        struct host_basic_info info;
        mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
        mach_port_t hostPort = mach_host_self();
        kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count);
        if ( result != KERN_SUCCESS )
            throw "host_info() failed";
        
        sHostCPU        = info.cpu_type;
        sHostCPUsubtype = info.cpu_subtype;
    #else
        size_t valSize = sizeof(sHostCPU);
        if (sysctlbyname ("hw.cputype", &sHostCPU, &valSize, NULL, 0) != 0) 
            throw "sysctlbyname(hw.cputype) failed";
        valSize = sizeof(sHostCPUsubtype);
        if (sysctlbyname ("hw.cpusubtype", &sHostCPUsubtype, &valSize, NULL, 0) != 0) 
            throw "sysctlbyname(hw.cpusubtype) failed";
    #endif
    }
    
  • 4.4 接着往下看,這裏會對macho文件進行實例化:
  •     try {
            // 實例化主程序,也就是machO這個可執行文件
            sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
            sMainExecutable->setNeverUnload();
            gLinkContext.mainExecutable = sMainExecutable;
            gLinkContext.processIsRestricted = sProcessIsRestricted;
            // 加載共享緩存庫
            checkSharedRegionDisable();
        #if DYLD_SHARED_CACHE_SUPPORT
            if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
                mapSharedCache();
        #endif
    
  • 4.5 進入實例化主程序代碼如下,加載完畢後會返回一個ImageLoader鏡像加載類,這是一個抽象類,用於加載特定可執行文件格式的類,對於程序中需要的依賴庫、插入庫,會創建一個對應的image對象,對這些image進行鏈接,調用各image的初始化方法等等,包括對runtime的初始化。
  • {
        // isCompatibleMachO 是檢查mach-o的subtype是否是當前cpu可以支持
        if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
            ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
    //將image添加到imagelist。所以我們在Xcode使用image list命令查看的第一個便是我們的machO
            addImage(image);
            return image;
        }
        
        throw "main executable not a known format";
    }
    
  • 4.6 使用image list命令演示如下圖,看到的第一個0x000000010401c000地址就是macho這個可執行文件的地址。

     

     

  • 4.7 對macho文件進行實例化後,會看到一個checkSharedRegionDisable()的方法,這裏是在加載共享緩存庫。這個共享緩存庫是個什麼東西呢? 其實我們可以理解爲是系統公用的動態庫(蘋果禁止第三方使用動態庫)。如我們最常用的UIKit框架就在共享緩存庫中,舉個例子,微信、QQ、支付寶、天貓等APP都會使用到UIKit這個框架,如果每個應用都加載UIKit,勢必會導致內存緊張。所以實際是這些APP都會共享一套UIKit框架,應用中用到了對應了UIKit框架中的方法,dyld就會去拿對應的資源供給這些APP使用。如下圖展示了越獄手機中System的library中framework的庫,也證明了這一點:

     

    共享緩存庫

  • 5. 插入庫:我們繼續看該方法中的剩餘源碼,這裏將會加載所有插入庫,逆向中的代碼注入就是在這一步完成的,framework的詳細代碼注入流程請看我的這篇文章。這裏有一個sAllImages.size()-1的操作,實際上是排除了主程序。

  •     // 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.
            sInsertedDylibCount = sAllImages.size()-1;
    
    
    

    6. 鏈接主程序:內部通過imageLoader的實例對象去調用link方法,遞歸加載所依賴的系統庫和第三方庫。

            // link main executable
            gLinkContext.linkingMainExecutable = true;
            link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
            gLinkContext.linkingMainExecutable = false;
            if ( sMainExecutable->forceFlat() ) {
                gLinkContext.bindFlat = true;
                gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
            }
            result = (uintptr_t)sMainExecutable->getMain();
    

    7. 初始化函數

    10

    8. 運行初始化程序:

     

  • 8.1 遞歸:加載我們所需要的依賴的系統庫和第三方庫。

     

    12

  • 9. notifySingle函數,這是一個與運行時建立聯繫的關鍵函數:

    13

  • 9.1 我們發現notifySingle這個函數中調用了load_images方法,點進去發現這是一個函數指針,裏面並沒有找到load_images的調用,通過對dyld文件的全局搜索,也沒有發現。所以此時我們推斷它是在運行時調用的,正好objc運行時代碼也是開源的,接下來我們下載objc源碼進行分析。
    void     (*notifySingle)(dyld_image_states, const ImageLoader* image);
    
  • 9.2 在objc_init中我們會發現調用,這裏load_images。
  •   _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    

    14

  • 9.3 在load_images中完成call_load_methods的調用,這裏就是加載所有類文件及分類文件的load方法:
  • load_images(const char *path __unused, const struct mach_header *mh)
    {
        // 如果這裏沒有+load方法,則返回時不帶鎖
        if (!hasLoadMethods((const headerType *)mh)) return;
    
        recursive_mutex_locker_t lock(loadMethodLock);
    
        // 發現load方法
        {
            mutex_locker_t lock2(runtimeLock);
            prepare_load_methods((const headerType *)mh);
        }
    
        // 加載所有load方法
        call_load_methods();
    }
    
  • 9.4 call_load_methods方法調用,在call_load_methods中,通過doWhile循環來調用call_class_loads加載每個類的load方法,然後再加載分類的loads方法。
  • void call_load_methods(void)
    {
        static bool loading = NO;
        bool more_categories;
    
        loadMethodLock.assertLocked();
    
        // Re-entrant calls do nothing; the outermost call will finish the job.
        if (loading) return;
        loading = YES;
    
        void *pool = objc_autoreleasePoolPush();
    
        do {
            // 1. 循環調用所有類文件的laod方法
            while (loadable_classes_used > 0) {
                call_class_loads();
            }
    
            // 2.調用所有分類方法
            more_categories = call_category_loads();
    
            // 3. Run more +loads if there are classes OR more untried categories
        } while (loadable_classes_used > 0  ||  more_categories);
    
        objc_autoreleasePoolPop(pool);
    
        loading = NO;
    }
    
  • 9.5 根據上面的調用順序,我們知道是先加載類文件中的load方法,然後再加載分類文件中的load方法,演示如圖:

  • 15

  • 10. 在調用完notifySigin後,我們發現繼續調用了doInitialization,doModInitFunctions會調用machO文件中_mod_init_func段的函數,也就是我們在文件中所定義的全局C++構造函數。

    // let objc know we are about to initalize this image
    fState = dyld_image_state_dependents_initialized;
    oldState = fState;
    context.notifySingle(dyld_image_state_dependents_initialized, this);
    
    // initialize this image
    this->doInitialization(context);
    
  • 10.1 所以通過上述代碼的調用順序我們知道先類文件load,再分類文件load,然後再是C++構造函數,最後就進入了我們的main主程序!演示如下:

     

  • 通過上面的分析,我們從斷點開始,查看方法的堆棧調用順序,一步一步追蹤dyld的加載流程,也就將main函數調用前的神祕面紗揭露無疑,你也可以根據上述的步驟自己動手追蹤APP的加載過程,這樣會更加印象深刻!

    總結:main()函數調用之前,其實是做了很多準備工作,主要是dyld這個動態鏈接器在負責,核心流程如下:

    1. 程序執行從_dyld_star開始

  • 1.1. 讀取macho文件信息,設置虛擬地址偏移量,用於重定向。
  • 1.2. 調用dyld::_main方法進入macho文件的主程序。
  • 2.1. 設置的環境變量方便我們打印出更多的信息。
  • 2.1. 調用getHostInfo()來獲取machO頭部獲取當前運行架構的信息。
  • 7.1. 經過一系列的初始化函數最終調用notifSingle函數。
  • 7.2. 此回調是被運行時_objc_init初始化時賦值的一個函數load_images
  • 7.3. load_images裏面執行call_load_methods函數,循環調用所用類以及分類的load方法。
  • 7.4. doModInitFunctions函數,內部會調用全局C++對象的構造函數,即_ _ attribute_ _((constructor))這樣的函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章