应用加载流程探索 - dyld

1,433 阅读10分钟

接下来,我们开始探索应用的加载流程,应用程序在启动的时候系统究竟做了什么事情, 当我们command+r运行项目时,就会生成一个mach-o这样的可执行文件。

Mach-O文件

mach-o是一种用于可执行文件、目标文件,静态库,动态库的文件格式。 属于 Mach-O 格式的常见文件:

  • 目标文件 .o
  • 库文件
    • .a
    • .dylib
    • Framework
  • 可执行文件
  • dyld ( 动态链接器 )
  • .dsym ( 符号表 ) 我们的工程在生成可执行文件之前都需要经过很多的过程:
  • 预编译:主要去处理源代码中#开头的预编译命令,删除注释,替换宏定义,将#import倒入的文件放在知道位置等操作。
  • 编译:进行词法分析,语法分析,语义分析,生成相应的汇编代码文件
  • 汇编:加编译完的汇编代码文件翻译成机器指令,生成.o文件
  • 链接:静态链接 和 动态链接 静态库的本质是一堆.o文件的集合,当我们的代码经过静态链接后我们的程序中就不会再存在静态库里;动态库的本质是已经链接完全的镜像image(表示任意一种类型的可执行文件),镜像image可以看成三种类型可执行文件dylibbundle,我们在项目中断点,用lldb输入image list查看当前镜像信息 20220512145848141.png 我们可以看到非常多的框架信息,这些都是属于image镜像文件。

dyld

我们app启动的时候,真正的加载过程是从一个exec()的函数开始,这个函数为我们的程序分配内存,创建进程,然后将app对应的可执行文件加载进内存,再将dyld加载到内存,dyld进行动态链接。 dyld具体的工作流程:

  • 1.找到可执行文件中所依赖的动态库并加载到内存,这是一个递归加载的过程,因为可以会依赖其他动态库
  • 2.rebase和binding,此处涉及到虚拟内存,略过
  • 3.调起main() 所以,想要研究dyld,我们需要在main之前进行断点,我们重写一个+load()方法,查看其堆栈信息,因为+load方法肯定是在main之前执行的 20220512152205842.png

_dyld_start

我们先来看一下,入口函数_dyld_start,在dyld的源码中搜索到dyldStartup.s中我们找到其汇编实现,我们找一下arm64架构的源码

#if __arm64__ && !TARGET_OS_SIMULATOR
.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

...省略...

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

...省略...

// LC_MAIN case, set up stack for call to main()
Lnew: mov lr, x1     // simulate return address into _start in libdyld.dylib
#if __LP64__
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

我看不懂汇编代码,可是堆栈信息我们知道,他会调用dyldbootstrap::start

dyldbootstrap::start

dyldbootstrap::start就是指dyldbootstrap这个命名空间作用域里的start函数。来到源码中 ,搜索dyldbootstrap,然后找到start函数 .

uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue) {
    // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    rebaseDyld(dyldsMachHeader);
    
    // kernel sets up env pointer to be just past end of agv array
    const char** envp = &argv[argc+1];

    // kernel sets up apple pointer to be just past end of envp array
    const char** apple = envp;
    
    while(*apple != NULL) { ++apple; }
    ++apple;
    
    // set up random value for stack canary
    __guard_setup(apple);

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

    _subsystem_init(apple);

    // now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = appsMachHeader->getSlide();
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

其作用就是初始化,为main函数做好一系列的准备工作,然后调用 dyld 的 main 函数 dyld::_main

dyld::_main

直接点击跳转到 dyld - main 函数中 . 该函数是加载 app 的主要函数.

uintptr_t _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue) {
    ···将近1000行代码,省略··· 
}

其最下面是有个返回值result,我们通过操作result的代码,来阅读其内部的代码,先将这1000行代码复制到一个空白文档中,全局搜索result,有16个。

// if this is host dyld, check to see if iOS simulator is being run
    const char* rootPath = _simple_getenv(envp, "DYLD_ROOT_PATH");
    if ( (rootPath != NULL) ) {
        // look to see if simulator has its own dyld
        char simDyldPath[PATH_MAX];
        strlcpy(simDyldPath, rootPath, PATH_MAX);
        strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
        int fd = dyld3::open(simDyldPath, O_RDONLY, 0);
        if ( fd != -1 ) {
            const char* errMessage = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue, &result);
            if ( errMessage != NULL )
                halt(errMessage);
            return result;
        }
    }
    
    // try using launch closure
        if ( mainClosure != nullptr ) {
            CRSetCrashLogMessage("dyld3: launch started");
            if ( mainClosure->topImage()->fixupsNotEncoded() )
                sLaunchModeUsed |= DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
            Diagnostics diag;
            bool closureOutOfDate;
            bool recoverable;
            bool launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
                                              mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
            if ( !launched && closureOutOfDate && allowClosureRebuilds ) {
                // closure is out of date, build new one
                mainClosure = buildLaunchClosure(canUseClosureFromDisk, mainExecutableCDHash, mainFileInfo, envp, bootToken);
                if ( mainClosure != nullptr ) {
                    diag.clearError();
                    sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
                    if ( mainClosure->topImage()->fixupsNotEncoded() )
                        sLaunchModeUsed |= DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
                    else
                        sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
                    launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
                                                 mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
                }
            }
            if ( launched ) {
                gLinkContext.startedInitializingMainExecutable = true;
                if (sSkipMain)
                    result = (uintptr_t)&fake_main;
                return result;
            }
            
    {
            // find entry point for main executable
            result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
            if ( result != 0 ) {
                // main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
                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->getEntryFromLC_UNIXTHREAD();
                *startGlue = 0;
            }

        }
        
     if (sSkipMain) {
        notifyMonitoringDyldMain();
        if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
            dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
        }
        ARIADNEDBG_CODE(220, 1);
        result = (uintptr_t)&fake_main;
        *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
    }

我们找到了四处操作了result的地方,仔细查看,我们能够看出操作result的地方只有result = (uintptr_t)sMainExecutable->result = (uintptr_t)&fake_main, 我们先来看fake_main做了什么事情, int fake_main() { return 0; },没有任何操作,只是返回0,所以我们重点来看一下sMainExecutable具体都做了什么工作

sMainExecutable

我们先来查看其初始化的地方

// 为工程的可执行文件初始化一个 imageLoader,镜像文件的加载器
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

其下面的代码才是我们关系的加载过程,我们还是将他们复制到一个空白文档中,方便我们阅读。 我们在下面找到了一个函数initializeMainExecutable,其注释的含义就是进行所有初始化操作,我们到源码中搜索然后点击跳转查看其实现

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

// 为所有动态库进行初始化
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]);
    }
}
// 为我们的主可执行文件进行初始化
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]);
}

初始化主要是使用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);
}

代码量很少,我们重点研究一下processInitializers,查看其内部实现

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进行初始化库

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

经过前面的流程,我们终于找到真正初始化的地方了,其内部使用mach_absolute_time让objc知道初始化image这个可执行文件,因为我们所有初始化可执行文件都需要依赖ObjCcontext.notifySingle进行单个通知工作, ,doInitialization调用init方法,dyld_image_state_initialized让所有人知道已完成image初始化工作。

notifySingle
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo){
    …省略部分…
    if ( state == dyld_image_state_mapped ) {
        if (!image->inSharedCache() || (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {
            dyld_uuid_info info;
            if ( image->getUUID(info.imageUUID) ) {
                info.imageLoadAddress = image->machHeader();
                addNonSharedCacheImageUUID(info);
            }
        }
    }

    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());
        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);
        }
    }
    …省略部分…
}

我们看到在初始化image时,将state传递的是dyld_image_state_dependents_initializednotifySingle中会调用sNotifyObjCInit这个函数,我们源码中全局搜索,发现其赋值的位置在registerObjCNotifiers,再次全局搜索registerObjCNotifiers的调用位置,找到了_dyld_objc_notify_register,继续搜索_dyld_objc_notify_register,找到不调用的地方,说明该函数不是在dyld中进行的调用的,我们打开objc的源码进行搜索,发现其是在objc_init中进行的调用,所以说我们所有初始化可执行文件都需要依赖ObjC

/***********************************************************************
* _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();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

_objc_init

现在我们来探究一下_objc_init,首先来看一下谁对他进行的调用,objc源码中进行断点调试 20220512173943286.png 我们发现是libdispatch.dylib _os_object_init,说明是在GCD的源码中进行的调用,下面的调用栈最终指向的是libSystem.B.dylib libSystem_initializer。不再进行更深入的查找了,我们主要还是来看_dyld_objc_notify_register调用是传递了三个参数 , 这三个分别代表:

  • map_images : dyldimage 加载进内存时 , 会触发该函数
  • load_images : dyld 初始化 image 会触发该方法. ( 我们所熟知的 load 方法也是在此处调用 )
  • unmap_image : dyldimage 移除时 , 会触发该函数 和dyld源码对比,我们发现load_images对应的就是registerObjCNotifiers中的sNotifyObjCInitnotifySingle中的(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());,相当于就是执行了_objc_init中的load_images这个函数。为什么+load会在main函数之前执行,其实就是因为load_images在此执行会加载所有的+load方法。
doInitialization

调用完load_images后,下面将继续执行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);  
}

doImageInitdoModInitFunctions两个函数中,都有一句报错信息,... that does not link with libSystem.dylib,说明libSystem.dylib这个动态库必须在第一个初始化,否则其他无法初始化,因为libSystem.dylib中的方法才能初始化_objc_init,其他的动态库需要依赖_objc_init才可以进行初始化。

上面我们知道load_images的调用位置,那map_images是在哪里调用的呢,我们全局搜索后发现notifyBatchPartial方法中进行的调用, 继续查找其调用位置notifyBatchregisterImageStateBatchChangeHandlerregisterObjCNotifiers,我们再次查找notifyBatch,在之前调用递归processInitializers的方法下面,找到了其调用ImageLoader::runInitializers,也就是说dyld在初始化动态库的时候就会调用map_imagesload_images这两个函数。 map_imagesload_images这两个函数具体功能,再下篇文章中再来了解。

总结

dyld - 加载动态库和可执行文件的初始化操作:

  • dyld 加载到开始链接主程序的时候, 递归调用 recursiveInitialization 函数
  • 这个函数第一次执行 , 进行 libsystem 的初始化, 会走到 doInitialization -> doModInitFunctions -> libSystemInitialized
  • libsystem 的初始化,它会调用起 libdispatch_initlibdispatchinit 会调用 _os_object_init,这个函数里面调用了 _objc_init
  • _objc_init 中注册并保存了 map_imagesload_imagesunmap_image 函数地址
  • 注册完毕继续回到 recursiveInitialization 递归下一次调用,例如 libobjc, 当 libobjc 来到 recursiveInitialization 调用时,会触发 notifySingle 调用 里注册好的sNotifyObjCInit回调函数。也就是 libobjc中的load_images