App啟動(dòng)過程 - dyld加載動(dòng)態(tài)庫

開頭

在MacOS和iOS上,可執(zhí)行程序的啟動(dòng)依賴于xnu內(nèi)核進(jìn)程運(yùn)作和動(dòng)態(tài)鏈接加載器dyld。其中后者的執(zhí)行時(shí)長可以通過開發(fā)階段中在Xcode的schema指定環(huán)境變量 DYLD_PRINT_STATISTICS 為true來指示在調(diào)試環(huán)境下打印,結(jié)果如下:

Total pre-main time: 847.45 milliseconds (100.0%)
         dylib loading time:  82.91 milliseconds (9.7%)
        rebase/binding time: 600.04 milliseconds (70.8%)
            ObjC setup time:  68.04 milliseconds (8.0%)
           initializer time:  96.24 milliseconds (11.3%)
           slowest intializers :
             libSystem.B.dylib :  10.75 milliseconds (1.2%)
    libMainThreadChecker.dylib :  17.94 milliseconds (2.1%)
           MeridianLBSTestDemo : 109.76 milliseconds (12.9%)

通過輸出日志,可以知道一些main函數(shù)執(zhí)行之前的事件,以及各個(gè)事件的耗時(shí)分布情況。這些事件的背后負(fù)責(zé)人就是dyld,它會(huì)將App依賴的動(dòng)態(tài)庫和App文件加載到內(nèi)存以后執(zhí)行,動(dòng)態(tài)庫不是可執(zhí)行文件,無法獨(dú)自執(zhí)行。當(dāng)點(diǎn)擊App的時(shí)候,系統(tǒng)在內(nèi)核態(tài)完成一些必要配置,從App的MachO文件解析出dyld的地址,這里會(huì)記錄在MachO的LC_LOAD_DYLINKER命令中,內(nèi)容參考如下:

          cmd LC_LOAD_DYLINKER
      cmdsize 28
         name /usr/lib/dyld (offset 12)
Load command 8
     cmd LC_UUID
 cmdsize 24
    uuid DF0F9B2D-A4D7-37D0-BC6B-DB0297766CE8
Load command 9
      cmd LC_VERSION_MIN_IPHONEOS

dyld的地址在 /usr/lib/dyld中找到。解析加載完畢之后會(huì)執(zhí)行dyld,運(yùn)行在用戶態(tài)進(jìn)程(此處不做深入)。dyld的源碼是開源的,參考官方的551版,下面會(huì)對(duì)從dyld加載庫文件到app的main函數(shù)被執(zhí)行這段過程做簡單分析總結(jié),便于充分了解App的啟動(dòng)原理。

__dyld_start

系統(tǒng)內(nèi)核在加載動(dòng)態(tài)庫前,會(huì)加載dyld,然后調(diào)用去執(zhí)行__dyld_start(匯編語言實(shí)現(xiàn)的方法)。該函數(shù)會(huì)執(zhí)行dyldbootstrap::start(),后者會(huì)執(zhí)行_main()函數(shù),dyld的加載動(dòng)態(tài)庫的代碼就是從_main()開始執(zhí)行的。這里可以查看 dyldStartup.s的部分內(nèi)容(以x86_x64架構(gòu)做參考),其中標(biāo)出了 _dyld_start()與 dyldbootstrap的start方法。


#if __x86_64__
#if !TARGET_IPHONE_SIMULATOR
    .data
    .align 3
__dyld_start_static: 
    .quad   __dyld_start
#endif


#if !TARGET_IPHONE_SIMULATOR
    .text
    .align 2,0x90
    .globl __dyld_start
__dyld_start:
    popq    %rdi        # param1 = mh of app
    pushq   $0      # push a zero for debugger end of frames marker
    movq    %rsp,%rbp   # pointer to base of kernel frame
    andq    $-16,%rsp       # force SSE alignment
    subq    $16,%rsp    # room for local variables
    
    # call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
    movl    8(%rbp),%esi    # param2 = argc into %esi
    leaq    16(%rbp),%rdx   # param3 = &argv[0] into %rdx
    movq    __dyld_start_static(%rip), %r8
    leaq    __dyld_start(%rip), %rcx
    subq     %r8, %rcx  # param4 = slide into %rcx
    leaq    ___dso_handle(%rip),%r8 # param5 = dyldsMachHeader
    leaq    -8(%rbp),%r9
    call    __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
    movq    -8(%rbp),%rdi
    cmpq    $0,%rdi
    jne Lnew

start方法的核心方法是dyld::_main,在這里完成app的程序入口的配置。下面會(huì)對(duì)main方法做詳細(xì)分析。


//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
                intptr_t slide, const struct macho_header* dyldsMachHeader,
                uintptr_t* startGlue)
{
    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

    // allow dyld to use mach messaging
    mach_init();
  
  //其他配置
  // ...
    // now that we are done bootstrapping dyld, call dyld's main
  //ASLR
    uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyld::_main

下面開始深入分析_main方法(只對(duì)方法中的關(guān)注部分做遞歸深入,其余部分不做分析)。


// Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
    dyld3::kdebug_trace_dyld_signpost(DBG_DYLD_SIGNPOST_START_DYLD, 0, 0);

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

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

    uintptr_t result = 0;
    sMainExecutableMachHeader = mainExecutableMH;
    sMainExecutableSlide = mainExecutableSlide;
#if __MAC_OS_X_VERSION_MIN_REQUIRED
    // 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 = my_open(simDyldPath, O_RDONLY, 0);
        if ( fd != -1 ) {
            const char* errMessage = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue, &result);
            if ( errMessage != NULL )
                halt(errMessage);
            return result;
        }
    }
#endif

    CRSetCrashLogMessage("dyld: launch started");

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

    configureProcessRestrictions(mainExecutableMH);

#if __MAC_OS_X_VERSION_MIN_REQUIRED
    if ( gLinkContext.processIsRestricted ) {
        pruneEnvironmentVariables(envp, &apple);
        // set again because envp and apple may have changed or moved
        setContext(mainExecutableMH, argc, argv, envp, apple);
    }
    else
#endif
    {
        checkEnvironmentVariables(envp);
        defaultUninitializedFallbackPaths(envp);
    }
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    getHostInfo(mainExecutableMH, mainExecutableSlide);
  // ...todo

第一步:設(shè)置運(yùn)行環(huán)境,配置環(huán)境變量。

開始的時(shí)候,告知內(nèi)核dyld的uuid和主App的uuid信息。將傳入的變量mainExecutableMH和mainExecutableSlide賦值給sMainExecutableMachHeader和sMainExecutableSlide保存,這兩個(gè)分別是mach_header類型結(jié)構(gòu)體和long類型數(shù)據(jù),表示當(dāng)前App的Mach-O頭部信息和ASLR位移長度;有了頭部信息,加載器就可以從頭開始,遍歷整個(gè)Mach-O文件的信息。 接著執(zhí)行了setContext(),此方法設(shè)置了全局一個(gè)鏈接上下文,包括一些回調(diào)函數(shù)、參數(shù)與標(biāo)志設(shè)置信息,其中的context結(jié)構(gòu)體實(shí)例、回調(diào)函數(shù)都是dyld自己的實(shí)現(xiàn)。代碼片斷如下:


    // setContext ...
    gLinkContext.loadLibrary            = &libraryLocator;
    gLinkContext.terminationRecorder    = &terminationRecorder;
    gLinkContext.flatExportFinder       = &flatFindExportedSymbol;
    gLinkContext.coalescedExportFinder  = &findCoalescedExportedSymbol;
    gLinkContext.getCoalescedImages     = &getCoalescedImages;
  //...

接著執(zhí)行configureProcessRestrictions函數(shù),根據(jù)當(dāng)前進(jìn)程是否受限,再次配置鏈接上下文以及其他環(huán)境參數(shù); 對(duì)于進(jìn)程受限類型以及各自的處理原因不做深入分析,感興趣的話可以自行Google。在main函數(shù)中會(huì)發(fā)現(xiàn)很多 'DYLD_' 開頭的環(huán)境變量,我們?cè)赬code的Edit Schema的Argument中配置的環(huán)境變量會(huì)被保存到EnvironmentVariables類型的結(jié)構(gòu)體實(shí)例中。

第二步:加載共享緩存

551版對(duì)應(yīng)dyld3,與dyld不同點(diǎn)在main方法中可以看出,在老的main方法中,完成第一步以后會(huì)初始化主App,然后加載共享緩存。到了dyld3,對(duì)他們的順序做了調(diào)整:加載緩存的步驟可以劃分為 mapSharedCache和checkVersionedPaths,在dyld3中,會(huì)先執(zhí)行mapSharedCache,然后加載主App,最后checkVersionedPaths;這里的差異原因有待深入調(diào)研 。(蘋果在2017年發(fā)布的dyld3,視頻鏈接參考這里

對(duì)于共享緩存的理解:dyld加載時(shí),為了優(yōu)化程序啟動(dòng),啟用了共享緩存(shared cache)技術(shù)。共享緩存會(huì)在進(jìn)程啟動(dòng)時(shí)被dyld映射到內(nèi)存中,之后,當(dāng)任何Mach-O映像加載時(shí),dyld首先會(huì)檢查該Mach-O映像與所需的動(dòng)態(tài)庫是否在共享緩存中,如果存在,則直接將它在共享內(nèi)存中的內(nèi)存地址映射到進(jìn)程的內(nèi)存地址空間。mapSharedCache參考如下:


static void mapSharedCache()
{
    dyld3::SharedCacheOptions opts;
    opts.cacheDirOverride   = sSharedCacheOverrideDir;
    opts.forcePrivate       = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);
#if __x86_64__ && !TARGET_IPHONE_SIMULATOR
    opts.useHaswell         = sHaswell;
#else
    opts.useHaswell         = false;
#endif
    opts.verbose            = gLinkContext.verboseMapping;
    loadDyldCache(opts, &sSharedCacheLoadInfo);

    // update global state
    if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
        dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
        dyld::gProcessInfo->sharedCacheSlide                = sSharedCacheLoadInfo.slide;
        dyld::gProcessInfo->sharedCacheBaseAddress          = (unsigned long)sSharedCacheLoadInfo.loadAddress;
        sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
        dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
    }
}

這里會(huì)執(zhí)行核心方法loadDyldCache, 對(duì)該方法簡要說下:


bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results) {
/**
* 省略
**/
#if TARGET_IPHONE_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
        if ( reuseExistingCache(options, results) )
            return (results->errorMessage != nullptr);
        // slow path: this is first process to load cache
        return mapCacheSystemWide(options, results);
    }
#endif
}

在loadDyldCache會(huì)有關(guān)鍵判斷:

  1. 是否運(yùn)行在模擬器,模擬器有單獨(dú)處理

  2. 如果緩存已經(jīng)映射到了共享區(qū)域下,就把其在共享區(qū)域的地址映射到本進(jìn)程的地址空間。感興趣的可以深入研究 reuseExistingCache。如果沒有,就加載緩存,并映射。

  3. 被加載的緩存位于 /System/Library/Caches/com.apple.dyld下的若干組,每個(gè)cpu架構(gòu)代表一組。

第三步:初始化主App

系統(tǒng)會(huì)對(duì)已經(jīng)映射到進(jìn)程空間的主程序(在XNU解析MachO階段就完成了映射操作)創(chuàng)建一個(gè)imageLoader,再將其加入到master list中(sAllImages)。如果加載的MachO的硬件架構(gòu)與本設(shè)備相符,就執(zhí)行imageLoader的創(chuàng)建和添加操作。其中主要實(shí)現(xiàn)是ImageLoaderMachO::instantiateMainExecutable方法,該方法將主App的MachHeader,ASLR,文件路徑和前面提到的鏈接上下文作為參數(shù),做imageLoader的實(shí)例化操作。下面重點(diǎn)看下 instantiateMainExecutable方法。

在instantiateMainExecutable方法中

  1. 對(duì)代碼簽名、MachO加密, 動(dòng)態(tài)庫數(shù)量,段的數(shù)量相關(guān)信息的loadCommand做解析,提取出command數(shù)據(jù) ---- sniffLoadCommands方法

  2. 根據(jù)步驟1的結(jié)果,決定是否執(zhí)行Compressed模式下的instantiateMainExecutable方法還是Classic模式的instantiateMainExecutable方法。

這里主要做地址檢查,解析剩下的loadCommand,設(shè)置動(dòng)態(tài)鏈接和符號(hào)表信息


//CPU架構(gòu)是否匹配 
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
    //macho header, ASLR, 執(zhí)行路徑, 鏈接上下文
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
   //分配主程序image的內(nèi)存,更新。
        addImage(image);
        return (ImageLoaderMachO*)image;
    }

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

最后會(huì)走到上面提到的 checkVersionedPaths方法,設(shè)置加載的動(dòng)態(tài)庫版本,這里的動(dòng)態(tài)庫還沒有包括經(jīng) DYLD_INSERT_LIBRARIES插入的庫。

第四步:加載插入的動(dòng)態(tài)庫

如果 DYLD_INSERT_LIBRARIES不為空,就調(diào) loadInsertedDylib方法去加載。


static void loadInsertedDylib(const char* path)
{
    try {
        LoadContext context;
        // configure loading context
    //load
        image = load(path, context, cacheIndex);
    }
    catch (const char* msg) { /**/ }
}

/**
做路徑展開,搜索查找,排重,以及緩存查找工作。其中路徑的展開和搜索分幾個(gè)階段(phase)
*/

ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
{
    // try all path permutations and check against existing loaded images
    ImageLoader* image = loadPhase0(path, orgPath, context, cacheIndex, NULL);

    // try all path permutations and try open() until first success
    std::vector<const char*> exceptions;
    image = loadPhase0(path, orgPath, context, cacheIndex, &exceptions);
#if !TARGET_IPHONE_SIMULATOR
    // <rdar://problem/16704628> support symlinks on disk to a path in dyld shared cache
    if ( image == NULL)
        image = loadPhase2cache(path, orgPath, context, cacheIndex, &exceptions);
#endif
}
// create image by mapping in a mach-o file
ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateFromFile(const char* path, int fd, const uint8_t* fileData, size_t lenFileData,
                                                            uint64_t offsetInFat, uint64_t lenInFat, const struct stat& info, 
                                                            unsigned int segCount, unsigned int libCount, 
                                                            const struct linkedit_data_command* codeSigCmd, 
                                                            const struct encryption_info_command* encryptCmd, 
                                                            const LinkContext& context)
{
    ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart((macho_header*)fileData, path, segCount, libCount);

    try {
        // record info about file  
        image->setFileInfo(info.st_dev, info.st_ino, info.st_mtime);

        // if this image is code signed, let kernel validate signature before mapping any pages from image
        image->loadCodeSignature(codeSigCmd, fd, offsetInFat, context);
        
        // Validate that first data we read with pread actually matches with code signature
        image->validateFirstPages(codeSigCmd, fd, fileData, lenFileData, offsetInFat, context);

        // mmap segments
        image->mapSegments(fd, offsetInFat, lenInFat, info.st_size, context);

        // if framework is FairPlay encrypted, register with kernel
        image->registerEncryption(encryptCmd, context);
        
        // probe to see if code signed correctly
        image->crashIfInvalidCodeSignature();

        // finish construction
        image->instantiateFinish(context);
        
        // if path happens to be same as in LC_DYLIB_ID load command use that, otherwise malloc a copy of the path

#if __MAC_OS_X_VERSION_MIN_REQUIRED
        // <rdar://problem/6563887> app crashes when libSystem cannot be found
        else if ( (installName != NULL) && (strcmp(path, "/usr/lib/libgcc_s.1.dylib") == 0) && (strcmp(installName, "/usr/lib/libSystem.B.dylib") == 0) )
            image->setPathUnowned("/usr/lib/libSystem.B.dylib");
#endif
        else if ( (path[0] != '/') || (strstr(path, "../") != NULL) ) {
            // rdar://problem/10733082 Fix up @rpath based paths during introspection
            // rdar://problem/5135363 turn relative paths into absolute paths so gdb, Symbolication can later find them
        }
    }
    catch (...) {
        // ImageLoader::setMapped() can throw an exception to block loading of image
        // <rdar://problem/6169686> Leaked fSegmentsArray and image segments during failed dlopen_preflight
    }
    
    return image;
}

load方法不僅被 loadInsertedDylib調(diào)用,也會(huì)被 dlopen等運(yùn)行時(shí)加載動(dòng)態(tài)庫的方法使用。里面的核心方法是 loadPhase0, loadPhase1~6; 這些phase的搜索路徑對(duì)應(yīng)各個(gè)環(huán)境變量如下:

  1. DYLD_ROOT_PATH->LD_LIBRARY_PATH->DYLD_FRAMEWORK_PATH->原始路徑->DYLD_FALLBACK_LIBRARY_PATH。在loadPhase6會(huì)走 ImageLoaderMachO::instantiateFromFile方法加載實(shí)例化imageLoader;再走checkAndAddImage方法去驗(yàn)證,然后加入到master list中(sAllImages)。

  2. 如果loadPhase0返回的是空地址,則走 loadPhase2cache方法去緩存中查找,找到以后從 ImageLoaderMachO::instantiateFromCache方法去實(shí)例化,否則拋異常。

  3. ImageLoaderMachO的兩個(gè)方法 instantiateFromFile、instantiateFromCache是loader將 MachO文件解析映射到內(nèi)存的核心方法,兩個(gè)都會(huì)進(jìn)入Compressed和Classic的分叉步驟。以Compressed下的instantiateFromFile來分析,其中會(huì)有幾個(gè)我們需要留意的步驟

    1. 交給內(nèi)核去驗(yàn)證動(dòng)態(tài)庫的代碼簽名 loadCodeSignature。

    2. 映射到內(nèi)存的first page, (4k大?。┡c代碼簽名是否match。在這里會(huì)執(zhí)行沙盒,簽名認(rèn)證,對(duì)于在線上運(yùn)行時(shí)加載動(dòng)態(tài)庫的需求,可以重點(diǎn)研究這里。

    3. 根據(jù) DYLD_ENCRYPTION_INFO,讓內(nèi)核去注冊(cè)加密信息 <u style="box-sizing: border-box; font-family: __SYMBOL, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif; font-variant-ligatures: none; font-variant-numeric: tabular-nums; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registerEncryption。</u> 在該方法中,會(huì)調(diào)用內(nèi)核方法 mremap_encrypted,傳入加密數(shù)據(jù)的地址和長度等數(shù)據(jù),查看了內(nèi)核代碼,應(yīng)該是根據(jù)cryptid是否為1做了解密操作。

    4. 如果走到phase6, 會(huì)調(diào)xmap函數(shù)將動(dòng)態(tài)庫從本地mmap到用戶態(tài)內(nèi)存空間。

根據(jù)上面的分析,主程序imageLoader在全局image表的首位,后面的是插入的動(dòng)態(tài)庫的imageLoader,每個(gè)動(dòng)態(tài)庫對(duì)應(yīng)一個(gè)loader。

第五步:鏈接主程序 -- Rebase & Bind

鏈接所有動(dòng)態(tài)庫,進(jìn)行符號(hào)修正綁定工作。這里的主worker是ImageLoader的link方法。


void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
    uint64_t t0 = mach_absolute_time();
    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->recursiveUpdateDepth(context.imageCount());

    uint64_t t2 = mach_absolute_time();
    this->recursiveRebase(context);
    context.notifyBatch(dyld_image_state_rebased, false);
    
    uint64_t t3 = mach_absolute_time();
    this->recursiveBind(context, forceLazysBound, neverUnload);

    uint64_t t4 = mach_absolute_time();
    if ( !context.linkingMainExecutable )
        this->weakBind(context);
    uint64_t t5 = mach_absolute_time(); 

    context.notifyBatch(dyld_image_state_bound, false);
    uint64_t t6 = mach_absolute_time(); 

    std::vector<DOFInfo> dofs;
    this->recursiveGetDOFSections(context, dofs);
    context.registerDOFs(dofs);
    uint64_t t7 = mach_absolute_time(); 

    // interpose any dynamically loaded images
    if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
        this->recursiveApplyInterposing(context);
    }
    
    // clear error strings
    (*context.setErrorStrings)(0, NULL, NULL, NULL);

    fgTotalLoadLibrariesTime += t1 - t0;
    fgTotalRebaseTime += t3 - t2;
    fgTotalBindTime += t4 - t3;
    fgTotalWeakBindTime += t5 - t4;
    fgTotalDOF += t7 - t6;
    
    // done with initial dylib loads
    fgNextPIEDylibAddress = 0;
}

link方法主要做了以下操作:

  1. recursiveLoadLibraries遞歸加載所有依賴庫,完成之后發(fā)送一個(gè)狀態(tài)為 dyld_image_state_dependents_mapped的通知。(如果加載的動(dòng)態(tài)庫需要從硬盤讀取,IO的開銷就很大了

  2. recursiveRebase遞歸修正自己和依賴庫的基地址,因?yàn)锳SLR(上文中已經(jīng)提到過)的原因,需要根據(jù)隨機(jī)slide修正基地址。

  3. recursiveBind對(duì)于nolazy的符號(hào)進(jìn)行遞歸綁定,lazy的符號(hào)會(huì)在運(yùn)行時(shí)動(dòng)態(tài)綁定(首次被調(diào)用才去綁定)。

  4. weakBind 弱符號(hào)綁定,比如未初始化的全局變量就是弱符號(hào)。對(duì)弱符號(hào)和強(qiáng)符號(hào)感興趣可以自行Google。

  5. recursiveGetDOFSections和 registerDOFs遞歸獲取和注冊(cè)程序的DOF節(jié)區(qū),dtrace會(huì)用其動(dòng)態(tài)跟蹤。

在步驟1里,遞歸加載主App在打包階段就確定好的動(dòng)態(tài)庫的操作會(huì)使用前面提到的setContext里的鏈接上下文,調(diào)用它的loadLibrary方法;然后優(yōu)先去加載依賴的動(dòng)態(tài)庫。loadLibary的實(shí)現(xiàn)在設(shè)置鏈接上下文的時(shí)候就已經(jīng)賦值確定,即 libraryLocator,在這個(gè)方法里會(huì)用到上面提到的 load方法

在步驟3里,會(huì)有符號(hào)綁定的操作,在這里詳細(xì)分析下。


void ImageLoader::recursiveBind(const LinkContext& context, bool forceLazysBound, bool neverUnload) {
    // Normally just non-lazy pointers are bound immediately.
    // The exceptions are:
    //   1) DYLD_BIND_AT_LAUNCH will cause lazy pointers to be bound immediately
    //   2) some API's (e.g. RTLD_NOW) can cause lazy pointers to be bound immediately
    if ( fState < dyld_image_state_bound ) {
        // break cycles
        fState = dyld_image_state_bound;
        try {
            // bind lower level libraries first
            for(unsigned int i=0; i < libraryCount(); ++i) {
                ImageLoader* dependentImage = libImage(i);
                if ( dependentImage != NULL )
                    dependentImage->recursiveBind(context, forceLazysBound, neverUnload);
            }
            // bind this image
            this->doBind(context, forceLazysBound); 
            // mark if lazys are also bound
            if ( forceLazysBound || this->usablePrebinding(context) )
                fAllLazyPointersBound = true;
            // mark as never-unload if requested
            if ( neverUnload )
                this->setNeverUnload();
                
            context.notifySingle(dyld_image_state_bound, this, NULL);
        }
        catch (const char* msg) {
        //...//
        }
    }
}

recursiveBind完成遞歸綁定符號(hào)表的操作。此處的符號(hào)表針對(duì)的是非延遲加載的符號(hào)表,對(duì)于DYLD_BIND_AT_LAUNCH等特殊情況下的non-lazy符號(hào)才執(zhí)行立即綁定。該方法的核心是ImageLoaderMach的doBind,讀取image的動(dòng)態(tài)鏈接信息的bind_off與bind_size來確定需要綁定的數(shù)據(jù)偏移與大小,然后挨個(gè)對(duì)它們進(jìn)行綁定,綁定操作具體使用bindAt函數(shù);調(diào)用resolve解析完符號(hào)表后,調(diào)用bindLocation完成最終的綁定操作,需要綁定的符號(hào)信息有三種:

BIND_TYPE_POINTER:需要綁定的是一個(gè)指針。直接將計(jì)算好的新值嶼值即可。

BIND_TYPE_TEXT_ABSOLUTE32:一個(gè)32位的值。取計(jì)算的值的低32位賦值過去。

BIND_TYPE_TEXT_PCREL32:重定位符號(hào)。需要使用新值減掉需要修正的地址值來計(jì)算出重定位值。

對(duì)延遲綁定的實(shí)現(xiàn)感興趣的可以在Xcode中調(diào)試查看,或者參考這個(gè)

第六步:鏈接插入的動(dòng)態(tài)庫

這里參與鏈接的動(dòng)態(tài)庫根據(jù)第四部中加載的插入的動(dòng)態(tài)庫,從sAllImages的第二個(gè)imageLoader開始,依次取出并執(zhí)行l(wèi)ink方法和ImageLoaderMachO::registerInterposing方法來鏈接和動(dòng)態(tài)庫的數(shù)據(jù)插入操作(符號(hào)信息,__DATA段的__interpose,。。。),如果對(duì)插入動(dòng)態(tài)庫加載環(huán)節(jié)感興趣,可以深入研究這個(gè)方法。link的工作原理前面已經(jīng)描述過。

后面還有一個(gè)針對(duì)加速表下的插入操作和對(duì)主程序執(zhí)行weak bind(弱綁定)的處理,如果支持加速表 是不會(huì)去支持插入加載動(dòng)態(tài)庫的。對(duì)這部分感興趣可以深入研究以下代碼及調(diào)用環(huán)節(jié).


        // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
        for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
            image->registerInterposing();
        }
    #if SUPPORT_ACCELERATE_TABLES
        if ( (sAllCacheImagesProxy != NULL) && ImageLoader::haveInterposingTuples() ) {
            // Accelerator tables cannot be used with implicit interposing, so relaunch with accelerator tables disabled
            ImageLoader::clearInterposingTuples();
            // unmap all loaded dylibs (but not main executable)
            }
            // note: we don't need to worry about inserted images because if DYLD_INSERT_LIBRARIES was set we would not be using the accelerator table
            goto reloadAllImages;
        }
    #endif
        // apply interposing to initial set of images
        for(int i=0; i < sImageRoots.size(); ++i) {
            sImageRoots[i]->applyInterposing(gLinkContext);
        }
        gLinkContext.linkingMainExecutable = false;

第七步:初始化主程序 -- OC, C++全局變量初始化

初始化主程序觸發(fā)App的Objective-C init的方法是 initializeMainExecutable。中間還有個(gè)對(duì)主程序image執(zhí)行弱綁定的操作。


void initializeMainExecutable()
{
    // record that we've reached this step
    gLinkContext.startedInitializingMainExecutable = true;

    // run initialzers for any inserted dylibs
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    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]);
    
    // register cxa_atexit() handler to run static terminators in all loaded images when this process exits
    if ( gLibSystemHelpers != NULL ) 
        (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);

    // dump info if requested
    if ( sEnv.DYLD_PRINT_STATISTICS )
        // output
    if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
        // output
}

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
    uint64_t t1 = mach_absolute_time();
    mach_port_t thisThread = mach_thread_self();
    ImageLoader::UninitedUpwards up;
    up.count = 1;
    up.images[0] = this;
    processInitializers(context, thisThread, timingInfo, up);
    context.notifyBatch(dyld_image_state_initialized, false);
    mach_port_deallocate(mach_task_self(), thisThread);
    uint64_t t2 = mach_absolute_time();
    fgTotalInitTime += (t2 - t1);
}

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
                                     InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
    // Calling recursive init on all images in images list, building a new list of
    // uninitialized upward dependencies.
    for (uintptr_t i=0; i < images.count; ++i) {
        images.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
    }
    // If any upward dependencies remain, init them.
    if ( ups.count > 0 )
        processInitializers(context, thisThread, timingInfo, ups);
}

從 initializeMainExecutable可以得到以下幾個(gè)要點(diǎn):

  1. DYLD_PRINT_STATISTICS和 DYLD_PRINT_STATISTICS_DETAILS如果被設(shè)置,在初始化完畢以后會(huì)打印dyld啟動(dòng)App的各個(gè)重要時(shí)間節(jié)點(diǎn)信息(沒有包括全部細(xì)節(jié))

  2. 首先會(huì)對(duì)插入的動(dòng)態(tài)庫執(zhí)行 runInitializers核心方法,保證他們?cè)谥鞒绦騣mage之前完成初始化;再對(duì)主image執(zhí)行runInitializers。

  3. 每一步操作完畢會(huì)去通知觀察者,notifySingle或者notifyBatch方法,發(fā)送的通知類型參考下面的枚舉:

enum dyld_image_states
{
    dyld_image_state_mapped                 = 10,       // No batch notification for this
    dyld_image_state_dependents_mapped      = 20,       // Only batch notification for this
    dyld_image_state_rebased                = 30, 
    dyld_image_state_bound                  = 40,
    dyld_image_state_dependents_initialized = 45,       // Only single notification for this
    dyld_image_state_initialized            = 50,
    dyld_image_state_terminated             = 60        // Only single notification for this
};

runInitializer方法及后面的調(diào)用鏈流程如下,這里總結(jié)幾點(diǎn)

runInitializer流程圖
  1. 對(duì)于dumpdcrypted這一類注入方法實(shí)現(xiàn)功能的插件,他們添加的靜態(tài)方法會(huì)在 doModInitFunctions方法中被解析出來,位置在MachO文件的 __DATA段的 __mod_init_func section。C++的全局對(duì)象也會(huì)出現(xiàn)在這個(gè)section中。

  2. 在遞歸初始化 (recursiveInitialization)中,如果當(dāng)前執(zhí)行的是主程序image,doInitialization完畢后會(huì)執(zhí)行 notifySingle方法去通知觀察者。在doInitialization方法前會(huì)發(fā)送 state為 dyld_image_state_dependents_initialized的通知,由這個(gè)通知,會(huì)調(diào)用 libobjc的 load_images,最后去依次調(diào)用各個(gè)OC類的load方法以及分類的load方法。

  3. Objective-C的入口方法是 _objc_init,dyld喚起它的執(zhí)行路徑是從runInitializers -> recursiveInitialization -> doInitialization -> doModInitFunctions ->.. _objc_init。

void _objc_init(void)
{       
    // Register for unmap first, in case some +load unmaps something
    _dyld_register_func_for_remove_image(&unmap_image);
    dyld_register_image_state_change_handler(dyld_image_state_bound,
                                             1/*batch*/, &map_2_images);
    dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
  1. _objc_init會(huì)在dyld中注冊(cè)兩個(gè)通知,對(duì)應(yīng)的回調(diào)會(huì)分別執(zhí)行將OC類加載到內(nèi)存和調(diào)用load方法的操作。后面的就是OC類加載的經(jīng)典方法map_2_images了。

  2. 從 recursiveInitialization的以下代碼片段可以看出 load是在 全局實(shí)例或者方法調(diào)用前被觸發(fā)的。

            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
            // initialize this image
            bool hasInitializers = this->doInitialization(context);
            // let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_initialized, this, NULL);

第八步:配置主程序入口

        // find entry point for main executable
        result = (uintptr_t)sMainExecutable->getThreadPC();

對(duì)主程序image執(zhí)行g(shù)etThreadPC去查找程序入口,getThread去拿到 LC_MAIN加載命令的數(shù)據(jù),根據(jù)命令的entryoff(__TEXT段的main函數(shù)在MachO的偏移地址)+ MachO頭在內(nèi)存的地址的和。如果沒有LC_MAIN命令,再從LC_UNIXTHREAD命令的內(nèi)容取拿入口地址,最后返回給外界。

補(bǔ)充:dyld閉包

在第二步和第三步之間有一個(gè)查找閉包并以其結(jié)果作為程序入口返回的代碼,這里是 WWDC 2017推出的dyld3中提出的一種優(yōu)化App啟動(dòng)速度的技術(shù);大致步驟如下:

  1. 如果滿足條件:開啟閉包 ( DYLD_USE_CLOSURES 環(huán)境變量為 1),App的路徑在白名單中(目前只有系統(tǒng)App享有使用閉包的特權(quán)),共享緩存加載地址不為空,則往下執(zhí)行。

  2. 去內(nèi)存中查找閉包數(shù)據(jù),這里的方法是 findClosure。如果內(nèi)存中不存在,再去 /private/var/staged_system_apps路徑下去查找硬盤數(shù)據(jù),找到就返回結(jié)果。

  3. 如果沒有閉包數(shù)據(jù),就會(huì)調(diào)用socket通信走RPC去獲取閉包數(shù)據(jù),執(zhí)行方法為 callClosureDaemon,感興趣可以研究下

  4. 如果閉包數(shù)據(jù)不為空,就會(huì)走核心方法:launchWithClosure,基于閉包去啟動(dòng)App,并且返回該方法中獲取的程序入口地址給外界。這個(gè)方法重復(fù)了上面的各個(gè)步驟。具體實(shí)現(xiàn)和內(nèi)部的數(shù)據(jù)結(jié)構(gòu)有待分析。

小結(jié)

官方的 dyld項(xiàng)目中除了 dyld和 dyld3的源碼外,還有一些工具可以供我們使用,下面列出一些可以利用的工具

  1. update_dyld_shared_cache_tool:在MacOS中有它的可執(zhí)行程序,這里在項(xiàng)目中無法直接編譯它。其作用是強(qiáng)制更新系統(tǒng)的共享緩存里動(dòng)態(tài)庫的版本。否則在系統(tǒng)更新和重裝系統(tǒng)的時(shí)候,才會(huì)去更新。

  2. dyld_closure_util:一個(gè)制作dyld閉包的工具;閉包在 WWDC2017的 dyld3介紹中被提出,用來緩存app在啟動(dòng)階段,依賴的動(dòng)態(tài)庫路徑、符號(hào)查找結(jié)果,代碼簽名等根據(jù)解析MachO頭部獲取數(shù)據(jù),便于app的啟動(dòng)提速。該工具也需要修改才能編譯。

  3. dsc_extractor:用來從動(dòng)態(tài)庫共享緩存中提取出所有系統(tǒng)庫,這個(gè)工程可以直接編譯成可執(zhí)行文件,用于后續(xù)的調(diào)研系統(tǒng)庫實(shí)現(xiàn)的工作。

每個(gè)MachO都會(huì)由一個(gè)imageLoader來處理加載和依賴管理的操作,這里是由dyld來安排。主程序app的image的加載是由內(nèi)核來完成的。其他的動(dòng)態(tài)庫的加載細(xì)節(jié)可以參考上面提到的link方法實(shí)現(xiàn),當(dāng)一個(gè)image加載完畢,dyld會(huì)發(fā)送 dyld_image_state_bound通知;著名的hook工具 fishhook的實(shí)現(xiàn)原理也是借助監(jiān)聽這個(gè)通知,在回調(diào)里完成hook操作的。

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

相關(guān)閱讀更多精彩內(nèi)容

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