九 OC底層原理 APP 的加載流程

前言

當(dāng)我們的應(yīng)用程序被打開的時候,kernel(內(nèi)核)就會將應(yīng)用程序加載到內(nèi)存中,同時kernel 又回加載另一個程序,就是我們的dyld(動態(tài)鏈接器), 不管是應(yīng)用程序,還是dyld 都是一個可執(zhí)行文件,在MacOS系統(tǒng)中稱做Mach-o

一. Mach-o 介紹

  • 當(dāng)我們編譯一個 ios 程序的時候,可以在 程序的根目錄下的文件夾 products 中看到一個 .app 的文件,右擊該文件 查看包內(nèi)容,就一個看到程序的同名的Mach-o 的一個可執(zhí)行文件

  • 現(xiàn)在我們利用 MachOView 軟件,查看Mach-o的機(jī)構(gòu)

    image.png

1.1 mach-o 文件結(jié)構(gòu)

  • Header: Mach Header 里會有Mach-OCPU 信息,以及 Load Command 的信息。
  • Load commands : 包含Mach-O 里命令類型信息,名稱和二進(jìn)制文件的位置。
  • Data: DataSegment 的數(shù)據(jù)組成,是 Mach-O 占比最多的部分,有代碼有數(shù)據(jù),比如符號表。Data 共三個 Segment,TEXT、DATA、LINKEDIT。其中 TEXT 和 DATA 對應(yīng)一個或多個 Section,LINKEDIT 沒有 Section
image.png

1.2 Segment

一般Mach-O文件有多個段(Segement),每個段有不同的功能,一般包括:

  • __TEXT: 代碼段,包含頭文件、代碼和只讀常量。只讀不可修改
  • __DATA: 包含全局變量,靜態(tài)變量等,該段可寫;
  • __LINKEDIT: 包含了方法和變量的元數(shù)據(jù)(位置,偏移量),以及代碼簽名等信息。只讀不可修改。

二 APP 加載

  • 我們在 程序的 main 函數(shù)執(zhí)行之前 下個斷點(diǎn), 我們可以看到在執(zhí)行main()函數(shù)之前,調(diào)用的是 start,同時,這一流程是由libdyld.dylib庫執(zhí)行的。dyld 是開源庫,可以下載源碼探索。
    dyld 開源下載地址

    image.png

  • 為了更詳細(xì)的看見執(zhí)行過程 我們在ViewControllerload函數(shù)下個斷點(diǎn)

    image.png

2.1 __dyld_start

我們在開源代碼中的 dyldStartup.s 找到了 __dyld_start的函數(shù)入口,在函數(shù)實(shí)現(xiàn)中找到了這樣一段注釋,這樣我們可以得知 __dyld_start是一個匯編函數(shù),其內(nèi)調(diào)用了 dyldbootstrap::start

image.png

2.2 dyldbootstrap::start

image.png

這個函數(shù)注釋表示,這段代碼是用來啟動 dyld的,一般情況下程序的啟動工作都是 dyld和crt完成的,在dyld中需要我們手動完成

這段代碼主要執(zhí)行的三個事情

2.3 rebaseDyld

image.png

方法注釋表示:在磁盤中,在dyld 的 Data segment中所有的指針都是鏈在一起的,需要修復(fù)執(zhí)行正確的指針,所有的鏡像的基地址都是0,所以偏移量slide 就是加載地址

2.4 __guard_setup

// set up random value for stack canary
就是 設(shè)置棧 溢出保護(hù)

2.5 dyld::_main

執(zhí)行了dyldmain 函數(shù)

三 dyld::_main

3.1 dyld::_main 入口解析

image.png

注釋:表示這是 dyld的入口,內(nèi)核加載 dyld 并且跳轉(zhuǎn)到了 __dyld_start函數(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)
{
    ······
    // 準(zhǔn)備dyld的啟動工作,配置相關(guān)的環(huán)境變量
    ······
    CRSetCrashLogMessage("dyld: launch started");
    // 設(shè)置上下文
    setContext(mainExecutableMH, argc, argv, envp, apple);
    ······
    // 檢測線程是否受限
    configureProcessRestrictions(mainExecutableMH, envp);

    {
        // 檢測環(huán)境變量
        checkEnvironmentVariables(envp);
        defaultUninitializedFallbackPaths(envp);
    }
    ······
    // 打印opts
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    // 打印環(huán)境變量
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    ······
    // 獲取程序架構(gòu)
    getHostInfo(mainExecutableMH, mainExecutableSlide);

    // load shared cache 檢測共享緩存禁用狀態(tài)
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
        if ( sSharedCacheOverrideDir)
            mapSharedCache();
#else
        // 加載共享緩存庫
        mapSharedCache();
#endif
    }
    ······
        // add dyld itself to UUID list 將dyld 添加到UUID列表中
        addDyldImageToUUIDList();
    ······
        // instantiate ImageLoader for main executable  實(shí)例化主程序
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
    ······
        // load any inserted libraries 加載插入的動態(tài)庫
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
    ······
        // 鏈接主程序
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
    ······
        // link any inserted libraries  鏈接所有插入的動態(tài)庫
        // 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
        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);
                }
            }
        }
        ······
        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        // //弱符號綁定
        sMainExecutable->weakBind(gLinkContext);

        sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);

        CRSetCrashLogMessage("dyld: launch, running initializers");
        // run all initializers   執(zhí)行所有初始化方法
        initializeMainExecutable(); 

        // notify any montoring proccesses that this process is about to enter main()
        // notifyMonitoringDyldMain監(jiān)聽dyld的main
        notifyMonitoringDyldMain();     
            // find entry point for main executable
            // 找到主程序的入口
        result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
    return result;
}

3.2 主要過程-簡述

  1. 設(shè)置運(yùn)行環(huán)境,為可執(zhí)行文件的加載做準(zhǔn)備工作
  2. 映射共享緩存到當(dāng)前進(jìn)程的邏輯內(nèi)存空間
  3. 實(shí)例話主程序
  4. 加入插入的動態(tài)庫
  5. 鏈接主程序
  6. 鏈接插入的動態(tài)庫
  7. 執(zhí)行弱符號綁定
  8. 執(zhí)行初始化方法
  9. 查找程序入口并返回main()

3.3.1 checkSharedRegionDisable 檢測共享緩存

函數(shù)底部的注釋,表示iOS不能在共享區(qū)域以外運(yùn)行

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

3.3.2 mapSharedCache() 函數(shù)的核心就是 loadDyldCache()

共享區(qū)域的共享的實(shí)現(xiàn)

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 ) {
        // 如果為強(qiáng)制私有類型 緩存就只映射到當(dāng)前進(jìn)程
        // mmap cache into this process only
        return mapCachePrivate(options, results);
    }
    else {
        // 緩存已經(jīng)映射到共享區(qū)域的時候
        // fast path: when cache is already mapped into shared region
        bool hasError = false;
        // 復(fù)制已有的緩存
        if ( reuseExistingCache(options, results) ) {
            hasError = (results->errorMessage != nullptr);
        } else {
            // 當(dāng)前進(jìn)程就是第一個加載緩存的進(jìn)程
            // slow path: this is first process to load cache
            hasError = mapCacheSystemWide(options, results);
        }
        return hasError;
    }
#endif
}

3.3.3 instantiateFromloadedImage

isCompatibleMachO 是檢查Mach-O的subtype是否是當(dāng)前cpu可以支持; 內(nèi)核會映射到主可執(zhí)行文件中,我們需要為映射到主可執(zhí)行文件的文件,創(chuàng)建ImageLoader。
instantiateMainExecutable 就是實(shí)例化可執(zhí)行文件, 這個期間會解析LoadCommand, 這個之后會發(fā)送 dyld_image_state_mapped 通知; 在此方法中,讀取image,然后addImage() 到鏡像列表。
addImage() 就是將image 添加到 imageList。因此我們可以lldb 調(diào)試image list 命令,就可以看到第一個就是macho

static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
    // try mach-o loader
    if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);

        addImage(image);
        return (ImageLoaderMachO*)image;
    }
    
    throw "main executable not a known format";
}

3.3.4 loadInsertedDylib

加載插入的動態(tài)庫

3.3.5 link(sMainExecutable,...) 鏈接主程序 和 link(image,...) 鏈接所有插入的動態(tài)庫

對上面生成的 Image 進(jìn)行鏈接。這個過程就是將加載進(jìn)來的二進(jìn)制變?yōu)榭捎脿顟B(tài)的過程。其主要做的事有對image進(jìn)行 load(加載),rebase(基地址復(fù)位)bind(外部符號綁定),我們可以查看源碼:

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
    ......
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);  
    ......
    this->recursiveRebaseWithAccounting(context);
    ......
    this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
}

recursiveLoadLibraries: 遞歸加載所有依賴庫進(jìn)內(nèi)存。
recursiveRebase: 遞歸對自己以及依賴庫進(jìn)行rebase操作
recursiveBindWithAccounting: 對庫中所有nolazy的符號進(jìn)行bind,一般的情況下多數(shù)符號都是lazybind的,他們在第一次使用的時候才進(jìn)行bind。

3.3.6 initializeMainExecutable()

void initializeMainExecutable()
{
    // record that we've reached this step
    gLinkContext.startedInitializingMainExecutable = true;
    // 調(diào)用每個image 的 initalizer 方法進(jìn)行初始化,
    // 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]);
        }
    }
    // 為主程序可執(zhí)行文件執(zhí)行初始化
    // 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 )
        ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
    if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
        ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

3.3.7 ImageLoader::runInitializers 流程

  • ImageLoader::runInitializers


    image.png
  • processInitializers


    image.png

    在這里,對鏡像表中的所有鏡像執(zhí)行recursiveInitialization ,創(chuàng)建一個未初始化的向上依賴新表。如果依賴中未初始化完畢,則繼續(xù)執(zhí)行processInitializers,直到全部初始化完畢。

  • ImageLoader::recursiveInitialization

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                          InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    recursive_lock lock_info(this_thread);
    recursiveSpinLock(lock_info);

    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        uint8_t oldState = fState;
        // break cycles
        fState = dyld_image_state_dependents_initialized-1;
        try {
            // initialize lower level libraries first
            for(unsigned int i=0; i < libraryCount(); ++i) {
                ImageLoader* dependentImage = libImage(i);
                if ( dependentImage != NULL ) {
                    // don't try to initialize stuff "above" me yet
                    if ( libIsUpward(i) ) {
                        uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
                        uninitUps.count++;
                    }
                    else if ( dependentImage->fDepth >= fDepth ) {
                        dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
                    }
                }
            }
            
            // record termination order
            if ( this->needsTermination() )
                context.terminationRecorder(this);

            // let objc know we are about to initialize this image
            uint64_t t1 = mach_absolute_time();
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            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);
            
            if ( hasInitializers ) {
                uint64_t t2 = mach_absolute_time();
                timingInfo.addTime(this->getShortName(), t2-t1);
            }
        }
        catch (const char* msg) {
            // this image is not initialized
            fState = oldState;
            recursiveSpinUnLock();
            throw;
        }
    }
    
    recursiveSpinUnLock();
}

在這個函數(shù)中 我們主要關(guān)注

  • context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
  • doInitialization(context)
  • context.notifySingle(dyld_image_state_initialized, this, NULL);

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

通知objc我們要初始化這個鏡像,這里 通過 notifySingle 函數(shù)對sNotifyObjCInit 進(jìn)行函數(shù)調(diào)用。

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo){
...
 if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
        uint64_t t0 = mach_absolute_time();
        dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
        (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
            ...
        }
    } 
...
}

獲取鏡像文件的真實(shí)地址 (*sNotifyObjCInit)(image->getRealPath(), image->machHeader() , 但是根據(jù)最開始的堆棧信息 可以看到 下一步就應(yīng)該是調(diào)用 load_images

image.png

主要是因為 (*sNotifyObjCInit)(image->getRealPath(), image->machHeader() 這是一個回調(diào)函數(shù)

在if判斷條件中對sNotifyObjCInit進(jìn)行了非空判斷,也就是有值,在本文件搜索,發(fā)現(xiàn)它在 registerObjCNotifiers 中被賦值

3.3.7.2 registerObjCNotifiers

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, 發(fā)現(xiàn) 只有 _dyld_objc_notify_register 中調(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);
}

參數(shù):

  • _dyld_objc_notify_mapped : dyld 將 image 加載進(jìn)內(nèi)存時 , 會觸發(fā)該函數(shù).
  • _dyld_objc_notify_init : dyld 初始化 image 會觸發(fā)該方法. ( 我們所熟知的 load 方法也是在此處調(diào)用 ) .
  • unmap__dyld_objc_notify_unmapped: dyld 將 image 移除時 , 會觸發(fā)該函數(shù) .

我們在 下一個符號斷點(diǎn) _dyld_objc_notify_register

image.png

可以看到 _dyld_objc_notify_register 之前調(diào)用了 _objc_init

3.3.7.3 _objc_init

_dyld_objc_notify_register函數(shù)是供 objc runtime 使用的,當(dāng)objc鏡像被映射,取消映射,和初始化時 被調(diào)用的注冊處理器。我們可以在libobjc.A.dylib庫里,_objc_init函數(shù)中找到其調(diào)用。


/***********************************************************************
* _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(); // 環(huán)境變量
    tls_init(); //線程 key 的綁定
    static_init(); //初始化系統(tǒng)內(nèi)置的 C++ 靜態(tài)構(gòu)造函數(shù)
    runtime_init(); //主要是運(yùn)行時的初始化,主要分為兩部分:分類初始化和類的表初始化
    exception_init(); // 初始化libobjc異常處理
    cache_init(); //緩存初始化
    _imp_implementationWithBlock_init(); //啟動機(jī)制回調(diào)

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

3.3.8 doInitialization

我們從最新的堆棧信息 可以看到 recursiveInitialization() 函數(shù),之后就調(diào)用了doInitialization()

image.png

在 doModInitFunctions之后 會 先執(zhí)行 libSystem_initializer,保證系統(tǒng)庫優(yōu)先初始化完畢,在這里初始化 libdispatch_init,進(jìn)而在_os_object_init 中 調(diào)用 _objc_init。

由于 runtime 向 dyld 綁定了回調(diào),當(dāng) image 加載到內(nèi)存后,dyld 會通知 runtime 進(jìn)行處理
runtime 接手后調(diào)用 map_images 做解析和處理,接下來 load_images 中調(diào)用 call_load_methods 方法,遍歷所有加載進(jìn)來的 Class,按繼承層級依次調(diào)用 Class 的 +load 方法和其 Category 的 +load 方法。
至此,可執(zhí)行文件和動態(tài)庫中所有的符號(Class,Protocol,Selector,IMP,…)都已經(jīng)按格式成功加載到內(nèi)存中,被 runtime 所管理,在這之后,runtime 的那些方法(動態(tài)添加 Class、swizzle 等等才能生效)

四 總結(jié)- 流程

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

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

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