iOS性能優(yōu)化(3)-啟動(dòng)優(yōu)化2

一.dyld簡(jiǎn)介

dyld(the dynamic link editor)是蘋果的動(dòng)態(tài)鏈接器秸仙,是蘋果操作系統(tǒng)一個(gè)重要組成部分,在系統(tǒng)內(nèi)核做好程序準(zhǔn)備工作之后桩盲,交由dyld負(fù)責(zé)余下的工作寂纪。而且它是開源的,任何人可以通過蘋果官網(wǎng)下載它的源碼來閱讀理解它的運(yùn)作方式,了解系統(tǒng)加載動(dòng)態(tài)庫(kù)的細(xì)節(jié)捞蛋。

dyld下載地址:http://opensource.apple.com/tarballs/dyld孝冒。筆者下載的是519.2.2版本。

二. 共享緩存機(jī)制

在iOS系統(tǒng)中拟杉,每個(gè)程序依賴的動(dòng)態(tài)庫(kù)都需要通過dyld(位于/usr/lib/dyld)一個(gè)一個(gè)加載到內(nèi)存庄涡,然而,很多系統(tǒng)庫(kù)幾乎是每個(gè)程序都會(huì)用到的搬设,如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次穴店,勢(shì)必造成運(yùn)行緩慢,為了優(yōu)化啟動(dòng)速度和提高程序性能焕梅,共享緩存機(jī)制就應(yīng)運(yùn)而生迹鹅。所有默認(rèn)的動(dòng)態(tài)鏈接庫(kù)被合并成一個(gè)大的緩存文件卦洽,放到/System/Library/Caches/com.apple.dyld/目錄下贞言,按不同的架構(gòu)保存分別保存著,筆者的iPhone6里面就有dyld_shared_cache_armv7s和dyld_shared_cache_armv64兩個(gè)文件阀蒂,如下圖所示该窗。

dyld_shared_cache文件

想要分析某個(gè)系統(tǒng)庫(kù),就需要從dyld_shared_cache里先將的原始二進(jìn)制文件提取出來蚤霞,這里從易到難提供3種方法:

1. dyld_cache_extract提取

dyld_cache_extract(https://github.com/macmade/dyld_cache_extract)是一個(gè)可視化的工具酗失,使用極其簡(jiǎn)單,把dyld_shared_cache載入即可解析出來昧绣,如下圖所示规肴。

dyld_cache_extract解析
2. jtool提取

以提取CFNetwork為例,使用如下命令即可:

$ jtool -extract CFNetwork  ./dyld_shared_cache_arm64
Extracting  /System/Library/Frameworks/CFNetwork.framework/CFNetwork at 0x147a000  into dyld_shared_cache_arm64.CFNetwork
3. dsc_extractor提取

在dyld源代碼的launch-cache文件夾里面找到dsc_extractor.cpp夜畴,將653行的“#if 0”修改為“#if 1”,然后用如下命令編譯生成dsc_extractor拖刃,并使用它提取所有緩存文件:

$ clang++ dsc_extractor.cpp dsc_iterator.cpp -o ddc_extractor
$ ./dsc_extractor ./dyld_shared_cache_arm64 ./

三. dyld加載過程

一個(gè)iOS程序的main()函數(shù)位于main.m中,這是我們熟知的程序入口贪绘。但很少有人去關(guān)心main()函數(shù)之前到底發(fā)生了什么兑牡。本章就帶著這個(gè)疑問,從main()函數(shù)入手税灌,探索一下dyld的加載過程均函。
先用Xcode新建一個(gè)Single View App工程,并在main()函數(shù)下斷菱涤,然后運(yùn)行苞也,調(diào)用棧如下圖所示。

main()函數(shù)調(diào)用棧


main()函數(shù)之前僅有一個(gè)libdyld.dylib`start入口粘秆,這顯然不是我們想要的如迟,根據(jù)這個(gè)線索順藤摸瓜,在dyld源代碼dyldStartup.s中找到了__dyld_start函數(shù)翻擒,此函數(shù)由匯編實(shí)現(xiàn)氓涣,兼容各種平臺(tái)架構(gòu)牛哺,此處僅摘錄arm64架構(gòu)下的匯編代碼片段:

#if __arm64__
.data
.align 3
__dso_static:
.quad   ___dso_handle
.text
.align 2
.globl __dyld_start
__dyld_start:
mov     x28, sp
and sp, x28,  #~15       // force 16-byte alignment of stack
mov x0,  #0
mov x1,  #0
stp x1, x0,  [sp,  #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp,  #16             // make room for local variables
ldr     x0,  [x28]  // get app's mh into x0
ldr     x1,  [x28,  #8]           // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add     x2, x28,  #16        // get argv into x2
adrp    x4,___dso_handle@page
add     x4,x4,___dso_handle@pageoff // get dyld's mh in to x4
adrp    x3,__dso_static@page
ldr     x3,[x3,__dso_static@pageoff]  // get unslid start of dyld
sub x3,x4,x3 // x3 now has slide of dyld
mov x5,sp // x5 has &startGlue
// call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
bl  __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
mov x16,x0 // save entry point address in x16
ldr     x1,  [sp]
cmp x1,  #0
b.ne Lnew
// LC_UNIXTHREAD way, clean up stack and jump to result
add sp, x28,  #8     // restore unaligned stack pointer without app mh
br  x16 // jump to the program's entry point
// LC_MAIN case, set up stack for call to main()
Lnew: mov lr, x1 // simulate return address into _start in libdyld.dylib
ldr     x0,  [x28,  #8]       // main param1 = argc
add     x1, x28,  #16        // main param2 = argv
add x2, x1, x0, lsl #3
add x2, x2,  #8      // main param3 = &env[0]
mov x3, x2
Lapple: ldr x4,  [x3]
add x3, x3,  #8
cmp x4,  #0
b.ne Lapple  // main param4 = apple
br  x16
#endif  // __arm64__

源碼中可以看到一條bl命令,根據(jù)注釋可以知道是跳轉(zhuǎn)到dyldbootstrap::start()函數(shù):

// call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
bl  __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm

dyldbootstrap::start()函數(shù)中做了很多dyld初始化相關(guān)的工作劳吠,包括:

  • rebaseDyld() dyld重定位引润。
  • mach_init() mach消息初始化。
  • __guard_setup() 棧溢出保護(hù)痒玩。

初始化工作完成后淳附,此函數(shù)調(diào)用到了dyld::_main(),再將返回值傳遞給__dyld_start去調(diào)用真正的main()函數(shù)蠢古。在dyldInitialization.cpp文件中可以找到dyldbootstrap::start()函數(shù)的實(shí)現(xiàn)如下:

//
//  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  )  {
        // dyld重定位
        rebaseDyld(dyldsMachHeader, slide);
    }
    // allow dyld to use mach messaging
    // mach消息初始化
    mach_init();
    // 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
    // 棧溢出保護(hù)
    __guard_setup(apple);
    
    // now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
    // 進(jìn)入dyld::_main()函數(shù)
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyld::_main()是整個(gè)App啟動(dòng)的關(guān)鍵函數(shù)奴曙,此函數(shù)里面做了很多事情,代碼如下:

//
// 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)
{
    // Grab the cdHash of the main executable from the environment
    // 第一步草讶,設(shè)置運(yùn)行環(huán)境
    uint8_t mainExecutableCDHashBuffer[20];
    const  uint8_t* mainExecutableCDHash =  nullptr;
    if  ( hexToBytes(_simple_getenv(apple,  "executable_cdhash"),  40, mainExecutableCDHashBuffer)  )
        // 獲取主程序的hash
        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;
    // 獲取主程序的macho_header結(jié)構(gòu)
    sMainExecutableMachHeader = mainExecutableMH;
    // 獲取主程序的slide值
    sMainExecutableSlide = mainExecutableSlide;
    
    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];
    
    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
    // 獲取進(jìn)程名稱
    sExecShortName =  ::strrchr(sExecPath,  '/');
    if  ( sExecShortName != NULL )
        ++sExecShortName;
    else
        sExecShortName = sExecPath;
    
    // 配置進(jìn)程受限模式
    configureProcessRestrictions(mainExecutableMH);
    
    // 檢測(cè)環(huán)境變量
    checkEnvironmentVariables(envp);
    defaultUninitializedFallbackPaths(envp);
    
    // 如果設(shè)置了DYLD_PRINT_OPTS則調(diào)用printOptions()打印參數(shù)
    if  ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    // 如果設(shè)置了DYLD_PRINT_ENV則調(diào)用printEnvironmentVariables()打印環(huán)境變量
    if  ( sEnv.DYLD_PRINT_ENV )
        printEnvironmentVariables(envp);
    // 獲取當(dāng)前程序架構(gòu)
    getHostInfo(mainExecutableMH, mainExecutableSlide);
    //-------------第一步結(jié)束-------------
    
    // load shared cache
    // 第二步洽糟,加載共享緩存
    // 檢查共享緩存是否開啟,iOS必須開啟
    checkSharedRegionDisable((mach_header*)mainExecutableMH);
    if  ( gLinkContext.sharedRegionMode !=  ImageLoader::kDontUseSharedRegion )  {
        mapSharedCache();
    }
    ...
    
    try  {
        // add dyld itself to UUID list
        addDyldImageToUUIDList();
        
        // instantiate ImageLoader for main executable
        // 第三步 實(shí)例化主程序
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
        
        // Now that shared cache is loaded, setup an versioned dylib overrides
#if SUPPORT_VERSIONED_PATHS
        checkVersionedPaths();
#endif
        
        // dyld_all_image_infos image list does not contain dyld
        // add it as dyldPath field in dyld_all_image_infos
        // for simulator, dyld_sim is in image list, need host dyld added
#if TARGET_IPHONE_SIMULATOR
        // get path of host dyld from table of syscall vectors in host dyld
        void* addressInDyld = gSyscallHelpers;
#else
        // get path of dyld itself
        void* addressInDyld =  (void*)&__dso_handle;
#endif
        char dyldPathBuffer[MAXPATHLEN+1];
        int len = proc_regionfilename(getpid(),  (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
        if  ( len >  0  )  {
            dyldPathBuffer[len]  =  '\0';  // proc_regionfilename() does not zero terminate returned string
            if  ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath)  !=  0  )
                gProcessInfo->dyldPath = strdup(dyldPathBuffer);
        }
        
        // load any inserted libraries
        // 第四步 加載插入的動(dòng)態(tài)庫(kù)
        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.
        // 記錄插入的動(dòng)態(tài)庫(kù)數(shù)量
        sInsertedDylibCount = sAllImages.size()-1;
        
        // link main executable
        // 第五步 鏈接主程序
        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;
        }
        
        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)庫(kù)
        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();
            }
        }
        
        // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
        for  (long i=sInsertedDylibCount+1; i < sAllImages.size();  ++i)  {
            ImageLoader* image = sAllImages[I];
            if  ( image->inSharedCache()  )
                continue;
            image->registerInterposing();
        }
        ...
        
        // apply interposing to initial set of images
        for(int i=0; i < sImageRoots.size();  ++i)  {
            sImageRoots[i]->applyInterposing(gLinkContext);
        }
        gLinkContext.linkingMainExecutable =  false;
        
        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        // 第七步 執(zhí)行弱符號(hào)綁定
        sMainExecutable->weakBind(gLinkContext);
        
        // If cache has branch island dylibs, tell debugger about them
        if  (  (sSharedCacheLoadInfo.loadAddress != NULL)  &&  (sSharedCacheLoadInfo.loadAddress->header.mappingOffset >=  0x78)  &&  (sSharedCacheLoadInfo.loadAddress->header.branchPoolsOffset !=  0)  )  {
            uint32_t count = sSharedCacheLoadInfo.loadAddress->header.branchPoolsCount;
            dyld_image_info info[count];
            const  uint64_t* poolAddress =  (uint64_t*)((char*)sSharedCacheLoadInfo.loadAddress + sSharedCacheLoadInfo.loadAddress->header.branchPoolsOffset);
            // <rdar://problem/20799203> empty branch pools can be in development cache
            if  (  ((mach_header*)poolAddress)->magic == sMainExecutableMachHeader->magic )  {
                for  (int poolIndex=0; poolIndex < count;  ++poolIndex)  {
                    uint64_t poolAddr = poolAddress[poolIndex]  + sSharedCacheLoadInfo.slide;
                    info[poolIndex].imageLoadAddress =  (mach_header*)(long)poolAddr;
                    info[poolIndex].imageFilePath =  "dyld_shared_cache_branch_islands";
                    info[poolIndex].imageFileModDate =  0;
                }
                // add to all_images list
                addImagesToAllImages(count, info);
                // tell gdb about new branch island images
                gProcessInfo->notification(dyld_image_adding, count, info);
            }
        }
        
        CRSetCrashLogMessage("dyld: launch, running initializers");
        ...
        // run all initializers
        // 第八步 執(zhí)行初始化方法
        initializeMainExecutable();
        
        // notify any montoring proccesses that this process is about to enter main()
        dyld3::kdebug_trace_dyld_signpost(DBG_DYLD_SIGNPOST_START_MAIN_DYLD2,  0,  0);
        notifyMonitoringDyldMain();
        
        // find entry point for main executable
        // 第九步 查找入口點(diǎn)并返回
        result =  (uintptr_t)sMainExecutable->getThreadPC();
        if  ( result !=  0  )  {
            // main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
            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->getMain();
            *startGlue =  0;
        }
    }
    catch(const  char* message)  {
        syncAllImages();
        halt(message);
    }
    catch(...)  {
        dyld::log("dyld: launch failed\n");
    }
    ...
    
    return result;
}

筆者對(duì)上面比較重要的地方加了一些注釋堕战,方便讀者查看坤溃。整個(gè)加載過程可細(xì)分為九步:

  • 第一步:設(shè)置運(yùn)行環(huán)境。
  • 第二步:加載共享緩存嘱丢。
  • 第三步:實(shí)例化主程序薪介。
  • 第四步:加載插入的動(dòng)態(tài)庫(kù)。
  • 第五步:鏈接主程序越驻。
  • 第六步:鏈接插入的動(dòng)態(tài)庫(kù)汁政。
  • 第七步:執(zhí)行弱符號(hào)綁定
  • 第八步:執(zhí)行初始化方法。
  • 第九步:查找入口點(diǎn)并返回缀旁。
1.設(shè)置運(yùn)行環(huán)境

這一步主要是設(shè)置運(yùn)行參數(shù)记劈、環(huán)境變量等。代碼在開始的時(shí)候诵棵,將入?yún)ainExecutableMH賦值給了sMainExecutableMachHeader抠蚣,這是一個(gè)macho_header結(jié)構(gòu)體,表示的是當(dāng)前主程序的Mach-O頭部信息履澳,加載器依據(jù)Mach-O頭部信息就可以解析整個(gè)Mach-O文件信息嘶窄。接著調(diào)用setContext()設(shè)置上下文信息,包括一些回調(diào)函數(shù)距贷、參數(shù)柄冲、標(biāo)志信息等。設(shè)置的回調(diào)函數(shù)都是dyld模塊自身實(shí)現(xiàn)的忠蝗,如loadLibrary()函數(shù)實(shí)際調(diào)用的是libraryLocator()现横,負(fù)責(zé)加載動(dòng)態(tài)庫(kù)。代碼片斷如下:

static  void setContext(const macho_header* mainExecutableMH,  int argc, const  char* argv[],  const  char* envp[],  const  char* apple[])
{
    gLinkContext.loadLibrary =  &libraryLocator;
    gLinkContext.terminationRecorder =  &terminationRecorder;
    ...
}

configureProcessRestrictions()用來配置進(jìn)程是否受限,代碼邏輯比較簡(jiǎn)單戒祠,sEnvMode默認(rèn)等于envNone(即受限模式)骇两,如果設(shè)置了get_task_allow權(quán)限或者是內(nèi)核開發(fā)時(shí)會(huì)設(shè)置成envAll,如果設(shè)置了uid和gid則立即變成受限模式姜盈,代碼片段如下:

static  void configureProcessRestrictions(const macho_header* mainExecutableMH)
{
    sEnvMode = envNone;  // 受限模式
    gLinkContext.requireCodeSignature =  true;  // 需要代碼簽名
    uint32_t flags;
    if  ( csops(0, CS_OPS_STATUS,  &flags,  sizeof(flags))  !=  -1  )  {
        // 啟用代碼簽名
        if  ( flags & CS_ENFORCEMENT )  {
            // get_task_allow
            if  ( flags & CS_GET_TASK_ALLOW )  {
                // Xcode built app for Debug allowed to use DYLD_* variables
                // Xcode調(diào)試時(shí)允許使用DYLD_*環(huán)境變量
                sEnvMode = envAll;  // 非受限模式
            }
            else  {
                // Development kernel can use DYLD_PRINT_* variables on any FairPlay encrypted app
                uint32_t secureValue =  0;
                size_t secureValueSize =  sizeof(secureValue);
                if  (  (sysctlbyname("kern.secure_kernel",  &secureValue,  &secureValueSize, NULL,  0)  ==  0)  &&  (secureValue ==  0)  && isFairPlayEncrypted(mainExecutableMH)  )  {
                    sEnvMode = envPrintOnly;
                }
            }
        }
        else  {
            // Development kernel can run unsigned code
            // 內(nèi)核開發(fā)運(yùn)行運(yùn)行非簽名代碼
            sEnvMode = envAll;  // 非受限模式
            gLinkContext.requireCodeSignature =  false;  // 無需代碼簽名
        }
    }
    // 如果設(shè)置了uid低千、gid則變成受限模式
    if  ( issetugid()  )  {
        sEnvMode = envNone;
    }
}

checkEnvironmentVariables()檢測(cè)環(huán)境變量,如果sEnvMode為envNone就直接返回馏颂,否則調(diào)用processDyldEnvironmentVariable()處理并設(shè)置環(huán)境變量示血,代碼如下:

static  void checkEnvironmentVariables(const  char* envp[])
{
    if  ( sEnvMode == envNone )
        return;
    const  char** p;
    for(p = envp;  *p != NULL; p++)  {
        const  char* keyEqualsValue =  *p;
        if  ( strncmp(keyEqualsValue,  "DYLD_",  5)  ==  0  )  {
            const  char* equals = strchr(keyEqualsValue,  '=');
            if  ( equals != NULL )  {
                strlcat(sLoadingCrashMessage,  "\n",  sizeof(sLoadingCrashMessage));
                strlcat(sLoadingCrashMessage, keyEqualsValue,  sizeof(sLoadingCrashMessage));
                const  char* value =  &equals[1];
                const  size_t keyLen = equals-keyEqualsValue;
                char key[keyLen+1];
                strncpy(key, keyEqualsValue, keyLen);
                key[keyLen]  =  '\0';
                if  (  (sEnvMode == envPrintOnly)  &&  (strncmp(key,  "DYLD_PRINT_",  11)  !=  0)  )
                    continue;
                // 處理并設(shè)置環(huán)境變量
                processDyldEnvironmentVariable(key, value, NULL);
            }
        }
        else  if  ( strncmp(keyEqualsValue,  "LD_LIBRARY_PATH=",  16)  ==  0  )  {
            const  char* path =  &keyEqualsValue[16];
            sEnv.LD_LIBRARY_PATH = parseColonList(path, NULL);
        }
    }
    ...
}

最后是調(diào)用getHostInfo()獲取當(dāng)前程序架構(gòu),至此救拉,第一步的準(zhǔn)備工作就完成了难审。
細(xì)心的讀者可能會(huì)注意到,整個(gè)過程中有一些DYLD_*開頭的環(huán)境變量亿絮,比如:

// 如果設(shè)置了DYLD_PRINT_OPTS則調(diào)用printOptions()打印參數(shù)
if  ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
// 如果設(shè)置了DYLD_PRINT_ENV則調(diào)用printEnvironmentVariables()打印環(huán)境變量
if  ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);

其實(shí)告喊,只要在Xcode中配置一下即可讓這些環(huán)境變量生效,我們隨便打開個(gè)工程然后依次點(diǎn)擊“Product”->“Scheme”->“Edit Scheme…”壹无,如下圖所示葱绒。

Edit Scheme

在接下來的頁(yè)面中,點(diǎn)擊“Arguments”選項(xiàng)卡斗锭,按下圖所示添加環(huán)境變量并設(shè)置“Value”為1。

Xcode設(shè)置環(huán)境變量

運(yùn)行Xcode即可看到控制臺(tái)打印的詳細(xì)信息:

opt[0]  =  "/var/mobile/Containers/Bundle/Application/B64DB1D3-AD29-4F47-9C77-97B40079E276/Launch.app/Launch"
TMPDIR=/private/var/mobile/Containers/Data/Application/F5D83A13-1C1B-48B7-9764-356C1E1A6764/tmp
__CF_USER_TEXT_ENCODING=0x1F5:0:0
SHELL=/bin/sh
SQLITE_ENABLE_THREAD_ASSERTIONS=1
OS_ACTIVITY_DT_MODE=YES
HOME=/private/var/mobile/Containers/Data/Application/F5D83A13-1C1B-48B7-9764-356C1E1A6764
CFFIXED_USER_HOME=/private/var/mobile/Containers/Data/Application/F5D83A13-1C1B-48B7-9764-356C1E1A6764
FBSClientLogging=0
NSUnbufferedIO=YES
PATH=/usr/bin:/bin:/usr/sbin:/sbin
LOGNAME=mobile
XPC_SERVICE_NAME=UIKitApplication:com.chinapyg.Launch1[0x6e2d]
DYLD_INSERT_LIBRARIES=/Library/MobileSubstrate/MobileSubstrate.dylib:/Developer/usr/lib/libBacktraceRecording.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
CLASSIC=0
CFLOG_FORCE_STDERR=YES
DYLD_PRINT_OPTS=1
DYLD_PRINT_ENV=1
USER=mobile
_MSSafeMode=0
XPC_FLAGS=0x1
DYLD_LIBRARY_PATH=/usr/lib/system/introspection

后面還有很多這樣的DYLD_*開頭的環(huán)境變量失球,感興趣的讀者可以自行測(cè)試岖是,筆者不再逐一演示。

2.加載共享緩存

這一步先調(diào)用checkSharedRegionDisable()檢查共享緩存是否禁用实苞。該函數(shù)的iOS實(shí)現(xiàn)部分僅有一句注釋豺撑,從注釋我們可以推斷iOS必須開啟共享緩存才能正常工作,代碼如下:

static  void checkSharedRegionDisable(const mach_header* mainExecutableMH)
{
// iOS cannot run without shared region
}

接下來調(diào)用mapSharedCache()加載共享緩存黔牵,而mapSharedCache()里面實(shí)則是調(diào)用了loadDyldCache()聪轿,從代碼可以看出,共享緩存加載又分為三種情況:

  • 僅加載到當(dāng)前進(jìn)程猾浦,調(diào)用mapCachePrivate()陆错。
  • 共享緩存已加載,不做任何處理金赦。
  • 當(dāng)前進(jìn)程首次加載共享緩存音瓷,調(diào)用mapCacheSystemWide()。

loadDyldCache()的實(shí)現(xiàn)代碼如下:

bool loadDyldCache(const  SharedCacheOptions& options,  SharedCacheLoadInfo* results)
{
    results->loadAddress =  0;
    results->slide =  0;
    results->cachedDylibsGroup =  nullptr;
    results->errorMessage =  nullptr;
    
    if  ( options.forcePrivate )  {
        // mmap cache into this process only
        // 僅加載到當(dāng)前進(jìn)程
        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
        // 當(dāng)前進(jìn)程首次加載共享緩存
        return mapCacheSystemWide(options, results);
    }
}

mapCachePrivate()绳慎、mapCacheSystemWide()里面就是具體的共享緩存解析邏輯,感興趣的讀者可以詳細(xì)分析。

3. 實(shí)例化主程序

這一步將主程序的Mach-O加載進(jìn)內(nèi)存杏愤,并實(shí)例化一個(gè)ImageLoader靡砌。instantiateFromLoadedImage()首先調(diào)用isCompatibleMachO()檢測(cè)Mach-O頭部的magic、cputype珊楼、cpusubtype等相關(guān)屬性乏奥,判斷Mach-O文件的兼容性,如果兼容性滿足亥曹,則調(diào)用ImageLoaderMachO::instantiateMainExecutable()實(shí)例化主程序的ImageLoader邓了,代碼如下:

static  ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh,  uintptr_t slide,  const  char* path)
{
    // try mach-o loader
    // 嘗試加載MachO
    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";
}

ImageLoaderMachO::instantiateMainExecutable()函數(shù)里面首先會(huì)調(diào)用sniffLoadCommands()函數(shù)來獲取一些數(shù)據(jù),包括:

  • compressed:若Mach-O存在LC_DYLD_INFO和LC_DYLD_INFO_ONLY加載命令媳瞪,則說明是壓縮類型的Mach-O骗炉,代碼片段如下:
switch  (cmd->cmd)  {
    case LC_DYLD_INFO:
    case LC_DYLD_INFO_ONLY:
        if  ( cmd->cmdsize !=  sizeof(dyld_info_command)  )
            throw  "malformed mach-o image: LC_DYLD_INFO size wrong";
        dyldInfoCmd =  (struct dyld_info_command*)cmd;`
        // 存在LC_DYLD_INFO或者LC_DYLD_INFO_ONLY則表示是壓縮類型的Mach-O
        *compressed =  true;
        break;
        ...
}
  • segCount:根據(jù) LC_SEGMENT_COMMAND 加載命令來統(tǒng)計(jì)段數(shù)量,這里拋出的錯(cuò)誤日志也說明了段的數(shù)量是不能超過255個(gè)蛇受,代碼片段如下:
case LC_SEGMENT_COMMAND:
segCmd =  (struct macho_segment_command*)cmd;
...
if  ( segCmd->vmsize !=  0  )
*segCount +=  1;
if  (  *segCount >  255  )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
  • libCount:根據(jù) LC_LOAD_DYLIB句葵、LC_LOAD_WEAK_DYLIB、LC_REEXPORT_DYLIB兢仰、LC_LOAD_UPWARD_DYLIB 這幾個(gè)加載命令來統(tǒng)計(jì)庫(kù)的數(shù)量乍丈,庫(kù)的數(shù)量不能超過4095個(gè)。代碼片段如下:
case LC_LOAD_DYLIB:
case LC_LOAD_WEAK_DYLIB:
case LC_REEXPORT_DYLIB:
case LC_LOAD_UPWARD_DYLIB:
*libCount +=  1;
if  (  *libCount >  4095  )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
  • codeSigCmd:通過解析LC_CODE_SIGNATURE來獲取代碼簽名加載命令把将,代碼片段如下:
case LC_CODE_SIGNATURE:
*codeSigCmd =  (struct linkedit_data_command*)cmd;`
break;
  • encryptCmd:通過LC_ENCRYPTION_INFO和LC_ENCRYPTION_INFO_64來獲取段的加密信息轻专,代碼片段如下:
case LC_ENCRYPTION_INFO:
...
*encryptCmd =  (encryption_info_command*)cmd;
break;
case LC_ENCRYPTION_INFO_64:
...
*encryptCmd =  (encryption_info_command*)cmd;
break;

ImageLoader是抽象類,其子類負(fù)責(zé)把Mach-O文件實(shí)例化為image察蹲,當(dāng)sniffLoadCommands()解析完以后请垛,根據(jù)compressed的值來決定調(diào)用哪個(gè)子類進(jìn)行實(shí)例化,代碼如下:

ImageLoader*  ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh,  uintptr_t slide,  const  char* path,  const  LinkContext& context)
{
    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
}

此過程可以用下圖來進(jìn)行直觀描述洽议。

ImageLoaderMachO::instantiateMainExecutable()函數(shù)流程

下面以ImageLoaderMachOCompressed::instantiateMainExecutable()為例來看一下實(shí)現(xiàn):

// create image for main executable
ImageLoaderMachOCompressed*  ImageLoaderMachOCompressed::instantiateMainExecutable(const macho_header* mh,  uintptr_t slide,  const  char* path,
                                                                                   unsigned  int segCount,  unsigned  int libCount,  const  LinkContext& context)
{
    ImageLoaderMachOCompressed* image =  ImageLoaderMachOCompressed::instantiateStart(mh, path, segCount, libCount);
    
    // set slide for PIE programs
    image->setSlide(slide);
    
    // for PIE record end of program, to know where to start loading dylibs
    if  ( slide !=  0  )
        fgNextPIEDylibAddress =  (uintptr_t)image->getEnd();
    
    image->disableCoverageCheck();
    image->instantiateFinish(context);
    image->setMapped(context);
    
    if  ( context.verboseMapping )  {
        dyld::log("dyld: Main executable mapped %s\n", path);
        for(unsigned  int i=0, e=image->segmentCount(); i < e;  ++i)  {
            const  char* name = image->segName(i);
            if  (  (strcmp(name,  "__PAGEZERO")  ==  0)  ||  (strcmp(name,  "__UNIXSTACK")  ==  0)  )
                dyld::log("%18s at 0x%08lX->0x%08lX\n", name, image->segPreferredLoadAddress(i), image->segPreferredLoadAddress(i)+image->segSize(i));
            else
                dyld::log("%18s at 0x%08lX->0x%08lX\n", name, image->segActualLoadAddress(i), image->segActualEndAddress(i));
        }
    }
    return image;
}

這里總結(jié)為4步:

  • ImageLoaderMachOCompressed::instantiateStart()創(chuàng)建ImageLoaderMachOCompressed對(duì)象宗收。
  • image->disableCoverageCheck()禁用段覆蓋檢測(cè)。
  • image->instantiateFinish()首先調(diào)用parseLoadCmds()解析加載命令亚兄,然后調(diào)用this->setDyldInfo()設(shè)置動(dòng)態(tài)庫(kù)鏈接信息混稽,最后調(diào)用this->setSymbolTableInfo() 設(shè)置符號(hào)表相關(guān)信息,代碼片段如下:
  • image->setMapped()函數(shù)注冊(cè)通知回調(diào)审胚、計(jì)算執(zhí)行時(shí)間等等匈勋。

在調(diào)用完ImageLoaderMachO::instantiateMainExecutable()后繼續(xù)調(diào)用addImage(),將image加入到sAllImages全局鏡像列表菲盾,并將image映射到申請(qǐng)的內(nèi)存中颓影。代碼如下:

static  void addImage(ImageLoader* image)
{
    // add to master list
    allImagesLock();
    sAllImages.push_back(image);
    allImagesUnlock();
    
    // update mapped ranges
    uintptr_t lastSegStart =  0;
    uintptr_t lastSegEnd =  0;
    for(unsigned  int i=0, e=image->segmentCount(); i < e;  ++i)  {
        if  ( image->segUnaccessible(i)  )
            continue;
        uintptr_t start = image->segActualLoadAddress(i);
        uintptr_t  end  = image->segActualEndAddress(i);
        if  ( start == lastSegEnd )  {
            // two segments are contiguous, just record combined segments
            lastSegEnd =  end;
        }
        else  {
            // non-contiguous segments, record last (if any)
            if  ( lastSegEnd !=  0  )
                addMappedRange(image, lastSegStart, lastSegEnd);
            lastSegStart = start;
            lastSegEnd =  end;
        }
    }
    if  ( lastSegEnd !=  0  )
        addMappedRange(image, lastSegStart, lastSegEnd);
    
    if  ( gLinkContext.verboseLoading ||  (sEnv.DYLD_PRINT_LIBRARIES_POST_LAUNCH &&  (sMainExecutable!=NULL)  && sMainExecutable->isLinked())  )  {
        dyld::log("dyld: loaded: %s\n", image->getPath());
    }
}

至此,初始化主程序這一步就完成了懒鉴。ImageLoaderMachOClassic::instantiateMainExecutable()函數(shù)的實(shí)現(xiàn)诡挂,同理可推碎浇,此處不再詳述。

4. 加載插入的動(dòng)態(tài)庫(kù)

這一步是加載環(huán)境變量DYLD_INSERT_LIBRARIES中配置的動(dòng)態(tài)庫(kù)璃俗,先判斷環(huán)境變量DYLD_INSERT_LIBRARIES中是否存在要加載的動(dòng)態(tài)庫(kù)奴璃,如果存在則調(diào)用loadInsertedDylib()依次加載,代碼如下:

if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL )  {
    for  (const  char*  const* lib = sEnv.DYLD_INSERT_LIBRARIES;  *lib != NULL;  ++lib)
        loadInsertedDylib(*lib);
}

loadInsertedDylib()內(nèi)部設(shè)置了一個(gè)LoadContext參數(shù)后城豁,調(diào)用了load()函數(shù)苟穆,
load()函數(shù)的實(shí)現(xiàn)為一系列的loadPhase*()函數(shù),loadPhase0()~loadPhase1()函數(shù)會(huì)按照下圖所示順序搜索動(dòng)態(tài)庫(kù)唱星,并調(diào)用不同的函數(shù)來繼續(xù)處理雳旅。

搜索順序

當(dāng)內(nèi)部調(diào)用到loadPhase5load()函數(shù)的時(shí)候,會(huì)先在共享緩存中搜尋间聊,如果存在則使用ImageLoaderMachO::instantiateFromCache()來實(shí)例化ImageLoader攒盈,否則通過loadPhase5open()打開文件并讀取數(shù)據(jù)到內(nèi)存后,再調(diào)用loadPhase6()哎榴,通過ImageLoaderMachO::instantiateFromFile()實(shí)例化ImageLoader型豁,最后調(diào)用checkandAddImage()驗(yàn)證鏡像并將其加入到全局鏡像列表中。
load()函數(shù)代碼如下:

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);
    if  ( image != NULL )  {
        CRSetCrashLogMessage2(NULL);
        return image;
    }
    
    // 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
    ...
}

5. 鏈接主程序

這一步調(diào)用link()函數(shù)將實(shí)例化后的主程序進(jìn)行動(dòng)態(tài)修正尚蝌,讓二進(jìn)制變?yōu)榭烧?zhí)行的狀態(tài)迎变。link()函數(shù)內(nèi)部調(diào)用了ImageLoader::link()函數(shù),從源代碼可以看到飘言,這一步主要做了以下幾個(gè)事情:

  • recursiveLoadLibraries() 根據(jù)LC_LOAD_DYLIB加載命令把所有依賴庫(kù)加載進(jìn)內(nèi)存衣形。
  • recursiveUpdateDepth() 遞歸刷新依賴庫(kù)的層級(jí)。
  • recursiveRebase() 由于ASLR的存在热凹,必須遞歸對(duì)主程序以及依賴庫(kù)進(jìn)行重定位操作泵喘。
  • recursiveBind() 把主程序二進(jìn)制和依賴進(jìn)來的動(dòng)態(tài)庫(kù)全部執(zhí)行符號(hào)表綁定。
  • weakBind() 如果鏈接的不是主程序二進(jìn)制的話般妙,會(huì)在此時(shí)執(zhí)行弱符號(hào)綁定,主程序二進(jìn)制則在link()完后再執(zhí)行弱符號(hào)綁定相速,后面會(huì)進(jìn)行分析碟渺。
  • recursiveGetDOFSections()、context.registerDOFs() 注冊(cè)DOF(DTrace Object Format)節(jié)突诬。
    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();
    // 遞歸加載加載主程序所需依賴庫(kù)
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
    ...
    uint64_t t1 = mach_absolute_time();
    context.clearAllDepths();
    // 遞歸刷新依賴庫(kù)的層級(jí)
    this->recursiveUpdateDepth(context.imageCount());
    uint64_t t2 = mach_absolute_time();
    // 遞歸進(jìn)行rebase
    this->recursiveRebase(context);
    uint64_t t3 = mach_absolute_time();
    // 遞歸綁定符號(hào)表
    this->recursiveBind(context, forceLazysBound, neverUnload);
    uint64_t t4 = mach_absolute_time();
    if  (  !context.linkingMainExecutable )
        // 弱符號(hào)綁定
        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;
    // 注冊(cè)DOF節(jié)
    this->recursiveGetDOFSections(context, dofs);
    context.registerDOFs(dofs);
    uint64_t t7 = mach_absolute_time();
    ...
}

6. 鏈接插入的動(dòng)態(tài)庫(kù)

這一步與鏈接主程序一樣苫拍,將前面調(diào)用addImage()函數(shù)保存在sAllImages中的動(dòng)態(tài)庫(kù)列表循環(huán)取出并調(diào)用link()進(jìn)行鏈接,需要注意的是旺隙,sAllImages中保存的第一項(xiàng)是主程序的鏡像绒极,所以要從i+1的位置開始,取到的才是動(dòng)態(tài)庫(kù)的ImageLoader:

ImageLoader* image = sAllImages[i+1];

接下來循環(huán)調(diào)用每個(gè)鏡像的registerInterposing()函數(shù)蔬捷,該函數(shù)會(huì)遍歷Mach-O的LC_SEGMENT_COMMAND加載命令垄提,讀取__DATA,__interpose榔袋,并將讀取到的信息保存到fgInterposingTuples中,接著調(diào)用applyInterposing()函數(shù)铡俐,內(nèi)部經(jīng)由doInterpose()虛函數(shù)進(jìn)行替換操作凰兑,以ImageLoaderMachOCompressed::doInterpose()函數(shù)的實(shí)現(xiàn)為例:該函數(shù)內(nèi)部調(diào)用了eachBind()與eachLazyBind(),具體處理函數(shù)是interposeAt()审丘,該函數(shù)調(diào)用interposedAddress()在fgInterposingTuples中查找需要替換的符號(hào)地址吏够,進(jìn)行最終的符號(hào)地址替換,代碼如下:

void  ImageLoaderMachOCompressed::doInterpose(const  LinkContext& context)
{
    // update prebound symbols
    eachBind(context,  &ImageLoaderMachOCompressed::interposeAt);
    eachLazyBind(context,  &ImageLoaderMachOCompressed::interposeAt);
}

uintptr_t  ImageLoaderMachOCompressed::interposeAt(const  LinkContext& context,  uintptr_t addr,  uint8_t type,  const  char*,
                                                   uint8_t,  intptr_t,  long,  const  char*,  LastLookup*,  bool runResolver)
{
    if  ( type == BIND_TYPE_POINTER )  {
        uintptr_t* fixupLocation =  (uintptr_t*)addr;
        uintptr_t curValue =  *fixupLocation;
        uintptr_t newValue = interposedAddress(context, curValue,  this);
        if  ( newValue != curValue)  {
            *fixupLocation = newValue;
        }
    }
    return  0;
}

7. 執(zhí)行弱符號(hào)綁定

weakBind()首先通過getCoalescedImages()合并所有動(dòng)態(tài)庫(kù)的弱符號(hào)到一個(gè)列表里滩报,然后調(diào)用initializeCoalIterator()對(duì)需要綁定的弱符號(hào)進(jìn)行排序锅知,接著調(diào)用incrementCoalIterator()讀取dyld_info_command結(jié)構(gòu)的weak_bind_off和weak_bind_size字段,確定弱符號(hào)的數(shù)據(jù)偏移與大小脓钾,最終進(jìn)行弱符號(hào)綁定售睹,代碼如下:

bool  ImageLoaderMachOCompressed::incrementCoalIterator(CoalIterator& it)
{
    if  ( it.done  )
        return  false;
    
    if  (  this->fDyldInfo->weak_bind_size ==  0  )  {
        /// hmmm, ld set MH_WEAK_DEFINES or MH_BINDS_TO_WEAK, but there is no weak binding info
        it.done  =  true;
        it.symbolName =  "~~~";
        return  true;
    }
    const  uint8_t* start = fLinkEditBase + fDyldInfo->weak_bind_off;
    const  uint8_t* p = start + it.curIndex;
    const  uint8_t*  end  = fLinkEditBase + fDyldInfo->weak_bind_off +  this->fDyldInfo->weak_bind_size;
    uintptr_t count;
    uintptr_t skip;
    uintptr_t segOffset;
    while  ( p <  end  )  {
        uint8_t immediate =  *p & BIND_IMMEDIATE_MASK;
        uint8_t opcode =  *p & BIND_OPCODE_MASK;
        ++p;
        switch  (opcode)  {
            case BIND_OPCODE_DONE:
                it.done  =  true;
                it.curIndex = p - start;
                it.symbolName =  "~~~";  // sorts to end
                return  true;
        }
        break;
        ...
    }
    ...
    return  true;
}

8. 執(zhí)行初始化方法

這一步由initializeMainExecutable()完成。dyld會(huì)優(yōu)先初始化動(dòng)態(tài)庫(kù)惭笑,然后初始化主程序侣姆。該函數(shù)首先執(zhí)行runInitializers(),內(nèi)部再依次調(diào)用processInitializers()沉噩、recursiveInitialization()捺宗。我們?cè)趓ecursiveInitialization()函數(shù)里找到了notifySingle()函數(shù):

context.notifySingle(dyld_image_state_dependents_initialized,  this,  &timingInfo);

接著跟進(jìn)notifySingle函數(shù),看到下面處理代碼:

if  (  (state == dyld_image_state_dependents_initialized)  &&  (sNotifyObjCInit != NULL)  && image->notifyObjC()  )  {
    uint64_t t0 = mach_absolute_time();
    (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
    uint64_t t1 = mach_absolute_time();
    uint64_t t2 = mach_absolute_time();
    uint64_t timeInObjC = t1-t0;
    uint64_t emptyTime =  (t2-t1)*100;
    if  (  (timeInObjC > emptyTime)  &&  (timingInfo != NULL)  )  {
        timingInfo->addTime(image->getShortName(), timeInObjC);
    }
}

我們關(guān)心的只有sNotifyObjCInit這個(gè)回調(diào)川蒙,繼續(xù)尋找賦值的地方:

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
    // record functions to call
    sNotifyObjCMapped = mapped;
    sNotifyObjCInit = init;
    sNotifyObjCUnmapped = unmapped;
    ...

再接著找registerObjCNotifiers函數(shù)調(diào)用蚜厉,最終找到這里:

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}

那么到底誰(shuí)調(diào)用了_dyld_objc_notify_register()呢?靜態(tài)分析已經(jīng)無法得知畜眨,只能對(duì)_dyld_objc_notify_register()下個(gè)符號(hào)斷點(diǎn)觀察一下了昼牛,
點(diǎn)擊Xcode的“Debug”菜單,然后點(diǎn)擊“Breakpoints”康聂,接著選擇“Create Symbolic Breakpoint...”贰健。如下圖所示。

image.png

在彈出的對(duì)話框中設(shè)置_dyld_objc_notify_register()符號(hào)斷點(diǎn)恬汁,按下圖所示伶椿。

對(duì)_dyld_objc_notify_register()函數(shù)下斷點(diǎn)

運(yùn)行程序,成功命中斷點(diǎn)氓侧,從調(diào)用椉沽恚看到是libobjc.A.dylib的_objc_init函數(shù)調(diào)用了_dyld_objc_notify_register()。如下圖所示约巷。

_dyld_objc_notify_register調(diào)用棧

https://opensource.apple.com/tarballs/objc4/下載objc源代碼偎痛,找到_objc_init函數(shù):

/***********************************************************************
 * _objc_init
 * Bootstrap initialization. Registers our image notifier with dyld.
 * Called by libSystem BEFORE library initialization time
 **********************************************************************/

void _objc_init(void)
{
    static  bool initialized =  false;
    if  (initialized)  return;
    initialized =  true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();
    
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

這里注冊(cè)的init回調(diào)函數(shù)就是load_images(),回調(diào)里面調(diào)用了call_load_methods()來執(zhí)行所有的+ load方法《览桑現(xiàn)在修改一下工程源碼踩麦,加入以下代碼并下斷點(diǎn)即可看到調(diào)用棧:

+  (void)load {
    NSLog(@"load");
}
+ load方法調(diào)用棧

notifySingle()之后就是調(diào)用doInitialization()枚赡,代碼如下:

// initialize this image
// 調(diào)用constructor()
bool hasInitializers =  this->doInitialization(context);

doInitialization()內(nèi)部首先調(diào)用doImageInit來執(zhí)行鏡像的初始化函數(shù),也就是LC_ROUTINES_COMMAND中記錄的函數(shù)靖榕,然后再執(zhí)行doModInitFunctions()方法來解析并執(zhí)行DATA,__mod_init_func這個(gè)section中保存的函數(shù)标锄,如下圖所示。

_mod_init_funcs中保存的是全局C++對(duì)象的構(gòu)造函數(shù)以及所有帶attribute((constructor)的C函數(shù)茁计。

__mod_init_func保存的函數(shù)指針

現(xiàn)在添加一些代碼再來運(yùn)行一下程序即可驗(yàn)證料皇,如下圖所示。

解析__mod_init_func并查看調(diào)用棧

10. 查找入口點(diǎn)并返回

這一步調(diào)用主程序鏡像的getThreadPC()星压,從加載命令讀取LC_MAIN入口践剂,如果沒有LC_MAIN就調(diào)用getMain()讀取LC_UNIXTHREAD,找到后就跳到入口點(diǎn)指定的地址并返回娜膘。
至此逊脯,整個(gè)dyld的加載過程就分析完成了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末竣贪,一起剝皮案震驚了整個(gè)濱河市军洼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌演怎,老刑警劉巖匕争,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異爷耀,居然都是意外死亡甘桑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門歹叮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來跑杭,“玉大人,你說我怎么就攤上這事咆耿〉铝拢” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵萨螺,是天一觀的道長(zhǎng)女阀。 經(jīng)常有香客問我,道長(zhǎng)屑迂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任冯键,我火速辦了婚禮惹盼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惫确。我一直安慰自己手报,他們只是感情好蚯舱,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掩蛤,像睡著了一般枉昏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上揍鸟,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天兄裂,我揣著相機(jī)與錄音,去河邊找鬼阳藻。 笑死晰奖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的腥泥。 我是一名探鬼主播匾南,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蛔外!你這毒婦竟也來了蛆楞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤夹厌,失蹤者是張志新(化名)和其女友劉穎豹爹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尊流,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡帅戒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了崖技。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逻住。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖迎献,靈堂內(nèi)的尸體忽然破棺而出瞎访,到底是詐尸還是另有隱情,我是刑警寧澤吁恍,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布扒秸,位于F島的核電站,受9級(jí)特大地震影響冀瓦,放射性物質(zhì)發(fā)生泄漏伴奥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一翼闽、第九天 我趴在偏房一處隱蔽的房頂上張望拾徙。 院中可真熱鬧,春花似錦感局、人聲如沸尼啡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)崖瞭。三九已至狂巢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間书聚,已是汗流浹背唧领。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留寺惫,地道東北人疹吃。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像西雀,于是被迫代替她去往敵國(guó)和親萨驶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348