iOS 應用加載dyld篇

iOS 應用加載dyld

前言

我們探索了iOS底層對象以及類的原理,對其有了大概的了解。那么iOS應用倒地是如何啟動的呢?從我們點擊桌面的圖標開始,究竟發生了什么呢?APP的生命周期倒地是怎么樣的呢?下面我們來研究一下。

1.APP加載初探

首先我們新建一個Single View App的項目,讓在main.m中打一個斷點:

main

然后我們查看堆棧信息,發現在main方法執行前有一步start,點擊這個start我們可以看到這一流程是發生在libdyld.dylib這個庫。

start

這說明我們的APP在進入到main函數前還通過dyld做了很多事情,那么dyld到底都做了什么事情呢,我們不妨從Apple OpenSource上搞一份dyld的源碼來看看。我們選擇的是dyld-733.6版本:

2.Dyld探索

打開dyld工程很長很長的目錄結構,如果一行一行的去分析估計得猴年馬月了,我都不知道+load方法是我們開發中被調用很早的方法,我們不妨在+load方法處打個斷點來看看調用堆棧。

load

此時我們發現了一個叫做_dyld_start的調用,并且我們還發現斷點先到了+load方法處,而不是main,說明+load方法的調用要早與main函數的調用。

2.1 _dyld_start

打開dyld 733.6的源碼,全局搜索_dyld_start,我們來到dyldStartup.s這個文件,并在聚焦于arm64架構下的匯編代碼:

_dyld_start

對于這里的匯編代碼,我們肯定也沒必要逐行分析,我們直接定位到bl跳轉語句,

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

這里注釋的意思是調用位于dyldbootstrap命名空間下的start方法,我們繼續探索一下這個start方法。

2.2 dyldbootstrap::start

_start源碼:

//
//  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 dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

    // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    rebaseDyld(dyldsMachHeader);

    // kernel sets up env pointer to be just past end of agv array
    const char** envp = &argv[argc+1];
    
    // kernel sets up apple pointer to be just past end of envp array
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;

    // set up random value for stack canary
    __guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
    // run all C++ initializers inside dyld
    runDyldInitializers(argc, argv, envp, apple);
#endif

    // now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = appsMachHeader->getSlide();
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
  • 首先開始kdebug跟蹤,以指示dyld引導程序的啟動
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
  • 調用rebaseDyld函數,該函數內實現了對Mach-O內部DATA段指針的重設基址綁定Fixup 修復)操作,并且初始化了mach和系統調用,最后將修復好的DATA段的數據標記為只讀。

rebaseDyld函數源碼:

//
// On disk, all pointers in dyld's DATA segment are chained together.
// They need to be fixed up to be real pointers to run.
//
static void rebaseDyld(const dyld3::MachOLoaded* dyldMH)
{
    // walk all fixups chains and rebase dyld
    const dyld3::MachOAnalyzer* ma = (dyld3::MachOAnalyzer*)dyldMH;
    assert(ma->hasChainedFixups());
    uintptr_t slide = (long)ma; // all fixup chain based images have a base address of zero, so slide == load address
    __block Diagnostics diag;
    ma->withChainStarts(diag, 0, ^(const dyld_chained_starts_in_image* starts) {
        ma->fixupAllChainedFixups(diag, starts, slide, dyld3::Array<const void*>(), nullptr);
    });
    diag.assertNoError();

    // now that rebasing done, initialize mach/syscall layer
    mach_init();

    // <rdar://47805386> mark __DATA_CONST segment in dyld as read-only (once fixups are done)
    ma->forEachSegment(^(const dyld3::MachOFile::SegmentInfo& info, bool& stop) {
        if ( info.readOnlyData ) {
            ::mprotect(((uint8_t*)(dyldMH))+info.vmAddr, (size_t)info.vmSize, VM_PROT_READ);
        }
    });
}
  • 接下來 內核將env指針設置為剛好超出agv數組的末尾;內核將apple指針設置為剛好超出envp數組的末尾。并進行堆棧溢出的保護。
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
    
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;

// set up random value for stack canary
__guard_setup(apple);
  • 然后根據當前dyld是否有初始值,在dyld中運行所有c++初始化器
#if DYLD_INITIALIZER_SUPPORT
    // run all C++ initializers inside dyld
    runDyldInitializers(argc, argv, envp, apple);
#endif
  • 最后在dyld完成引導后,讀取Mach-O header 中的 偏移量aooSlide,然后調用dyld命名空間的_main函數。
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);

2.3 dyld::_main

通過搜索namespace dyld {我們來到dyld2.cpp文件內,找_main函數,一看600多行代碼,如果逐行閱讀肯定很不明智,我們挑選有注釋的地方看,因為注釋的地方基本都是最主要的。

首先看_main函數注釋:

dyld的入口函數,內核加載了dyld,然后跳轉到__dyly_start來設置一些寄存器的值,然后調用到了這里(__main),此函數返回__dyld_start所跳轉到的目標程序的main函數地址

內核標志檢查
  • 然后檢查是否有內核標志

cdHash值.jpg
  • 從環境變量中獲取主可執行文件的cdHash值,這個哈希值mainExecutableCDHash在后面用來校驗dyld3的啟動閉包。

模擬器加載.jpg
  • 追蹤dyld的加載,然后判斷當前是否是模擬器環境,如果不是模擬器,則追蹤dyld和主二進制可執行文件的加載。

platform ID.jpg
  • 在所有鏡像文件中設置platform ID,以便調試器可以告訴進程類型,一旦我們在rdar://43369446中讓內核處理它,這些都是可以刪除的。

15985844879874.jpg
  • 判斷是否為macOS執行環境,如果是則看看是否需要覆蓋平臺,然后判斷DYLD_ROOT_PATH環境變量是否存在,如果存在,然后判斷是模擬器是否有自己指定的dyld,如果有就使用,如果沒有就返回錯誤信息。

15985958960585.jpg
  • 打印日志:dyld 啟動開始
  • 根據傳入的參數設置上下文
  • 獲取指向exec路徑的指針
  • 如果不是模擬器環境,并且是iPhone OS的最小支持版本,如果內核沒有返回主可執行文件的真實路徑就進行路徑的賦值
  • 判斷exec路徑是否為絕對路徑,如果為相對路徑,則使用cwd轉化為絕對路徑

15985960490868.jpg
  • 為了后續的日志打印從exec路徑中取出進程的名稱(strrchr函數是獲取第二個參數出現的最后一個位置,然后返回從這個位置開始到結束的內容)
  • 根據APP主可執行文件Mach-O Header的內容配置進程的一些限制條件
  • 檢查我們是否應該強制dyld3。注意,由于AMFI的原因,我們必須在常規的env解析之外進行此操作

15985961852736.jpg
  • 判斷執行環境是否是macOS,如果是在判斷上下文的一些配置屬性是否以及配置好了,如果沒配置好,就再次就行一次上下文的配置操作
  • 檢查環境變量
  • 初始化默認的回調路徑
  • 判斷當前APP的Mach-O的平臺類型是IOSMAC,重置上下文的根路徑,標記iOSonMactrue,在做一些回調路徑的判斷和設置
  • 如果不是IOSMAC而是driverKit在做一些配置

15985969100276.jpg
  • 根據環境變量打印一些信息
  • 對環境變量的一些解析容錯處理等
  • 如果不是TMPDIR格式就退出

15985974845146.jpg
  • 獲取Mach-O info
  • 判斷共享緩存是否開始,如果開啟了就將共享換粗映射到當前進程的邏輯內存空間內

15985977914313.jpg
  • 如果我們還沒有閉包模式,那么檢查環境和緩存類型

Xnip2020-08-28_15-02-54.jpg
  • 在非模擬器環境判斷閉包的狀態,如果是Off就獲取上下文的錯誤信息并打印日志
  • 如果不是則進行閉包的處理,由于dyld3會創建一個啟動閉包,我們需要來讀取它,這里會先在緩存里面查找是否有啟動閉包的存在,系統級的APP的啟動閉包存在于共享緩存中,我們自己開發的APP的啟動閉包是在APP安裝或者升級的時候構建的,所以這里檢查dyld中的緩存是有意義的。
  • 我們會嘗試在運行時期間構建一個的閉包,如果它是一個iOS第三方二進制文件,或一個來自共享緩存的macOS二進制文件
  • 如果我們沒有找到一個有效的緩存閉包,那么嘗試建立一個新的
  • 在建立閉包后退出dyld,不運行程序
  • 如果找到了有效的閉包則嘗試使用啟動閉包
    • 打印日志: dyld3 開始啟動
    • 如果啟動失敗則創建一個新的啟動閉包嘗試再次啟動
    • 如果啟動成功,由于start()函數指針的方式調用_main方法的返回指針,所以需要進行簽名。
  • 如果還沒有使用閉包則進行舊的啟動方式

15985995917633.jpg
  • 開始接受gdb通知,
  • 使初始化的空間足夠大,這樣就不太可能需要重新分配(re-alloced)

15985997596035.jpg
  • 添加門控機制,支持系統訂單文件生成過程
  • 最主要的是addDyldImageToUUIDList,添加dyld的景象文件到UUID列表中,主要目的是啟用堆棧的符號化。

reloadAllImages:

這里就是重頭戲了,加載所有的鏡像文件,我們挑重點看,因為這里涉及很多邏輯,包括對包版本Mach-O的不嚴格的要求,以及對simulators, iOS, tvOS, watchOS的一些不同處理。

下面我們看到如下代碼:

// instantiate ImageLoader for main executable
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

這里根據函數的意思就是實例化我們的主程序,我們來到這個方法內部:

// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
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";
}

該方法的作用就是內核在dyld獲得控制之前在主可執行文件中進行映射。我們需要為主可執行文件中已經映射的文件創建一個ImageLoader。從上面的代碼不難看出實際創建是通過ImageLoaderMachO::instantiateMainExecutable函數真正創建的。我們點擊跳轉到該方法。

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

我們繼續探索,通過上面的方法還需要進入到sniffLoadCommands函數看看。又是一個長長的方法,PS倒地、難受、生無可戀,看看注釋就知道sniffLoadCommands的主要功能是判斷當前加載的Mach-O文件是不是原始的二進制文件還是壓縮的LINKEDIT,以及獲取該文件有多少段。好的,既然這個函數的左右已經明確了,并且我們只是為了拿到ImageLoader,那么直接返回。sniffLoadCommands執行完畢后根據LINKEDIT是壓縮的格式還是傳統格式分別調用ImageLoaderMachOCompressed::instantiateMainExecutableImageLoaderMachOClassic::instantiateMainExecutable進行實例化ImageLoader

15986006360353.jpg

拿到ImageLoaer后開始加載動態庫,并記錄插入庫的數量,先試插入的庫,然后是主庫,最后是其他的庫

15986014239415.jpg

加載完畢就是鏈接庫:

15986021020368.jpg
  • 首先是鏈接主二進制文件
  • 其次鏈接插入的動態庫

我們再來看看link函數吧

void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex)
{
    // add to list of known images.  This did not happen at creation time for bundles
    if ( image->isBundle() && !image->isLinked() )
        addImage(image);

    // we detect root images as those not linked in yet 
    if ( !image->isLinked() )
        addRootImage(image);
    
    // process images
    try {
        const char* path = image->getPath();
#if SUPPORT_ACCELERATE_TABLES
        if ( image == sAllCacheImagesProxy )
            path = sAllCacheImagesProxy->getIndexedPath(cacheIndex);
#endif
        image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
    }
    catch (const char* msg) {
        garbageCollectImages();
        throw;
    }
}

link函數內部,會遞歸調用,來實現庫里面引用庫的鏈接操作,對于Bundle類型的庫被連接過就不會再次鏈接了,其他的庫被鏈接過的有不會被再次鏈接


15986027554117.jpg
  • 即使沒有DYLD_INSERT_LIBRARIES環境變量, dyld也應該支持插入

15986028300073.jpg
  • 如果支持快速表,則處理快速表

15986029446096.jpg
  • 應用插入到初始圖像集
  • 現在已經注冊了插入操作,為主可執行文件綁定和通知
  • 現在已注冊插入的圖像的綁定和通知
  • 弱綁定后,只有所有插入的圖像鏈接
  • 設置只讀上下文

15986031463860.jpg
  • 打印dyld 開始運行 initializers
  • 調用initializeMainExecutable初始化

initializeMainExecutable 代碼:

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 )
        ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
    if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
        ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
  • 這里先為所有插入并鏈接完成的動態庫執行初始化操作
  • 然后再為主程序可執行文件執行初始化操作
  • 注冊cxa_atexit()處理程序,在進程退出時在所有加載的圖像中運行靜態終止符
  • 如果需要就打印一些垃圾信息

runInitializers 源碼:

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.imagesAndPaths[0] = { this, this->getPath() };
    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);
}

runInitializers 內部我們繼續探索到processInitializers:
processInitializers源碼:

// <rdar://problem/14412057> upward dylib initializers can be run too soon
// To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
// have their initialization postponed until after the recursion through downward dylibs
// has completed.
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
                                     InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
    uint32_t maxImageCount = context.imageCount()+2;
    ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
    ImageLoader::UninitedUpwards& ups = upsBuffer[0];
    ups.count = 0;
    // 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.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
    }
    // If any upward dependencies remain, init them.
    if ( ups.count > 0 )
        processInitializers(context, thisThread, timingInfo, ups);
}

然后我們根據源碼繼續探索recursiveInitialization

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

繼續探索notifySingle

notifySingle

在第938行是獲取鏡像文件真實地址的代碼。

那么sNotifyObjCInit到底是在什么地方初始化的呢?我們通過全局搜索,最后找的在registerObjCNotifiers給其賦值。

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;

    // call 'mapped' function with all images mapped so far
    try {
        notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
    }
    catch (const char* msg) {
        // ignore request to abort during registration
    }

    // <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
    for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
        ImageLoader* image = *it;
        if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
            dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
            (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
        }
    }
}

那么是什么地方調用了registerObjCNotifiers呢,我們繼續搜索registerObjCNotifiers來到了_dyld_objc_notify_register函數。

_dyld_objc_notify_register 源碼:

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

那么我們還是會有疑問,_dyld_objc_notify_register的調用是從哪里開始的呢?我們在次全局搜索,卻沒有什么合適的地方是其被調用的地方。但是我們發現了如下所示的注釋:僅供objc運行時使用,這個時候我們就知道了,此處的調用不在dyld源碼里面了,所以我們需要轉入libobjc源碼繼續探索。

//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded.  During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images.  During any later dlopen() call,
// dyld will also call the "mapped" function.  Dyld will call the "init" function when dyld would be called
// initializers in that image.  This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped);
15986056339510.jpg

** 果不其然,我們在objc源碼中的_objc_init函數中找到了_dyld_objc_notify_register的調用。此處使用的是objc4-779.1

經過上面一連串的跳轉(xiabicaozuo)寶寶早就暈了。但是我們還是要淡定,這里是dyld注冊回調,這樣我們的Runtime才能知道鏡像何時加載完畢。在ImageLoader::recursiveInitialization函數中有這樣一行代碼:

// initialize this image
bool hasInitializers = this->doInitialization(context);

這里是真正做初始化操作的地方

doInitialization 源碼:

bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
    CRSetCrashLogMessage2(this->getPath());

    // mach-o has -init and static initializers
    doImageInit(context);
    doModInitFunctions(context);
    
    CRSetCrashLogMessage2(NULL);
    
    return (fHasDashInit || fHasInitializers);
}

ImageLoaderMachO::doInitialization源碼中可以看到主要有兩個操作,一個是:doImageInit,一個是doModInitFunctions

doImageInit 源碼:

void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
    if ( fHasDashInit ) {
        const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
        const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
        const struct load_command* cmd = cmds;
        for (uint32_t i = 0; i < cmd_count; ++i) {
            switch (cmd->cmd) {
                case LC_ROUTINES_COMMAND:
                    Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
#if __has_feature(ptrauth_calls)
                    func = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)func, ptrauth_key_asia, 0);
#endif
                    // <rdar://problem/8543820&9228031> verify initializers are in image
                    if ( ! this->containsAddress(stripPointer((void*)func)) ) {
                        dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
                    }
                    if ( ! dyld::gProcessInfo->libSystemInitialized ) {
                        // <rdar://problem/17973316> libSystem initializer must run first
                        dyld::throwf("-init function in image (%s) that does not link with libSystem.dylib\n", this->getPath());
                    }
                    if ( context.verboseInit )
                        dyld::log("dyld: calling -init function %p in %s\n", func, this->getPath());
                    {
                        dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
                        func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
                    }
                    break;
            }
            cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
        }
    }
}

doImageInit 內部會通過初始地址 + 偏移量拿到初始化器 func,然后進行簽名的驗證。驗證通過后還要判斷初始化器是否在鏡像文件中以及 libSystem 庫是否已經初始化,最后才執行初始化器。

doModInitFunctions 源碼:

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
    if ( fHasInitializers ) {
        const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
        const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
        const struct load_command* cmd = cmds;
        for (uint32_t i = 0; i < cmd_count; ++i) {
            if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
                const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
                const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
                const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
                for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
                    const uint8_t type = sect->flags & SECTION_TYPE;
                    if ( type == S_MOD_INIT_FUNC_POINTERS ) {
                        Initializer* inits = (Initializer*)(sect->addr + fSlide);
                        const size_t count = sect->size / sizeof(uintptr_t);
                        // <rdar://problem/23929217> Ensure __mod_init_func section is within segment
                        if ( (sect->addr < seg->vmaddr) || (sect->addr+sect->size > seg->vmaddr+seg->vmsize) || (sect->addr+sect->size < sect->addr) )
                            dyld::throwf("__mod_init_funcs section has malformed address range for %s\n", this->getPath());
                        for (size_t j=0; j < count; ++j) {
                            Initializer func = inits[j];
                            // <rdar://problem/8543820&9228031> verify initializers are in image
                            if ( ! this->containsAddress(stripPointer((void*)func)) ) {
                                dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
                            }
                            if ( ! dyld::gProcessInfo->libSystemInitialized ) {
                                // <rdar://problem/17973316> libSystem initializer must run first
                                const char* installPath = getInstallPath();
                                if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
                                    dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
                            }
                            if ( context.verboseInit )
                                dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
                            bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
                            {
                                dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
                                func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
                            }
                            bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
                            if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
                                // now safe to use malloc() and other calls in libSystem.dylib
                                dyld::gProcessInfo->libSystemInitialized = true;
                            }
                        }
                    }
                    else if ( type == S_INIT_FUNC_OFFSETS ) {
                        const uint32_t* inits = (uint32_t*)(sect->addr + fSlide);
                        const size_t count = sect->size / sizeof(uint32_t);
                        // Ensure section is within segment
                        if ( (sect->addr < seg->vmaddr) || (sect->addr+sect->size > seg->vmaddr+seg->vmsize) || (sect->addr+sect->size < sect->addr) )
                            dyld::throwf("__init_offsets section has malformed address range for %s\n", this->getPath());
                        if ( seg->initprot & VM_PROT_WRITE )
                            dyld::throwf("__init_offsets section is not in read-only segment %s\n", this->getPath());
                        for (size_t j=0; j < count; ++j) {
                            uint32_t funcOffset = inits[j];
                            // verify initializers are in TEXT segment
                            if ( funcOffset > seg->filesize ) {
                                dyld::throwf("initializer function offset 0x%08X not in mapped image for %s\n", funcOffset, this->getPath());
                            }
                            if ( ! dyld::gProcessInfo->libSystemInitialized ) {
                                // <rdar://problem/17973316> libSystem initializer must run first
                                const char* installPath = getInstallPath();
                                if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
                                    dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
                            }
                            Initializer func = (Initializer)((uint8_t*)this->machHeader() + funcOffset);
                            if ( context.verboseInit )
                                dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
                            bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
                            {
                                dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
                                func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
                            }
                            bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
                            if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
                                // now safe to use malloc() and other calls in libSystem.dylib
                                dyld::gProcessInfo->libSystemInitialized = true;
                            }
                        }
                    }
                }
            }
            cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
        }
    }
}

doModInitFunctions函數通過判斷fHasInitializers,從load_command里循環加載所有方法


現在我們回到start函數

通知監聽所有監聽著該進程即將進入main()

// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
Xnip2020-08-28_17-56-53.jpg
  • 最后就是根據各個不同的版本對result進行賦值,還有部分容錯處理,最后返回result

小結:

至此我們的dyld::start就粗略的分析完畢了,過程有點復雜,有些也是一知半解的,對于dyld這個強大工具,由于本人才疏學淺,肯定難以分析的很透徹,如有錯誤歡迎指正。

3. _objc_init 初探

  • 注: 此處使用的是objc4-779.1_objc_init函數在objc-os.mm文件中。

根據上面的分析,我們來到objc4-779.1的源碼中分析,根據上面的各種調用關系,我們來到_objc_init這個函數中打個斷點,看看函數的調用棧,來驗證一下我們的分析。

函數調用棧.jpg

通過函數調用棧我們可以清楚的看到此處的調用順序,在dyld中最后調用的是doModInitFunctions函數。由此我們可以確定一個順序:

dyld->libSystem:libSystem_initializer->libdispatch:libdispatch_init->libdispatch:_os_object_init->libobjc:_objc_init

我們打開libSystem的源碼通過全局搜索libSystem_initializer,找到該函數,并在該函數內找到了libdispatch_init的調用。

libSystem_initializer.jpg

同樣我們來到libdispatch的源碼通過全局搜索libdispatch_init,找到該函數,并在函數內找到了_os_object_init的調用。

libdispatch_init.jpg

我們繼續在libdispatch的源碼查找_os_object_init,同樣也驗證了_objc_init函數的調用。

_os_object_init.jpg

至此我們找到了_objc_init的調用處,我們的初探就到這里結束了,那么_objc_init內部具體做了什么,我們會在后面的文章中著重分析。

4.總結

  • 本文通過從APP的啟動時的start函數引入到了對dyld的探索
  • dyld匯編中的_dyld_start找到dyldbootstrap::start
  • dyldbootstrap::start調用了rebaseDyld函數對Mach-O內部的DATA段指針進行了重設基址和綁定操作,并且初始化了mach的系統調用。接下來start函數還做了很多初始化操作
  • 下面從dyldbootstrap::start中調用了dyld::_main
  • _main中初始化一些上下文,對不同環境的加載做相應的區分,開啟各種日志的打印,接收通知,對dyld2dyld3的閉包做相應處理,最后來到重頭戲reloadAllImages加載所有的鏡像文件
  • 加載鏡像文件首先實例化一個ImageLoader,通過ImageLoader加載動態庫,記錄并插入庫的數量,先試引用的庫后主庫,最后是其他庫,加載完畢就是鏈接庫
  • 鏈接庫首先是鏈接主二進制文件,其次是引入的動態庫,鏈接時是遞歸操作的,通過遞歸來實現庫里面引用庫的連接操作,鏈接完畢后循環插入初始鏡像集
  • 接下來就是為主可執行文件綁定通知,進行弱引用處理,設置只讀上下文;初始化所有動態庫,先初始化所有插入并連接完成的動態庫,然后為主程序執行初始化操作
  • 然后我們通過獲取鏡像文件的真實地址對其進行初始化,初始化的時候通過各種調用的分析,我們找到了它在objc:_objc_init函數的具體初始化操作。
  • 最后通過notifyMonitoringDyldMain函數,通知監聽所有監聽著該進程即將進入main()函數
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370