iOS App啟動(dòng)過程

總結(jié)來說凯旋,大體分為如下步驟:

(1) 系統(tǒng)為程序啟動(dòng)做好準(zhǔn)備

  • 當(dāng)kernel(內(nèi)核)做好程序的啟動(dòng)準(zhǔn)備工作之后,系統(tǒng)的執(zhí)行由內(nèi)核態(tài)轉(zhuǎn)換為用戶態(tài),由 dyld 首先開始工作

(2) 系統(tǒng)將控制權(quán)交給 Dyld呀潭,Dyld 會(huì)負(fù)責(zé)后續(xù)的工作

(3) Dyld 加載程序所需的動(dòng)態(tài)庫

(3) Dyld 對程序進(jìn)行 rebase 以及 bind 操作

(4) Objc SetUp

(5) 運(yùn)行初始化函數(shù)

(6) 執(zhí)行程序的 main 函數(shù)

image.png

dyld

dyld(the dynamic link editor), 動(dòng)態(tài)鏈接器,是專門用來加載動(dòng)態(tài)庫以及主程序的庫.當(dāng)kernel做好程序的啟動(dòng)準(zhǔn)備工作之后,系統(tǒng)的執(zhí)行由內(nèi)核態(tài)轉(zhuǎn)換為用戶態(tài),由 dyld 首先開始工作,iOS 中用到的所有系統(tǒng)framework都是動(dòng)態(tài)庫,比如最常用的UIKit.framework,Foundation.framework等都是通過dyld加載進(jìn)來的。

dyld 主要的工作有
  • 初始化 App 運(yùn)行環(huán)境
  • 鏈接依賴的動(dòng)態(tài)庫以及主程序
  • rebase / binding
  • 返回 main.m 的函數(shù)地址

接下來分析下dyld 的源碼


截屏2020-06-22 10.43.17.png

可以看到入口函數(shù)事在 dyid_start方法里的dyldbootstrap::start方法,接下來去源碼里看看. 在 dyld 源碼里找到dyldStartup.s找到了__dyld_start,這里只截取了arm架構(gòu)的部分.

image.png

通過注釋可以看到有調(diào)用dyldbootstrap::start,那順著調(diào)用再往下看. 在dyldInitialization.cpp中找到了start

image.png

  • 首先通過slideOfMainExecutable拿到隨機(jī)地址的偏移量
  • 調(diào)用rebaseDyld重定位
  • mach_init() mach消息初始化
  • __guard_setup() 棧溢出保護(hù) 接下來調(diào)用了dyld::_main,將返回值傳遞給__dyld_start的調(diào)用main.m函數(shù).

dyld::_main

dyld::_main是dyld中的關(guān)鍵方法,代碼也非常多,它的實(shí)現(xiàn)可以分為以下幾步: (關(guān)鍵部分有注釋)

  • 設(shè)置運(yùn)行環(huán)境
  • 加載共享緩存
  • 加載主程序
  • 加載動(dòng)態(tài)庫
  • 鏈接主程序
  • 鏈接動(dòng)態(tài)庫
  • 初始化主程序
  • 返回入口地址

設(shè)置運(yùn)行環(huán)境


    // Grab the cdHash of the main executable from the environment
    uint8_t mainExecutableCDHashBuffer[20];
    const uint8_t* mainExecutableCDHash = nullptr;
    // 獲取主程序hash
    if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
        mainExecutableCDHash = mainExecutableCDHashBuffer;

    // Trace dyld's load
    // 通知kernal內(nèi)核dyld文件已加載
    notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
#if !TARGET_IPHONE_SIMULATOR
    // Trace the main executable's load
    // 通知kernal內(nèi)核mach-o文件已加載
    notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif
    CRSetCrashLogMessage("dyld: launch started");
    //設(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];

    // mach-o 絕對路徑
    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;
        }
    }

加載共享緩存

// load shared cache
    // 判斷共享緩存庫是否被禁用
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
#if TARGET_IPHONE_SIMULATOR
    // <HACK> until <rdar://30773711> is fixed
    gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
    // </HACK>
#endif
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
        // 映射共享緩存到共享區(qū)
        mapSharedCache();
    }

checkSharedRegionDisable是檢查共享緩存是否禁用,里面可以看到一行注釋,iOS 必須開啟共享緩存才能運(yùn)行.

static void checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if __MAC_OS_X_VERSION_MIN_REQUIRED
    // if main executable has segments that overlap the shared region,
    // then disable using the shared region
    if ( mainExecutableMH->intersectsRange(SHARED_REGION_BASE, SHARED_REGION_SIZE) ) {
        gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
        if ( gLinkContext.verboseMapping )
            dyld::warn("disabling shared region because main executable overlaps\n");
    }
#if __i386__
    if ( !gLinkContext.allowEnvVarsPath ) {
        // <rdar://problem/15280847> use private or no shared region for suid processes
        gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
    }
#endif
#endif
    // iOS cannot run without shared region
}

接下來調(diào)的mapSharedCache()就是加載共享緩存的邏輯,就不深入了.

加載主程序

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

CRSetCrashLogMessage(sLoadingCrashMessage);
// instantiate ImageLoader for main executable
// 主程序?qū)嵗?// 這里調(diào)用比較深,后續(xù)再看
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

這一步將主程序 Mach-O 加載進(jìn)內(nèi)存,并實(shí)例化了一個(gè)ImageLoader.先看下instantiateFromLoadedImage的調(diào)用棧:


image.png

其中ImageLoader是一個(gè)抽象類,它的兩個(gè)子類ImageLoaderMachOCompressed、ImageLoaderMachOClassic負(fù)責(zé)把 Mach-O 實(shí)例化為 Image.但要用哪個(gè)子類來進(jìn)行實(shí)例化是通過sniffLoadCommands來判斷Mach-O 文件的 LINKEDIT 是classic或者compressed.

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

加載動(dòng)態(tài)庫

// load any inserted libraries
// 插入動(dòng)態(tài)庫
if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
    for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
        loadInsertedDylib(*lib);
}

遍歷DYLD_INSERT_LIBRARIES環(huán)境變量,然后調(diào)用loadInsertedDylib加載.

鏈接主程序

// link 主程序
// link調(diào)用比較深,后續(xù)來看
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;
}

調(diào)用 link鏈接主程序,內(nèi)核調(diào)用的是ImageLoader::link 函數(shù),主要是做了加載動(dòng)態(tài)庫、rebase节值、binding 等操作,代碼比較多,我就不貼了,在附件的源碼上有我寫的詳細(xì)注釋.

鏈接動(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
        // 鏈接動(dòng)態(tài)庫
        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);
            }
        }

這一步將前面調(diào)用 addImage()函數(shù)保存在sAllImages 中的動(dòng)態(tài)庫列表循環(huán)調(diào)用 link進(jìn)行鏈接,然后調(diào)registerInterposing注冊符號替換. 注意這里的 i+1, 因?yàn)閟AllImages中第一項(xiàng)是主程序,所以取 i+1項(xiàng).

初始化主程序

    CRSetCrashLogMessage("dyld: launch, running initializers");
        // 初始化主程序
    #if SUPPORT_OLD_CRT_INITIALIZATION
        // Old way is to run initializers via a callback from crt1.o
        if ( ! gRunInitializersOldWay ) 
            initializeMainExecutable(); 
    #else
        // run all initializers
        initializeMainExecutable(); 
    #endif

        // notify any montoring proccesses that this process is about to enter main()
        if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
            dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
        }
        notifyMonitoringDyldMain();

這一步由initializeMainExecutable()完成蒋得。dyld會(huì)優(yōu)先初始化動(dòng)態(tài)庫,然后初始化主程序。該函數(shù)首先執(zhí)行runInitializers(),內(nèi)部再依次調(diào)用processInitializers()、recursiveInitialization(),在recursiveInitialization()函數(shù)里找到了 notifySingle();

context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
復(fù)制代碼

再往下找到sNotifyObjCInit,再去找它的賦值找到registerObjCNotifiers,從函數(shù)注釋來看是用objc runtime來調(diào)的,這塊之后再看.在查閱一些資料之后得知,這里的sNotifyObjCInit就是調(diào)用 objc 中的 load_images,它調(diào)用所有的 load 方法,在調(diào)用完 load 方法以后調(diào)用了

bool hasInitializers = this->doInitialization(context);
復(fù)制代碼

doInitialization又調(diào)用了doModInitFunctions, 也就是constuctor方法,關(guān)于這個(gè)方法可以參看鏈接.

返回入口地址


        // find entry point for main executable
        // 從 mach-o 中讀取程序入口, 主程序則讀取LC_UNIXTHREAD, 就是 main.m
        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;

這里調(diào)用主程序的getEntryFromLC_MAIN,就是從``Load Command中讀取LC_MAIN入口,如果沒有,就讀取LC_UNIXTHREAD,然后跳到入口處執(zhí)行,就回到了我們熟悉的main.m`.

App啟動(dòng)邏輯

最后 dyld 會(huì)調(diào)用 main() 函數(shù)狸棍。main() 會(huì)調(diào)用 UIApplicationMain(),程序啟動(dòng)味悄。

main.m文件草戈,此處就是應(yīng)用的入口了。程序啟動(dòng)時(shí)傍菇,先執(zhí)行main函數(shù)猾瘸,main函數(shù)是ios程序的入口點(diǎn),內(nèi)部會(huì)調(diào)用UIApplicationMain函數(shù)丢习,UIApplicationMain里會(huì)創(chuàng)建一個(gè)UIApplication對象 牵触,然后創(chuàng)建UIApplication的delegate對象 —–(您的)AppDelegate ,開啟一個(gè)消息循環(huán)(main runloop)咐低,每當(dāng)監(jiān)聽到對應(yīng)的系統(tǒng)事件時(shí)揽思,就會(huì)通知AppDelegate。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
image.png
說明

帶注釋 dyld源碼地址: Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末见擦,一起剝皮案震驚了整個(gè)濱河市钉汗,隨后出現(xiàn)的幾起案子羹令,更是在濱河造成了極大的恐慌,老刑警劉巖损痰,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件福侈,死亡現(xiàn)場離奇詭異,居然都是意外死亡卢未,警方通過查閱死者的電腦和手機(jī)肪凛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辽社,“玉大人伟墙,你說我怎么就攤上這事〉吻Γ” “怎么了戳葵?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長汉匙。 經(jīng)常有香客問我拱烁,道長,這世上最難降的妖魔是什么噩翠? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任邻梆,我火速辦了婚禮,結(jié)果婚禮上绎秒,老公的妹妹穿的比我還像新娘。我一直安慰自己尼摹,他們只是感情好见芹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蠢涝,像睡著了一般玄呛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上和二,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天徘铝,我揣著相機(jī)與錄音,去河邊找鬼惯吕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的甲锡。 我是一名探鬼主播虎韵,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼呆奕,長吁一口氣:“原來是場噩夢啊……” “哼梁钾!你這毒婦竟也來了姆泻?” 一聲冷哼從身側(cè)響起拇勃,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后妓肢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年横腿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遍搞。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖诊县,靈堂內(nèi)的尸體忽然破棺而出依痊,到底是詐尸還是另有隱情胸嘁,我是刑警寧澤性宏,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布诬辈,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咧党,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陨亡。 院中可真熱鬧傍衡,春花似錦深员、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至绣的,卻和暖如春叠赐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背屡江。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工芭概, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惩嘉。 一個(gè)月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓罢洲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宏怔。 傳聞我的和親對象是個(gè)殘疾皇子奏路,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355