iOS底層探索-程序加載preMain

前言

涉及內(nèi)容較多,很多細(xì)節(jié)需要進一步探索框喳,希望同學(xué)們多多批評指正佣渴。

XNU加載app

參考資料:
iOS 系統(tǒng)內(nèi)核 XNU:App 如何加載透硝?
XNU源碼

  1. fork 新進程
  2. 為Mach-O分配內(nèi)存
  3. 解析Mach-O
  4. 讀取Mach-O 頭文件
  5. 遍歷load command信息,將Mach-O映射到內(nèi)存宪肖,設(shè)置執(zhí)行app的入口點表制。
  6. 啟動dyld

總體來說,XNU加載就是為Mach-O創(chuàng)建一個新進程控乾,建立虛擬內(nèi)存空間么介,解析Mach-O文件,最后映射到存空間蜕衡,設(shè)置執(zhí)行App的入口點壤短。

設(shè)置完入口點后會通過 load_dylinker() 函數(shù)來解析加載 dyld,然后將入口點地址改成 dyld 的入口地址慨仿。這一步完后久脯,內(nèi)核部分就完成了 Mach-O 文件的加載。剩下的就是用戶態(tài)層 dyld 加載 App 了

dyld

參考資料:
dyld: Dynamic Linking On OS X
鏈接器:符號是怎么綁定到地址上的
dyld3-wwdc
iOS應(yīng)用的啟動流程和優(yōu)化詳解

dyld 是英文the dynamic link editor的簡寫镰吆,也就是動態(tài)鏈接器帘撰,是蘋果操作系統(tǒng)的一個重要組成部分。在iOS/Mac OSX系統(tǒng)中万皿,僅有很少量進程只需要內(nèi)核就能完成加載骡和,基本上所有的進程都需要動態(tài)鏈接的相赁。
Mach-O鏡像文件中會有很多對外部的庫和符號的引用,但是這些引用并不能直接用(對于動態(tài)庫的符號慰于,是undefined)钮科,這個填補工作就是由動態(tài)鏈器dyld來完成。

(undefined) external _NSLog (from Foundation) 
(undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation)

概括講婆赠,dyld主要做了下面幾件事

1. loading

先執(zhí)行Mach-O文件绵脯,根據(jù)Mach-O中的undefined符號加載對應(yīng)的dylib,系統(tǒng)會設(shè)置一個共享緩存來解決加載的遞歸依賴問題(在load函數(shù)有一個算法做相應(yīng)的事情),把dylib映射到進程內(nèi)存休里。

2. linking

  • rebase 修復(fù)指向當(dāng)前鏡像內(nèi)部的資源指針
ASLR:是Address Space Layout Randomization(地址空間布局隨機化)的簡稱蛆挫。App在被啟動的時候,程序和dylib會被映射到邏輯地址空間妙黍,這個邏輯地址空間有一個起始地址悴侵,ASLR技術(shù)讓這個起始地址是隨機的。這個地址如果是固定的拭嫁,黑客很容易就用起始地址+函數(shù)偏移地址找到對應(yīng)的函數(shù)地址可免。
Code Sign:就是蘋果代碼加密簽名機制,但是在Code Sign操作的時候做粤,加密的哈希不是針對整個文件浇借,而是針對每一個Page的。這個就保證了dyld在加載的時候怕品,可以對每個page進行獨立的驗證
  • bind 將符號綁定到動態(tài)庫里對應(yīng)的地址上妇垢,bind指向的是鏡像外部的資源指針(跨鏡像)

3. static initailizers

  • 當(dāng)我們dylib被映射到進程內(nèi)存,并被鏈接肉康,就需要對這些資源進行必要的初始化闯估,在iOS系統(tǒng)libsystem,libdispatch吼和,libObjc睬愤,這幾個庫有較高的優(yōu)先級,會被確保最先被初始化纹安。
  • libObjc會向dyld注冊回調(diào)函數(shù)尤辱,被加載的庫包括我們的主程序 通過回調(diào)函數(shù)把oc類,符號等內(nèi)容(比如我們聲明的自定義class和selector)交給libObjc庫管理厢岂。這也是我們下一篇內(nèi)容要研究的重點光督。
_dyld_objc_notify_register(&map_images, load_images, unmap_image)

4.skip main

至此,可執(zhí)行文件和動態(tài)庫都已被加載到內(nèi)存塔粒,各種資源指針都被修復(fù)指向正確的內(nèi)存地址结借,必要的初始化完成。dyld跳轉(zhuǎn)到main函數(shù)并執(zhí)行卒茬。

dyld代碼流程
截屏2021-07-14 下午1.25.31.png

用模擬跑的船老,真機流程不一樣的

dyldbootstrap::start

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
    /**
        1. rebase
     */
    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

    _subsystem_init(apple);

    // 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);
}
  • rebaseDyld
  • runDyldIntializers
  • runDyldInitializers
  • call dyld:_main
    發(fā)現(xiàn)沒有咖熟,一個庫要被進程使用,需要做的就是那么幾件事柳畔,
    load映射進內(nèi)存馍管,
    rebase/bind進行必要的資源指針修復(fù),
    一些環(huán)境變量的設(shè)置,
    Initializer必要的初始化薪韩,
    然后這個庫就可以被使用了确沸,dyld也不例外,然后dyld可以開心去加載其他動態(tài)庫了

dyld::_main

代碼非常長,貼幾個重要的片段俘陷,有興趣可以下載源碼看下

配置環(huán)境變量
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
    if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
        launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
    }

    //Check and see if there are any kernel flags
    dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));

#if __has_feature(ptrauth_calls)
    // Check and see if kernel disabled JOP pointer signing (which lets us load plain arm64 binaries)
    if ( const char* disableStr = _simple_getenv(apple, "ptrauth_disabled") ) {
        if ( strcmp(disableStr, "1") == 0 )
            sKeysDisabled = true;
    }
    else {
        // needed until kernel passes ptrauth_disabled for arm64 main executables
        if ( (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_V8) || (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_ALL) )
            sKeysDisabled = true;
    }
#endif

    // Grab the cdHash of the main executable from the environment
    uint8_t mainExecutableCDHashBuffer[20];
    const uint8_t* mainExecutableCDHash = nullptr;
    if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
        unsigned bufferLenUsed;
        if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
            mainExecutableCDHash = mainExecutableCDHashBuffer;
    }

    getHostInfo(mainExecutableMH, mainExecutableSlide);

#if !TARGET_OS_SIMULATOR
    // Trace dyld's load
    notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
    // Trace the main executable's load
    notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif

    uintptr_t result = 0;
    sMainExecutableMachHeader = mainExecutableMH;
    sMainExecutableSlide = mainExecutableSlide;


    // Set the platform ID in the all image infos so debuggers can tell the process type
    // FIXME: This can all be removed once we make the kernel handle it in rdar://43369446
    // The host may not have the platform field in its struct, but there's space for it in the padding, so always set it
    {
        __block bool platformFound = false;
        ((dyld3::MachOFile*)mainExecutableMH)->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) {
            if (platformFound) {
                halt("MH_EXECUTE binaries may only specify one platform");
            }
            gProcessInfo->platform = (uint32_t)platform;
            platformFound = true;
        });
        if (gProcessInfo->platform == (uint32_t)dyld3::Platform::unknown) {
            // There were no platforms found in the binary. This may occur on macOS for alternate toolchains and old binaries.
            // It should never occur on any of our embedded platforms.
#if TARGET_OS_OSX
            gProcessInfo->platform = (uint32_t)dyld3::Platform::macOS;
#else
            halt("MH_EXECUTE binaries must specify a minimum supported OS version");
#endif
        }
    }

#if TARGET_OS_OSX
    // Check to see if we need to override the platform
    const char* forcedPlatform = _simple_getenv(envp, "DYLD_FORCE_PLATFORM");
    if (forcedPlatform) {
        dyld_platform_t forcedPlatformType = 0;
        if (strncmp(forcedPlatform, "6", 1) == 0) {
            forcedPlatformType = PLATFORM_MACCATALYST;
        } else if (strncmp(forcedPlatform, "2", 1) == 0) {
            forcedPlatformType = PLATFORM_IOS;
        } else  {
            halt("DYLD_FORCE_PLATFORM is only supported for platform 2 or 6.");
        }
        const dyld3::MachOFile* mf = (dyld3::MachOFile*)sMainExecutableMachHeader;
        if (mf->allowsAlternatePlatform()) {
            gProcessInfo->platform = forcedPlatformType;
        }
    }

    // if this is host dyld, check to see if iOS simulator is being run
    const char* rootPath = _simple_getenv(envp, "DYLD_ROOT_PATH");
    if ( (rootPath != NULL) ) {
        // look to see if simulator has its own dyld
        char simDyldPath[PATH_MAX]; 
        strlcpy(simDyldPath, rootPath, PATH_MAX);
        strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
        int fd = dyld3::open(simDyldPath, O_RDONLY, 0);
        if ( fd != -1 ) {
            //TODO:模擬器流程分支return
            const char* errMessage = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue, &result);
            if ( errMessage != NULL )
                halt(errMessage);
            return result;
        }
    }
    else {
        ((dyld3::MachOFile*)mainExecutableMH)->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) {
            if ( dyld3::MachOFile::isSimulatorPlatform(platform) )
                halt("attempt to run simulator program outside simulator (DYLD_ROOT_PATH not set)");
        });
    }
#endif

    CRSetCrashLogMessage("dyld: launch started");
    //TODO:設(shè)置上下文
    setContext(mainExecutableMH, argc, argv, envp, apple);

    // Pickup the pointer to the exec path.
    //TODO: 獲取可執(zhí)行路徑
    sExecPath = _simple_getenv(apple, "executable_path");

    // <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
    if (!sExecPath) sExecPath = apple[0];

#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    // <rdar://54095622> kernel is not passing a real path for main executable
    if ( strncmp(sExecPath, "/var/containers/Bundle/Application/", 35) == 0 ) {
        if ( char* newPath = (char*)malloc(strlen(sExecPath)+10) ) {
            strcpy(newPath, "/private");
            strcat(newPath, sExecPath);
            sExecPath = newPath;
        }
    }
#endif

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

    // Remember short name of process for later logging
    sExecShortName = ::strrchr(sExecPath, '/');
    if ( sExecShortName != NULL )
        ++sExecShortName;
    else
        sExecShortName = sExecPath;

#if TARGET_OS_OSX && __has_feature(ptrauth_calls)
    // on Apple Silicon macOS, only Apple signed ("platform binary") arm64e can be loaded
    sOnlyPlatformArm64e = true;

    // internal builds, or if boot-arg is set, then non-platform-binary arm64e slices can be run
    if ( const char* abiMode = _simple_getenv(apple, "arm64e_abi") ) {
        if ( strcmp(abiMode, "all") == 0 )
            sOnlyPlatformArm64e = false;
    }
#endif
    //設(shè)置進程限制條件
    configureProcessRestrictions(mainExecutableMH, envp);

很多check/比較/set/get,對一些環(huán)境變量讀取罗捎,校驗,設(shè)置拉盾。

加載共享緩存

從iOS3.1開始桨菜,為了提高性能,絕大部分的系統(tǒng)動態(tài)庫文件都打包存放到了一個緩存文件中捉偏。共享緩存中存的都是系統(tǒng)級別的動態(tài)庫倒得。自己常見的動態(tài)庫或者第三方動態(tài)庫不會放到共享緩存中。

    // load shared cache
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
        if ( sSharedCacheOverrideDir)
            mapSharedCache(mainExecutableSlide);
#else
        mapSharedCache(mainExecutableSlide);
#endif

        // If this process wants a different __DATA_CONST state from the shared region, then override that now
        if ( (sSharedCacheLoadInfo.loadAddress != nullptr) && (gEnableSharedCacheDataConst != sharedCacheDataConstIsEnabled) ) {
            uint32_t permissions = gEnableSharedCacheDataConst ? VM_PROT_READ : (VM_PROT_READ | VM_PROT_WRITE);
            sSharedCacheLoadInfo.loadAddress->changeDataConstPermissions(mach_task_self(), permissions,
                                                                         (gLinkContext.verboseMapping ? &dyld::log : nullptr));
        }
    }
  • 核心函數(shù)mapSharedCache(mainExecutableSlide)
  • mapSharedCache中調(diào)用loadDyldCache加載共享緩存
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
    results->loadAddress        = 0;
    results->slide              = 0;
    results->errorMessage       = nullptr;

#if TARGET_OS_SIMULATOR
    // simulator only supports mmap()ing cache privately into process
    return mapCachePrivate(options, results);
#else
    if ( options.forcePrivate ) {
        // mmap cache into this process only
        return mapCachePrivate(options, results);
    }
    else {
        // fast path: when cache is already mapped into shared region
        bool hasError = false;
        if ( reuseExistingCache(options, results) ) {
            hasError = (results->errorMessage != nullptr);
        } else {
            // slow path: this is first process to load cache
            hasError = mapCacheSystemWide(options, results);
        }
        return hasError;
    }
#endif
}
  • 強制私有
  • 共享緩存已有
  • 第一次加載 這里會進入到// should be in mach/shared_region.h
dyld3 或 dyld2
  //判斷是否使用閉包模式也是dyld3的模式啟動 ClosureMode::on 用dyld3 否則使用dyld2
  if ( sClosureMode == ClosureMode::Off ) {
    //dyld2
    if ( gLinkContext.verboseWarnings )
            dyld::log("dyld: not using closures\n");
  } else {
    //dyld3  DYLD_LAUNCH_MODE_USING_CLOSURE 用閉包模式
    sLaunchModeUsed = DYLD_LAUNCH_MODE_USING_CLOSURE;
    const dyld3::closure::LaunchClosure* mainClosure = nullptr;
    dyld3::closure::LoadedFileInfo mainFileInfo;
    mainFileInfo.fileContent = mainExecutableMH;
    mainFileInfo.path = sExecPath;
    ...
    // 首先到共享緩存中去找是否有dyld3的mainClosure
    if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
            mainClosure = sSharedCacheLoadInfo.loadAddress->findClosure(sExecPath);
            ...
    }
 
   ...
    //如果共享緩存中有告私,然后去驗證closure是否是有效的
    if ( (mainClosure != nullptr) && !closureValid(mainClosure, mainFileInfo, 
    、mainExecutableCDHash, true, envp) ) {
            mainClosure = nullptr;
            sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
    }
    
    bool allowClosureRebuilds = false;
    if ( sClosureMode == ClosureMode::On ) {
            allowClosureRebuilds = true;
    } 
    ...
    
    //如果沒有在共享緩存中找到有效的closure 此時就會自動創(chuàng)建一個closure
    if ( (mainClosure == nullptr) && allowClosureRebuilds ) {
        ...
        if ( mainClosure == nullptr ) { 
        // 創(chuàng)建一個mainClosure
        mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, 
        bootToken);
        if ( mainClosure != nullptr )
                sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
        }
    }
   
    // try using launch closure
    // dyld3 開始啟動
    if ( mainClosure != nullptr ) {
        CRSetCrashLogMessage("dyld3: launch started");
        ...
        //啟動 launchWithClosure
        bool launched = launchWithClosure(mainClosure, 
        sSharedCacheLoadInfo.loadAddress,(dyld3::MachOLoaded*)mainExecutableMH,...);
         //啟動失敗                                                              
        if ( !launched && closureOutOfDate && allowClosureRebuilds ) {
                // closure is out of date, build new one
                // 如果啟動失敗 重新去創(chuàng)建mainClosure
                mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, 
                envp, bootToken);
                if ( mainClosure != nullptr ) {
                    ...
                    //dyld3再次啟動
                    launched = launchWithClosure(mainClosure,  sSharedCacheLoadInfo.loadAddress,
                    (dyld3::MachOLoaded*)mainExecutableMH,...);
                }
            }
            if ( launched ) {
                    gLinkContext.startedInitializingMainExecutable = true;
                    if (sSkipMain)
                    //啟動成功直接返回main函數(shù)的地址
                    result = (uintptr_t)&fake_main;
                    return result;
            }
            else {  
            //啟動失敗      
            }
    }
}
  • dyld3優(yōu)化
  1. 加載速度
    1.1. 一個deamon進程的解析器承桥,預(yù)處理所有可能影響啟動速度的search path驻粟,@path和環(huán)境變量
    1.2. 然后分析Mach-O的header和依賴,并完成所有符號查找的工作
    1.3. 然后將這些結(jié)構(gòu)創(chuàng)建成一個啟動閉包凶异,系統(tǒng)app的啟動閉包被構(gòu)建在sharedCache中蜀撑,第三方的app,在程序安裝或者更新的時候構(gòu)建這個啟動閉包剩彬,這些都在程序啟動前已經(jīng)被完成
    1.4 閉包被構(gòu)建在shared cache中酷麦,我們甚至不需要打開一個單獨的文件,加載速度很快
  2. 安全性
    加載閉包喉恋,并驗證啟動閉包的安全性沃饶,在dyld3之前在程序啟動時,dyld遞歸分析mach-oheader的依賴轻黑,可能修改并注入依賴庫的問題
  3. 看個官方的對比圖
    851626253562_.pic.jpg
實例化主程序

dyld3 和 dyld2走的流程差不多糊肤,dyld3 用的是閉包模式,更快氓鄙,更安全馆揉。imge是鏡像文件的意思,鏡像文件就是從磁盤映射到內(nèi)存的mach-O文件抖拦∩ǎ可以理解為只要是加載到內(nèi)存的mach-o文件就叫鏡像文件舷暮。

        //TODO: 實例化主程序,返回imageLoader對象,并交給dyld管理
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        //主程序賦值給glinkContext
        gLinkContext.mainExecutable = sMainExecutable;
       //主程序是否代碼簽名
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

For each executable file (dynamic shared object) in use, an ImageLoader is instantiated.

  • 主程序在dyld之前已經(jīng)被系統(tǒng)內(nèi)核映射到進程緩存噩茄。從上面這段官方注釋可知下面,任何一個可執(zhí)行文件要被使用,需要實例化一個imageLoader巢墅。
  • 實例化主程序的作用是為主可執(zhí)行文件實例化為一個ImageLoaderMachO對象诸狭,可以看做把主可執(zhí)行文件抽象為ImageLoaderMachO的實例,交給dyld管理君纫,并被程序使用驯遇。
  • 添加到了dyld管理的MappedRanges主列表-addImage()
實例化動態(tài)庫-加載插入的動態(tài)庫
        // load any inserted libraries
        // TODO:加載插入的庫
        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;
//核心函數(shù)-load
ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
//實例化一個ImageLoader
// map in file and instantiate an ImageLoader
static ImageLoader* loadPhase6(int fd, const struct stat& stat_buf, const char* path, const LoadContext& context)
// create image by mapping in a mach-o file
ImageLoader* ImageLoaderMachO::instantiateFromFile
//添加到dyld管理的主列表
static ImageLoader* checkandAddImage(ImageLoader* image, const LoadContext& context)
  • 做的事情跟實例化主程序差不多,就是把插入的動態(tài)庫都是實例化一個imageLoader,并添加到dyld管理的主列表
  • load方法實現(xiàn)了一個算法蓄髓,避免重復(fù)的庫文件加載
鏈接主程序
        // TODO:鏈接主程序
        gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
        if ( mainExcutableAlreadyRebased ) {
            // previous link() on main executable has already adjusted its internal pointers for ASLR
            // work around that by rebasing by inverse amount
            sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
        }
#endif
        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;
        }
鏈接動態(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
        // TODO:鏈接動態(tài)庫 循環(huán)
        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();
            }
            if ( gLinkContext.allowInterposing ) {
                // 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);
                }
            }
        }

鏈接主程序和鏈接動態(tài)庫邏輯基本一樣叉庐,核心函數(shù)是link()

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
    //dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
    
    // clear error strings
    (*context.setErrorStrings)(0, NULL, NULL, NULL);

    uint64_t t0 = mach_absolute_time();
//遞歸loadLibraries
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
    context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);

    // we only do the loading step for preflights
    if ( preflightOnly )
        return;

    uint64_t t1 = mach_absolute_time();
    context.clearAllDepths();
    this->updateDepth(context.imageCount());

    __block uint64_t t2, t3, t4, t5;
    {
        dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
        t2 = mach_absolute_time();
//遞歸rebase
        this->recursiveRebaseWithAccounting(context);
        context.notifyBatch(dyld_image_state_rebased, false);

        t3 = mach_absolute_time();
               //初始化主程序時,賦值為true
        if ( !context.linkingMainExecutable )
            this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);

        t4 = mach_absolute_time();
        if ( !context.linkingMainExecutable )
            this->weakBind(context);
        t5 = mach_absolute_time();
    }
  • link()函數(shù)主要做的事情
  1. recursiveLoadLibraries()会喝,保存一個依賴庫的數(shù)組陡叠,方便在內(nèi)存中找到自己的依賴庫,后面符號綁定時候也會用到這個數(shù)組肢执。
  2. recursiveRebaseWithAccounting(context)
主程序和動態(tài)庫的綁定
        // Bind and notify for the main executable now that interposing has been registered
        uint64_t bindMainExecutableStartTime = mach_absolute_time();
        sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
        uint64_t bindMainExecutableEndTime = mach_absolute_time();
        ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
        gLinkContext.notifyBatch(dyld_image_state_bound, false);

        // Bind and notify for the inserted images now interposing has been registered
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
            }
        }
        
        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        // TODO:主程序弱綁定(after all inserted images linked)
        sMainExecutable->weakBind(gLinkContext);
        gLinkContext.linkingMainExecutable = false;

        sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);
  • recursiveBind()
  • 對主可執(zhí)行文件和動態(tài)庫進行符號綁定操作枉阵,用到保存的libImages數(shù)組
  • 數(shù)據(jù)fixup完成后把一些數(shù)據(jù)段設(shè)為只讀
運行初始化方法

所有鏡像文件都已加載,并且資源指針也都修復(fù)完畢预茄,可以做一些必要的初始化了兴溜。

        // TODO:主程序初始化
        initializeMainExecutable(); 
void initializeMainExecutable()
{
    // record that we've reached this step
    gLinkContext.startedInitializingMainExecutable = true;

    // run initialzers for any inserted dylibs
    // 運行所有的dylibs中的initialzers方法
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    //先運行動態(tài)庫的初始化方法
    if ( rootCount > 1 ) {
            for(size_t i=1; i < rootCount; ++i) {
               sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
            }
    }

    // run initializers for main executable and everything it brings up 
    // 運行主程序的初始化方法
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

    ...
}
  • 這里是我們關(guān)注的重點
  • _objc_init在這里別調(diào)用,向dyld注冊回調(diào)函數(shù)耻陕,通過回調(diào)函數(shù)拙徽,各個執(zhí)行文件的oc class,協(xié)議诗宣,方法膘怕,符號等內(nèi)容將交給libObjc處理,包括我們主可執(zhí)行文件(也就是我們自己code出來的oc代碼)召庞。篇幅有限岛心,下一篇我們再著重講解。
返回main函數(shù)
    if (sSkipMain) {
         notifyMonitoringDyldMain();
        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);
        }
        ARIADNEDBG_CODE(220, 1);
        result = (uintptr_t)&fake_main;
        *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
    }

    return result;

總結(jié):

分析了main函數(shù)之前篮灼,iOS程序的加載過程

  1. 首先內(nèi)核fork進程鹉梨,分配進程內(nèi)存,把主可執(zhí)行文件map到內(nèi)存,并啟動dyld穿稳,這一步從內(nèi)核態(tài)過渡到用戶態(tài)
  2. dyld 首先會rebaseSelf,并做必要的環(huán)境設(shè)置存皂,然后分析mainExecutableMH查找可用的共享緩存,并加載。這里我們提到了
    dyld2 和 dyld3旦袋,以及dyld3相對于dyld2在啟動速度和安全上做的優(yōu)化骤菠。
  3. dyld的主要作用是,分析主程序mach-o文件疤孕,動態(tài)加載三方庫商乎,映進內(nèi)存,并對他們進行管理祭阀。由于ASLR及代碼簽名的原因鹉戚,需要對image進行rebase和binding操作,目的是讓程序內(nèi)的資源指針指向正確的內(nèi)存地址专控。在所有資源修復(fù)完畢之后抹凳,執(zhí)行主可執(zhí)行文件的初始化。也就是loading伦腐,rebase/binding,initializer三件套赢底。
  4. 在initializer中,dyld會保證最下層的動態(tài)庫libsystem被最先初始化柏蘑,libDispatch/libObjc也會很早被調(diào)用初始化幸冻。libObjc的初始化,會向dyld注冊回調(diào)函數(shù)咳焚,用于管理所有可執(zhí)行文件的OC部分洽损。放在下一篇來分析

思考:

我們已經(jīng)知道了main函數(shù)之前,程序的啟動的大致流程革半,我們可以從那幾個方面來提升程序的啟動速度碑定?

以下內(nèi)容,來自wwdc

  1. less dylib (減少庫加載督惰,必要時可以合并庫)
  2. less classed and methods (在加載時不傅,需要被管理旅掂,修復(fù)指針)
  3. less initializer (初始化主程序時赏胚,會調(diào)用所有的動態(tài)庫的initializer和c++構(gòu)造函數(shù))
  4. more swift (no initializer,不允許特定類型的未對齊數(shù)據(jù))
  5. less load
    總之商虐,你寫越少的代碼觉阅,程序啟動越快??????。這篇內(nèi)容寫得還漫長秘车,能力有限典勇,中間可能有不正確或者不準(zhǔn)確的地方,希望大家能在評論區(qū)多多留言交流叮趴。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末割笙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伤溉,老刑警劉巖般码,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異乱顾,居然都是意外死亡板祝,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門走净,熙熙樓的掌柜王于貴愁眉苦臉地迎上來券时,“玉大人,你說我怎么就攤上這事伏伯¢俣矗” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵舵鳞,是天一觀的道長震檩。 經(jīng)常有香客問我,道長蜓堕,這世上最難降的妖魔是什么抛虏? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮套才,結(jié)果婚禮上迂猴,老公的妹妹穿的比我還像新娘。我一直安慰自己背伴,他們只是感情好沸毁,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著傻寂,像睡著了一般息尺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疾掰,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天搂誉,我揣著相機與錄音,去河邊找鬼静檬。 笑死炭懊,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拂檩。 我是一名探鬼主播侮腹,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼稻励!你這毒婦竟也來了父阻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎加矛,沒想到半個月后钠署,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡荒椭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年谐鼎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趣惠。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡狸棍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出味悄,到底是詐尸還是另有隱情草戈,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布侍瑟,位于F島的核電站唐片,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏涨颜。R本人自食惡果不足惜费韭,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望庭瑰。 院中可真熱鬧星持,春花似錦、人聲如沸弹灭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽穷吮。三九已至逻翁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捡鱼,已是汗流浹背八回。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留堰汉,地道東北人辽社。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓伟墙,卻偏偏與公主長得像翘鸭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子戳葵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容