iOS應(yīng)用程序加載大致流程分析

前言

今天我們重點來分析一下,iOS App運行時,在main()方法執(zhí)行之前,程序到底做了哪些事?

準(zhǔn)備工作

示例,新建一個iOS應(yīng)用工程,查看方法加載的順序

__attribute__((constructor)) void lg_cFunction() {
//    printf();
    NSLog(@"%s -- 來了", __func__);
}

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        NSLog(@"%s -- 來了", __func__);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
---------------------------------分割線---------------------------------
@implementation ViewController

+ (void)load {
    NSLog(@"%s -- 來了", __func__);
}
@end

運行后

2020-09-28 11:06:39.756959+0800 Test1[49592:4931930] +[ViewController load] -- 來了
2020-09-28 11:06:39.757473+0800 Test1[49592:4931930] lg_cFunction -- 來了
2020-09-28 11:06:39.757671+0800 Test1[49592:4931930] main -- 來了
2020-09-28 11:06:39.800886+0800 Test1[49592:4931930] result is 0

發(fā)現(xiàn)當(dāng)前3個方法的調(diào)用順序是load --> lg_cFunction(c++方法) -->main入口,why?

編譯過程

帶著上述方法調(diào)用順序的疑問,我們先來大致了解下,App編譯的一個過程:


大致流程是:
源文件(.h .m .cpp)-->預(yù)編譯(檢查語法)-->編譯(轉(zhuǎn)化為匯編)-->匯編(生成機器碼文件) -->鏈接(也包括一些庫的鏈接)-->生成可執(zhí)行文件(在生成的.app中右鍵打開包文件,里頭的exec)
其中鏈接這一步蘋果系統(tǒng)使用的就是dyld庫來完成的。那什么是dyld呢?

dyld動態(tài)鏈接器

dyld 是英文 the dynamic link editor 的簡寫,翻譯過來就是動態(tài)鏈接器,是蘋果操作系統(tǒng)的一個重要的組成部分。在 iOS/Mac OSX 系統(tǒng)中,僅有很少量的進(jìn)程只需要內(nèi)核就能完成加載,基本上所有的進(jìn)程都是動態(tài)鏈接的,所以 Mach-O 鏡像文件中會有很多對外部的庫和符號的引用,但是這些引用并不能直接用,在啟動時還必須要通過這些引用進(jìn)行內(nèi)容的填補,這個填補工作就是由 動態(tài)鏈接器dyld來完成的,也就是符號綁定。

找入口

你想知道系統(tǒng)調(diào)用load之前走了什么流程?很簡單,在load方法里打斷點,然后lldb bt指令查看調(diào)用堆棧信息。

指令名稱 釋義
bt 查看調(diào)用堆棧信息,加all可打印多有thread的堆棧


上圖紅框處可知,最開始是從_dyld_start_開始的,我們?nèi)炙阉?code>_dyld_start_,尋找入口。
_dyld_start_.png

看注釋,我們注意到會調(diào)用dyldbootstrap::start
image.png

再次全局搜索dyldbootstrap,找到start函數(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);

    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    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;

    // set up random value for stack canary
    __guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
    // run all C++ initializers inside dyld
    runDyldInitializers(argc, argv, envp, apple);
#endif

    // 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);
}

最終調(diào)用dyld::_main,這個就是我們要找的入口!

dyld::_main流程大致分析

這個函數(shù)實現(xiàn)代碼量巨大,我們一步步看。

  1. 首先看函數(shù)的返回值result,如下圖:

    再搜索result的賦值地方,

上圖是初始化

上圖是一個if特殊情況條件里的返回,不考慮。


上圖是個宏編譯的if條件的,不考慮。


上圖也是一個if條件中的賦值,并return,不作考慮。

同理,不考慮。


首先3是宏條件編譯,不考慮,然后1和2是主要賦值的地方,都用到一個共同的變量sMainExecutable,應(yīng)該是關(guān)鍵。

也是if條件里的,不考慮。

最終發(fā)現(xiàn),賦值result的都是通過變量sMainExecutable,那我們再搜索sMainExecutable的賦值情況:

sMainExecutable.png

第一個就找到了,通過方法instantiateFromLoadedImage,再看注釋// instantiate ImageLoader for main executable -->為主可執(zhí)行文件實例化ImageLoader,接著我們重點看看instantiateFromLoadedImage函數(shù)

// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
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";
}

關(guān)鍵代碼是ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);

// create image for main executable
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;
    unsigned int segCount;
    unsigned int libCount;
    const linkedit_data_command* codeSigCmd;
    const encryption_info_command* encryptCmd;
    sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
    // instantiate concrete class based on content of load commands
    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
}

首先sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);

sniffLoadCommands.png

根據(jù)注釋,應(yīng)該是定義mach-o文件的格式,定義什么格式呢?
這時需要用到查看mach-o文件的軟件MachOView,舉個例子來看看:
先找到工程的.app文件所在位置:

再右鍵顯示包內(nèi)容:

選擇exec可執(zhí)行文件

拖入到MachOView中:

所以,sniffLoadCommands定義的就是mach-o里的區(qū)間Load Commands里的格式。
格式定義完成后,接著進(jìn)行初始化instantiateMainExecutable

// instantiate concrete class based on content of load commands
    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

不論是壓縮模式ImageLoaderMachOCompressed,還是標(biāo)準(zhǔn)模式ImageLoaderMachOClassic,最終都是生成ImageLoader對象,完成一個初始化sMainExecutable的過程。

至此,我們通過返回值result反向搜索賦值,找到sMainExecutable的初始化流程(第6577行),那么在sMainExecutable的初始化前,又走了哪些流程?我們一點點的往下看:














上述所有圖,大致描述了dyld::_main的大致流程:

  1. 環(huán)境變量配置
  2. 共享緩存處理
  3. 主程序表初始化
  4. 插入動態(tài)庫
  5. 鏈接主程序表
  6. 鏈接動態(tài)庫
  7. 弱符號綁定
  8. 初始化所有
  9. 主程序入口處理
第8步初始化流程詳細(xì)分析

大家肯定想知道:我們平時寫的對象到底是如何初始化的呢,說白了就是我們之前討論的_objc_init是在哪里觸發(fā)被調(diào)用的呢?帶著這個問題,我們首先看看initializeMainExecutable源碼:

再看看runInitializers源碼:

繼續(xù)processInitializers

繼續(xù)recursiveInitialization


很明顯,這里面,需要分成兩部分探索,一部分是notifySingle函數(shù),一部分是doInitialization函數(shù)。

首先探索notifySingle函數(shù)
小技巧-->全局搜索notifySingle(函數(shù)

紅框里是核心代碼
再搜索sNotifyObjCInit,看看哪里賦值處理


上圖賦值的是在函數(shù)registerObjCNotifiers,再搜索其被調(diào)用的地方

是在 _dyld_objc_notify_register進(jìn)行了調(diào)用,但是_dyld_objc_notify_register的函數(shù)需要在libobjc源碼中搜索

終于,是在 _objc_init中,這不正是我們最開始要找的問題所在嗎,哈哈!
_objc_init源碼中調(diào)用了_dyld_objc_notify_register,并傳入了參數(shù)load_images,那么sNotifyObjCInit = load_images,而load_images中會調(diào)用所有的+load方法。

整個load方法的調(diào)用鏈路就是:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一個回調(diào)處理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)

load方法的調(diào)用棧信息

load調(diào)用棧

前面的流程跟我們之前分析的一樣:_dyld_start-->dyld::main-->initialzeMainExecutetable()初始化主程序表-->dyld::notifySingle回調(diào)結(jié)果-->load_images加載文件,緊接著就調(diào)用了load方法,具體調(diào)用的位置如下圖:
load調(diào)用

cxx方法調(diào)用棧信息

同理,在cxx方法處打斷點,查看調(diào)用棧:

cxx調(diào)用

load不同的是,在recursiveInitialization之后,



和load不同的是,在doInitialization里觸發(fā)的cxx方法。那我們具體看看doInitialization的流程是如何處理的。

先看doImageInit

再看doModInitFunctions,和doImageInit差不多的流程

接著搜索LC_SEGMENT_COMMAND

以64位為例,看看LC_SEGMENT_64

看來是_TEXT區(qū)間相關(guān),我們查看mach-o符號文件:
_TEXT.png

所以doModInitFunctions就是在編譯調(diào)用上圖紅框處的區(qū)間里所有的函數(shù),其中就包含cxx函數(shù)的調(diào)用觸發(fā)。

cxx函數(shù)調(diào)用鏈
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions-->func()

那么回到之前的問題,objc_init是在哪里被調(diào)用呢?在initializeMainExecutable沒找到答案,那么接下來,只能使用終極大招了-->符號斷點。

符號斷點查看objc_init


前面的流程和cxx函數(shù)調(diào)用大致相同,在第3步時,會調(diào)用libSystem庫,再去到libdispatch庫,然后觸發(fā)_os_object_init-->_objc_init,請看下列圖:



上圖可知:objc_init調(diào)用鏈如下:

  1. _dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions
  2. libSystem_initlializer-->libdispatch_init-->_os_object_init-->_objc_init
  3. _objc_init-->注冊觀察者_(dá)dyld_objc_notify_register(&map_images, load_images, unmap_image)-->第2個參數(shù)load_images == sNotifyObjCInit,然后再在dyld加載的ImageLoader::recursiveInitialization這一步里notifySingle-->sNotifyObjCInit觸發(fā)回調(diào),讓_objc_init和dyld加載過程形成一個閉環(huán)。
main入口

上面我們知道了loadcxx函數(shù)的調(diào)用鏈,還剩下main()了,它是在哪里被調(diào)用的呢?智能在cxx函數(shù)里打斷點,然后打開匯編模式,跟著斷點一步步看了。


然后按住按鍵control,點擊step over

上圖紅框里發(fā)現(xiàn),其實和之前分析的流程基本一致,還是回到了_dyld_start,看來我們只有回到最初的匯編代碼里,去尋找main入口了:

在第3步也是_dyld_start的最后,找到了main(),此時才調(diào)用,當(dāng)然比loadcxx函數(shù)的調(diào)用時機都晚!

總結(jié)

借用Style_月月iOS-底層原理 15:dyld加載流程的dyld加載流程圖:

dyld加載流程

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

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