iOS底层原理之dyld应用程序加载

2,999 阅读36分钟

前言

前面我们对程序启动之后的许多底层知识进行了探索,但是程序是怎么加载启动的呢,作为一个程序员,应该有相应的了解,现在我们就来探索下程序加载的原理。

准备工作

1: 应用程序加载原理

开始今天话题之前,先对相关知识进行一下简介,以便后续能更好的分析理解。

1.1: 编译流程

编译流程:

  • 预编译(Precompile):把宏替换,删除注释,展开头文件,产生.i文件。
  • 编译(Compliling):把之前的.i文件转换成汇编语言,产生.s文件。
  • 汇编(Asembly):把汇编语言文件转换为机器码文件,产生.o文件。
  • 链接(Link):将所有的.o文件以及链接的库,生成一个MachO类型的可执行文件。

1.2: 编译流程图

CompileFlowchart.jpg

1.3: 库的简介

1.3.1: 动态库和静态库的区别

  • 静态库在程序编译时会被链接到目标代码中一起打包生成可执行文件,以.a.framework为文件后缀名。
  • 而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,以.tbd(之前叫.dylib) 和.framework为文件后缀名(系统直接提供给我们的framework都是动态库!)。

备注:.a是一个纯二进制文件,.framework中除了有二进制文件之外还有资源文件。.a,要有.h文件以及资源文件配合,.framework文件可以直接使用。总的来说,.a + .h + sourceFile = .framework。所以创建静态库最好还是用.framework的形式。

1.3.2: 静态库优缺点

静态库优缺点 优点

  • 静态库被打包到可执行文件中,编译成功后可执行文件可以独立运行,不需要依赖外部环境。

缺点

  • 编译生成的可执行文件会变大,如果静态库更新必须重新编译。
  • 多个APP使用同一个静态库,每个APP都会拷贝一份,浪费内存。

1.3.3: 动态库优缺点

动态库优缺点 优点

  • 减小编译生成的可执行文件(也可简单理解为APP)的体积。
  • 共享内容,节省资源。
  • 通过更新动态库,达到更新程序的目的。

缺点

  • 可执行文件不可以单独运行,必须依赖外部环境。

补充:

在其它大部分平台上,动态库都可以用于不同应用间共享,这就大大节省了内存。iOS平台 在iOS8之前,苹果不允许第三方框架使用动态方式加载,从iOS8开始允许开发者有条件地创建和使用动态框架,这种框架叫做Cocoa Touch Framework。虽然同样是动态框架,但是和系统framework不同,app中使用Cocoa Touch Framework制作的动态库在打包和提交APP时会被放到app main bundle的根目录中,运行在沙盒里,而不是系统中。也就是说,不同的app就算使用了同样的framework,但还是会有多份的框架被分别签名,打包和加载。不过iOS8上开放了App Extension功能,可以为一个应用创建插件,这样主app和插件之间共享动态库还是可行的。

苹果系统专属的framework是共享的(如UIKit), 但是我们自己使用Cocoa Touch Framework制作的动态库是放到app bundle中,运行在沙盒里的

1.3.4: 动静态库图解

DynamicStaticLibFlowchart.jpg

1.4: dyld简介

那么在程序启动时,这些库是怎么加载到内存中的呢?

是通过dyld动态链接器加载到内存中的。整个过程大概如下:

动态链接流程.jpeg

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。

本文将详细分析dyld加载流程。

2: dyld初探

既然dyld负责库的加载,那么加载完成之后肯定会进入程序的入口main函数,那么我们在main函数打上断点查看下调用栈。

image.png

可以看到在main函数之前只有libdyld.dylib库的start调用,下个start符号断点,发现并不会进入断点,说明在底层的符号根本不是start

+load方法比main函数先调用,在类文件里实现一个+load方法并打上断点查看调用栈。

image.png

根据调用栈显示+load方法的调用流程为:_dyld_start->dyldbootstrap::start->dyld::_main->dyld::initializeMainExecutable->ImageLoader::runInitializers->ImageLoader::processInitializers->ImageLoader::recursiveInitialization->dyld::notifySingle->load_images->+[ViewController load]

打开dyld源码,搜索_dyld_start,最终在dyldStartup.s文件找到了_dyld_start的汇编实现(也可直接查看真机汇编)。

静态汇编:

image.png

真机汇编: image.png

真机汇编和静态汇编都显示调用了dyldbootstrap::start函数。

dyldbootstrapc++的命名空间,start为其中的函数。搜索后发现dyldbootstrap::startdyldInitialization.cpp文件中,接下来结合源码分析dyldbootstrap::start+load方法和main函数的调用顺序,以及dyld是如何加载images的。

3: dyld源码分析

3.1: dyldbootstrap::start

// 函数所在文件:dyldInitialization.cpp
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
				const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

    // 告诉debug server dyld要开始启动了
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

    // 重定位dyld
    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;

    // 栈溢出保护
    __guard_setup(apple);

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

    _subsystem_init(apple);

    // 获取虚拟内存偏移
    uintptr_t appsSlide = appsMachHeader->getSlide();
    // 调用dyld::_main函数,再将其返回值传递给__dyld_start去调用真正的main()函数
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyldbootstrap::start主要做了一下几件事:

  • 告诉debug server``dyld要开始启动了。
  • 重定位dyld
  • 栈溢出保护。
  • dyld内运行所有C++初始化器。
  • 调用dyld::_main函数,再将其返回值传递给__dyld_start去调用真正的主程序入口main函数。

dyldbootstrap::start只是做了一些配置和初始化的工作,核心逻辑在dyld::_main函数中。

3.2: dyld::_main

dyld::_main函数是整个APP启动的关键函数,有800多行代码,这里只梳理整理流程,细节部分感兴趣的可以自己探索。

// 函数所在文件:dyld2.cpp
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
    //内核检测代码
    if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
        launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
    }
……

    /* ---第一步:设置运行环境 - begin--- */
    //主程序可执行文件 cdHash
    uint8_t mainExecutableCDHashBuffer[20];
    const uint8_t* mainExecutableCDHash = nullptr;
    if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
        unsigned bufferLenUsed;
        if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
            mainExecutableCDHash = mainExecutableCDHashBuffer;
    }
    //根据macho头文件配置CPU架构的信息,就是一些文件配置
    getHostInfo(mainExecutableMH, mainExecutableSlide);

……

    CRSetCrashLogMessage("dyld: launch started");
    //配置环境 将信息放入 gLinkContext 中( notifySingle函数 赋值在其中)
    setContext(mainExecutableMH, argc, argv, envp, apple);

……

    //根据环境变量 envp 配置进程是否受限制,AMFI相关(Apple Mobile File Integrity苹果移动文件保护)
    configureProcessRestrictions(mainExecutableMH, envp);

……

#if TARGET_OS_OSX
    if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
        pruneEnvironmentVariables(envp, &apple);
        // set again because envp and apple may have changed or moved
        //又设置一次上下文,在文件受限的时候可能更改了envp。
        setContext(mainExecutableMH, argc, argv, envp, apple);
    }
    else
#endif
    {
        //检测环境变量并设置默认值,这个时候还没有加载数据。
        checkEnvironmentVariables(envp);
        defaultUninitializedFallbackPaths(envp);
    }
……
    //打印环境变量,可以在"Scheme -> Arguments -> Environment Variables"中配置
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);

……
    /* ---第一步:设置运行环境 - end--- */
    
    
    /* ---第二步:加载共享缓存 - begin--- */
    //检查共享缓存是否可用,到了这里只读了主程序还没有加载主程序。iOS必须有共享缓存。
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
        if ( sSharedCacheOverrideDir)
            mapSharedCache(mainExecutableSlide);
#else
        //加载共享缓存方法
        mapSharedCache(mainExecutableSlide);
#endif
    /* ---第二步:加载共享缓存 - end--- */
……
    
……
    /* ---第三步:dyld3或dyld2加载 - begin--- */
#if !TARGET_OS_SIMULATOR
    //dyld3 ClosureMode模式,iOS11引入ClosureMode,iOS13后动态库和三方库都使用ClosureMode加载。
    if ( sClosureMode == ClosureMode::Off ) {
        // dyld2
        if ( gLinkContext.verboseWarnings )
            dyld::log("dyld: not using closures\n");
    } else {
        // dyld3
        // 启动模式 闭包模式 DYLD_LAUNCH_MODE_USING_CLOSURE
        sLaunchModeUsed = DYLD_LAUNCH_MODE_USING_CLOSURE;
        const dyld3::closure::LaunchClosure* mainClosure = nullptr;
        //主程序 info 和 Header
        dyld3::closure::LoadedFileInfo mainFileInfo;
        mainFileInfo.fileContent = mainExecutableMH;
        mainFileInfo.path = sExecPath;
……
        //第一次从共享缓存找闭包
        if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
            //先从共享缓存找实例闭包
            mainClosure = sSharedCacheLoadInfo.loadAddress->findClosure(sExecPath);
            if ( gLinkContext.verboseWarnings && (mainClosure != nullptr) )
                dyld::log("dyld: found closure %p (size=%lu) in dyld shared cache\n", mainClosure, mainClosure->size());
            if ( mainClosure != nullptr )
                //如果拿到设置状态
                sLaunchModeUsed |= DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
        }
……
        //拿到闭包 && 验证闭包,如果闭包失效
        if ( (mainClosure != nullptr) && !closureValid(mainClosure, mainFileInfo, mainExecutableCDHash, true, envp) ) {
            mainClosure = nullptr;
            //闭包失效设置状态
            sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
        }

……
        //判断mainClosure是否为空
        if ( (mainClosure == nullptr) && allowClosureRebuilds ) {
            // if forcing closures, and no closure in cache, or it is invalid, check for cached closure
            if ( !sForceInvalidSharedCacheClosureFormat )
                //缓存中找
                mainClosure = findCachedLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
            if ( mainClosure == nullptr ) {
                // if  no cached closure found, build new one
                //缓存中找不到则创建一个,一直拿 mainClosure 是为了拿他创建主程序。
                mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
                if ( mainClosure != nullptr )
                    //创建失败则设置状态
                    sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
            }
        }
……
        // 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;
            //启动主程序,mainClosure 相当于加载器
            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(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)
                    //主程序main函数,dyld的main执行完毕返回主程序的main
                    result = (uintptr_t)&fake_main;
                return result;
            }
            else {
                //失败报错
                if ( gLinkContext.verboseWarnings ) {
                    dyld::log("dyld: unable to use closure %p\n", mainClosure);
                }
                if ( !recoverable )
                    halt(diag.errorMessage());
            }
        }
    }
#endif // TARGET_OS_SIMULATOR
    // could not use closure info, launch old way

    //dyld2模式
    sLaunchModeUsed = 0;


    // install gdb notifier
    //两个回调地址放入stateToHandlers数组中
    stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
    stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
    // make initial allocations large enough that it is unlikely to need to be re-alloced
    //分配初始化空间,尽可能大一些保证后面够用。
    sImageRoots.reserve(16);
    sAddImageCallbacks.reserve(4);
    sRemoveImageCallbacks.reserve(4);
    sAddLoadImageCallbacks.reserve(4);
    sImageFilesNeedingTermination.reserve(16);
    sImageFilesNeedingDOFUnregistration.reserve(8);

……

    try {
        // add dyld itself to UUID list
        //dyld加入uuid列表
        addDyldImageToUUIDList();

#if SUPPORT_ACCELERATE_TABLES
……

        //主程序还没有rebase
        bool mainExcutableAlreadyRebased = false;
        if ( (sSharedCacheLoadInfo.loadAddress != nullptr) && !dylibsCanOverrideCache() && !sDisableAcceleratorTables && (sSharedCacheLoadInfo.loadAddress->header.accelerateInfoAddr != 0) ) {
            struct stat statBuf;
            if ( dyld3::stat(IPHONE_DYLD_SHARED_CACHE_DIR "no-dyld2-accelerator-tables", &statBuf) != 0 )
                sAllCacheImagesProxy = ImageLoaderMegaDylib::makeImageLoaderMegaDylib(&sSharedCacheLoadInfo.loadAddress->header, sSharedCacheLoadInfo.slide, mainExecutableMH, gLinkContext);
        }
//加载所有的可执行文件 image list,这里相当于是个标签。会循环。
reloadAllImages:
#endif
    /* ---第三步:dyld3或dyld2加载 - end--- */
……
    
    /* ---第四步:实例化主程序 - begin--- */
        //实例化主程序,加入到allImages(第一个靠dyld加载的image就是主程序)
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        //代码签名
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
    /* ---第四步:实例化主程序 - end--- */
……

#if defined(__x86_64__) && !TARGET_OS_SIMULATOR
        //设置加载动态库版本
        if (dyld::isTranslated()) {……}
#endif

        // Now that shared cache is loaded, setup an versioned dylib overrides
    #if SUPPORT_VERSIONED_PATHS
        //检查版本路径
        checkVersionedPaths();
    #endif
……
    /* ---第五步:加载插入动态库 - begin--- */
        //DYLD_INSERT_LIBRARIES 插入动态库
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            //遍历加载插入动态库
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
                loadInsertedDylib(*lib);
        }
        // -1为了排除主程序
        sInsertedDylibCount = sAllImages.size()-1;
        
    /* ---第五步:加载插入动态库 - end--- */

        // link main executable
    /* ---第六步:链接主程序 - begin--- */
        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();
    /* ---第六步:链接主程序 - end--- */
……
    /* ---第七步:链接动态库 - begin--- */
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                //i+1因为主程序,插入的image在主程序后面
                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);
                }
            }
        }
    /* ---第七步:链接动态库 - end--- */
……
    
        sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
……

        // Bind and notify for the inserted images now interposing has been registered
        if ( sInsertedDylibCount > 0 ) {
            //递归绑定
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                //绑定插入动态库
                image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
            }
        }
        
        // <rdar://problem/12186933> do weak binding only after all inserted images linked
        
        /* ---第八步:弱绑定主程序 - begin--- */
        //弱引用绑定主程序,所有镜像文件绑定完成后进行。
        sMainExecutable->weakBind(gLinkContext);
        gLinkContext.linkingMainExecutable = false;
        
        /* ---第八步:弱绑定主程序 - end--- */

        sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);

        CRSetCrashLogMessage("dyld: launch, running initializers");
    #if SUPPORT_OLD_CRT_INITIALIZATION
        // Old way is to run initializers via a callback from crt1.o
        if ( ! gRunInitializersOldWay ) 
            initializeMainExecutable(); 
    #else
    /* ---第九步:执行初始化 - begin--- */
        // run all initializers
        // 运行所有的初始化
        initializeMainExecutable(); 
    /* ---第九步:执行初始化 - end--- */
    #endif
……
    /* ---第十步:返回main函数 - begin--- */
        {
            // find entry point for main executable
            //找到主程序入口 LC_MAIN
            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;
    }
    //返回主程序
    return result;
    /* ---第十步:返回main函数 - end--- */
}

上面代码中比较重要的地方已经加了一些注释,方法读者查看。整个加载过程可细分为九步:

  • 第一步:设置运行环境。
  • 第二步:加载共享缓存。
  • 第三部:dyld2/dyld3ClosureMode闭包模式)加载程序。
  • 第四步:实例化主程序。
  • 第五步:加载插入动态库。
  • 第六步:链接主程序和动态库。
  • 第七步:弱绑定主程序。
  • 第八步:执行初始化。
  • 第九步:返回main函数。

备注:本文的image指动态库的镜像。

3.2.1: 设置运行环境

这一步主要是设置运行参数、环境变量等。代码在开始的时候调用getHostInfo函数获取当前程序架构,将入参mainExecutableMH赋值给了sMainExecutableMachHeader,这是一个macho_header结构体,表示的是当前主程序的Mach-O头部信息,加载器依据Mach-O头部信息就可以解析整个Mach-O文件信息。接着调用setContext()设置上下文信息,包括一些回调函数、参数、标志信息等。设置的回调函数都是dyld模块自身实现的,如loadLibrary()函数实际调用的是libraryLocator(),负责加载动态库。代码片断如下:

static void setContext(const macho_header* mainExecutableMH, int argc, const char* argv[], const char* envp[], const char* apple[])
{
    gLinkContext.loadLibrary = &libraryLocator;
    gLinkContext.terminationRecorder = &terminationRecorder;
    gLinkContext.flatExportFinder = &flatFindExportedSymbol;
    gLinkContext.coalescedExportFinder = &findCoalescedExportedSymbol;
    gLinkContext.getCoalescedImages = &getCoalescedImages;
    gLinkContext.undefinedHandler = &undefinedHandler;
    gLinkContext.getAllMappedRegions = &getMappedRegions;
    gLinkContext.bindingHandler = NULL;
    gLinkContext.notifySingle = &notifySingle;
    ...
}

在这个过程中有一些DYLD_开头的环境变量,比如:

    // 如果设置了DYLD_PRINT_OPTS则调用printOptions()打印参数
    if ( sEnv.DYLD_PRINT_OPTS )
    printOptions(argv);
    // 如果设置了DYLD_PRINT_ENV则调用printEnvironmentVariables()打印环境变量
    if ( sEnv.DYLD_PRINT_ENV ) 
    printEnvironmentVariables(envp);

其实,只要在Xcode中配置一下即可让这些环境变量生效,设置路径为Scheme->Arguments->Environment Variables,按下图所示添加环境变量并设置Value1

image.png

运行Xcode即可看到控制台打印的详细信息:

**opt[0] = "/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug/CategoryDemo"**
**__XCODE_BUILT_PRODUCTS_DIR_PATHS=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**MallocNanoZone=0**
**CA_DEBUG_TRANSACTIONS=0**
**COMMAND_MODE=unix2003**
**LOGNAME=weichunfang**
**USER=weichunfang**
**CA_ASSERT_MAIN_THREAD_TRANSACTIONS=0**
**HOME=/Users/weichunfang**
**PWD=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**DYLD_LIBRARY_PATH=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug:/usr/lib/system/introspection**
**__CF_USER_TEXT_ENCODING=0x1F5:0x19:0x34**
**LD_LIBRARY_PATH=/Applications/Xcode.app/Contents/Developer/../SharedFrameworks/**
**__XPC_DYLD_LIBRARY_PATH=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**SQLITE_ENABLE_THREAD_ASSERTIONS=1**
**DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/usr/lib/libBacktraceRecording.dylib:/Applications/Xcode.app/Contents/Developer/usr/lib/libMainThreadChecker.dylib:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Debugger/libViewDebuggerSupport.dylib**
**DYLD_PRINT_OPTS=1**
**DYLD_PRINT_ENV=1**
**METAL_DEVICE_WRAPPER_TYPE=1**
**METAL_DEBUG_ERROR_MODE=0**
**DYLD_FRAMEWORK_PATH=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**SECURITYSESSIONID=186a6**
**OS_ACTIVITY_DT_MODE=YES**
**SWIFTUI_VIEW_DEBUG=287**
**PATH=/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin**
**SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.mls7Pr51Bb/Listeners**
**__XPC_DYLD_FRAMEWORK_PATH=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**TMPDIR=/var/folders/f5/nnm3g2sd3pl8c6cz5bzpcxw80000gn/T/**
**XPC_FLAGS=0x0**
**SHELL=/bin/zsh**
**GPUProfilerEnabled=YES**
**XPC_SERVICE_NAME=com.apple.xpc.launchd.oneshot.0x10000001.Xcode**
**LaunchInstanceID=61EE7919-BC5B-4B07-8BAB-4505766DEAB9**
**NSUnbufferedIO=YES**

后面还有很多这样的DYLD_开头的环境变量,感兴趣的读者可以自行测试。

ASLR:image list0号位主程序地址。 image.png

3.2.2: 加载共享缓存

iOS必须要有共享缓存,共享缓存中存储的都是系统级别的动态库,比如UIKitCoreFoundation等,非系统级别的动态库不会放到共享缓存中,只运行在程序的沙盒中。

image.png

  • checkSharedRegionDisable函数里很明确的提示了iOS必须需要共享缓存。

接下来调用mapSharedCache函数加载共享缓存,而mapSharedCache函数里实则调用了loadDyldCache函数。

static void mapSharedCache(uintptr_t mainExecutableSlide)
{
    ...
    // 调用loadDyldCache
    loadDyldCache(opts, &sSharedCacheLoadInfo);
    ...
}
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
    // forcePrivate == YES,就是强制私有
    if ( options.forcePrivate ) {
        // 此时就不会加载共享缓存,而是把你需要的系统库仅缓存到此进程中
        return mapCachePrivate(options, results);
    }
    else {
        // fast path: when cache is already mapped into shared region
        bool hasError = false;
        // 共享缓存已加载,不做任何处理
        if ( reuseExistingCache(options, results) ) {
            hasError = (results->errorMessage != nullptr);
        } else {
            // slow path: this is first process to load cache
            // 当前进程首次加载共享缓存
            hasError = mapCacheSystemWide(options, results);
        }
        return hasError;
    }
#endif
}

loadDyldCache的三种情况:

  • 当强制私有时(forcePrivate == YES),仅加载到当前进程,不会加载到共享缓存中,调用mapCachePrivate
  • 共享缓存已加载,不进行任何处理。
  • 当前进程首次加载共享缓存,调用mapCacheSystemWide

mapCachePrivatemapCacheSystemWide里面就是具体的共享缓存解析逻辑,感兴趣的读者可以详细分析。

3.3.3: dyld3dyld2ClosureMode闭包模式)加载程序

iOS11引入dyld3闭包模式,以回调的方式加载,闭包模式加载速度更快,效率更高。iOS13后动态库和三方库都使ClosureMode加载。

  • dyld3:

    • 使用mainClosure来加载。
    • 找到/创建mainClosure后,通过launchWithClosure启动主程序,启动失败后会有重新创建mainClosure重新启动的逻辑。成功后返回result(主程序入口main函数)。launchWithClosure中的逻辑和dyld2启动主程序逻辑基本相同。
  • dyld2:启动主程序

    • 实例化主程序instantiateFromLoadedImagesMainExecutable 是通过instantiateFromLoadedImage赋值的,也就是把主程序加入allImages中。
    • 插入&加载动态库 loadInsertedDylib。加载在loadInsertedDylib中调用load(主程序和动态库都会添加到allImagesloadAllImages
    • 链接主程序和链接插入动态库(link,主程序链接在前)。在这个过程中记录了dyld加载的时长。可以通过配置环境变量打印出来。
    • 绑定符号(非懒加载、弱符号),懒加载在调用时绑定。
    • 初始化主程序initializeMainExecutable,这个时候还没有执行到主程序中的代码。
    • 找到主程序入口 LC_MAIN(main函数),然后返回主程序。

3.3.4: 实例化主程序

static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
    //实例化image(镜像)
    ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
    //将image添加到all Images中
    addImage(image);
    return (ImageLoaderMachO*)image;
//  throw "main executable not a known format";
}
  • 传入主程序的HeaderASLRpath实例化主程序生成image(镜像)。
  • image加入all images中(主程序image是第一个添加到数组中的)。

实例化主程序调用的是ImageLoaderMachO::instantiateMainExecutable函数。

// create image for main executable
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;
    //获取Load Commands
    sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
    // instantiate concrete class based on content of load commands
    //根据 compressed 确定用哪个子类进行加载image,ImageLoader是个抽象类,根据值选择对应的子类实例化image。
    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生成相关信息,比如compressed

  • 根据 compressed 确定用哪个子类进行加载imageImageLoader是个抽象类,根据值选择对应的子类实例化主程序。

sniffLoadCommands函数。

// 篇幅原因,仅截取部分代码

void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
                                            unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
                                            const linkedit_data_command** codeSigCmd,
                                            const encryption_info_command** encryptCmd)
{
    //根据LC_DYLIB_INFO 和  LC_DYLD_INFO_ONLY 来获取的
    *compressed = false;
    //segment数量
    *segCount = 0;
    //lib数量
    *libCount = 0;
    //代码签名和加密
    *codeSigCmd = NULL;
    *encryptCmd = NULL;
        ……
    // fSegmentsArrayCount is only 8-bits
    // segCount 最多 256 个
    if ( *segCount > 255 )
        dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);

    // fSegmentsArrayCount is only 8-bits
    // libCount最多 4096 个
    if ( *libCount > 4095 )
        dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
    // 确保依赖libsystem库
    if ( needsAddedLibSystemDepency(*libCount, mh) )
        *libCount = 1;

    // dylibs that use LC_DYLD_CHAINED_FIXUPS have that load command removed when put in the dyld cache
    if ( !*compressed && (mh->flags & MH_DYLIB_IN_CACHE) )
        *compressed = true;
}
  • compressed是根据LC_DYLIB_INFOLC_DYLD_INFO_ONLY来获取的。
  • segCount最多256个。
  • libCount最多4096个。
  • 确保依赖libsystem库。

大家可能对segmentcommand以及macho头文件还是有点模糊,通过MachOView工具认识下可执行文件:

image.png

图中很清晰明了macho头文件就是架构信息以及文件类型等。macho文件主要是3块内容HeaderCommodsData

3.3.5: 加载插入动态库

static void loadInsertedDylib(const char* path)
{
    unsigned cacheIndex;
    try {
    ……
        //调用load,加载动态库的真正函数
        load(path, context, cacheIndex);
    }
    ……
}
  • 根据上下文初始化配置调用load函数加载动态库。

3.3.6: 链接主程序和动态库

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
        image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
    }
}

link中调用了ImageLoader::link方法,ImageLoader负责加载image文件(主程序,动态库)每个image对应一个ImageLoader类的实例。

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{   
    // clear error strings
    (*context.setErrorStrings)(0, NULL, NULL, NULL);
    //起始时间。用于记录时间间隔
    uint64_t t0 = mach_absolute_time();
    //递归加载主程序依赖的库,完成之后发通知。
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
    context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
……
    uint64_t t1 = mach_absolute_time();
    context.clearAllDepths();
    this->updateDepth(context.imageCount());

    __block uint64_t t2, t3, t4, t5;
    {
        dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
        t2 = mach_absolute_time();
        //递归重定位,修正ASLR
        this->recursiveRebaseWithAccounting(context);
        context.notifyBatch(dyld_image_state_rebased, false);

        t3 = mach_absolute_time();
        if ( !context.linkingMainExecutable )
            //绑定NoLazy符号
            this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);

        t4 = mach_absolute_time();
        if ( !context.linkingMainExecutable )
            //绑定弱符号
            this->weakBind(context);
        t5 = mach_absolute_time();
    }

    // interpose any dynamically loaded images
    if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
        dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
        //递归应用插入的动态库
        this->recursiveApplyInterposing(context);
    }

    // now that all fixups are done, make __DATA_CONST segments read-only
    if ( !context.linkingMainExecutable )
        this->recursiveMakeDataReadOnly(context);

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

    if ( context.registerDOFs != NULL ) {
        std::vector<DOFInfo> dofs;
        this->recursiveGetDOFSections(context, dofs);
        //注册
        context.registerDOFs(dofs);
    }
    //计算结束时间.
    uint64_t t7 = mach_absolute_time();

    // clear error strings
    //配置环境变量,就可以看到dyld应用加载的时长。
    (*context.setErrorStrings)(0, NULL, NULL, NULL);
    fgTotalLoadLibrariesTime += t1 - t0;
    fgTotalRebaseTime += t3 - t2;
    fgTotalBindTime += t4 - t3;
    fgTotalWeakBindTime += t5 - t4;
    fgTotalDOF += t7 - t6;
    
    // done with initial dylib loads
    fgNextPIEDylibAddress = 0;
}
  • 递归加载所有的动态库。
  • 递归重定位,修正ASLR。
  • 递归绑定非懒加载。
  • 绑定弱符号。
  • 注册。
  • 记录时间,可以通过配置看到dyld应用加载时长

recursiveLoadLibraries方法是递归加载动态库,探究下具体怎么递归加载的(仅截取大致流程)。

void ImageLoader::recursiveLoadLibraries(const LinkContext& context, bool 
preflightOnly, const RPathChain& loaderRPaths, const char* loadPath){
    ...
    // get list of libraries this image needs
    //获取当前的image依赖的动态库
    DependentLibraryInfo libraryInfos[fLibraryCount]; 
    this->doGetDependentLibraries(libraryInfos);

    // get list of rpaths that this image adds
    //获取当前的image依赖的动态库的文件路径
    std::vector<const char*> rpathsFromThisImage;
    this->getRPaths(context, rpathsFromThisImage);
    const RPathChain thisRPaths(&loaderRPaths, &rpathsFromThisImage);

    // 加载image依赖的动态库
    for(unsigned int i=0; i < fLibraryCount; ++i){
      ...
      dependentLib = context.loadLibrary(requiredLibInfo.name, true, this->getPath(),
      &thisRPaths, cacheIndex);
      // 保存加载的动态库
      setLibImage(i, dependentLib, depLibReExported, requiredLibInfo.upward);	 
      ...
    `}`

    //告诉image依赖的动态库去加载各自需要的动态库
    for(unsigned int i=0; i < libraryCount(); ++i) {
            ImageLoader* dependentImage = libImage(i);
            if ( dependentImage != NULL ) {
                 dependentImage->recursiveLoadLibraries(context, preflightOnly,
                 thisRPaths, libraryInfos[i].name);
            }
    }
}
  • 获取当前image(镜像文件)依赖的动态库和动态库的文件路径。
  • 加载当前image依赖的动态库,并保持起来。
  • 告诉当前image依赖的动态库去加载各自需要的动态库。

链接动态库和链接主程序的逻辑基本一样,注意下循环取image文件的时候是从1开始,因为第0个位置是主程序,随便运行一个程序,lldb输出image list验证下。

image.png

  • lldb输出image list第一个就是主程序了。

3.3.7: 弱绑定主程序

大家可能注意到了,链接主程序的时候里面有弱符号绑定。但是在链接主程序的时候linkingMainExecutable = true,所以link里面的弱绑定在主程序时是不调用的,等动态库的都进行了弱绑定,最后对主程序进行弱绑定。

3.3.8: 执行初始化

initializeMainExecutable函数流程复杂,下文单独解析。

3.3.9: 返回main函数

获取主程序的入口main函数,再将其返回传递给__dyld_start去调用。

4: 初始化流程解析

初始化流程内容复杂,本节单独探索。

4.1: 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 ) {
        //从1开始到最后。(第0个为主程序)
        for(size_t i=1; i < rootCount; ++i) {
            //image初始化,调用 +load 和 构造函数
            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]);
}
  • 初始化images,下标从1开始,然后再初始化主程序(下标0runInitializers
  • 可以配置环境变量DYLD_PRINT_STATISTICSDYLD_PRINT_STATISTICS_DETAILS打印相关信息。

4.2: ImageLoader::runInitializers函数解析

从上面可以看出initializeMainExecutable函数里主程序和image初始化调用的都是ImageLoader::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);
}
  • up.count值设置为1然后调用ImageLoader::processInitializers函数。

4.3: ImageLoader::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;
    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.
    // 为了保证所有的向上依赖关系都初始化,再次把没有初始化的image去初始化
    if ( ups.count > 0 )
        processInitializers(context, thisThread, timingInfo, ups);
}
  • 调用了ImageLoader::recursiveInitialization函数。

4.4: ImageLoader::recursiveInitialization函数解析

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                          InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
……
    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 ) {
……
                    else if ( dependentImage->fDepth >= fDepth ) {
                        //依赖文件递归初始化
                        dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
                    }
                }
            }       
……
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            //这里调用传递的状态是dyld_image_state_dependents_initialized,image传递的是自己。也就是最后调用了自己的+load。从libobjc.A.dylib开始调用。
            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
            
            // initialize this image
            //初始化镜像文件,调用c++构造函数。libSystem的libSystem_initializer就是在这里调用的。会调用到objc_init中。_dyld_objc_notify_register 中会调用自身的+load方法,然后c++构造函数。
            //1.调用libSystem_initializer->objc_init 注册回调。
            //2._dyld_objc_notify_register中调用 map_images,load_images,这里是首先初始化一些系统库,调用系统库的load_images。比如libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib。
            //3.自身的c++构造函数
            bool hasInitializers = this->doInitialization(context);

            // let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            //这里调用不到+load方法。 notifySingle内部fState==dyld_image_state_dependents_initialized 才调用+load。
            context.notifySingle(dyld_image_state_initialized, this, NULL);
……
        }
……
    }
    recursiveSpinUnLock();
}
  • 需要初始化的动态库image是从libImage()中获取,而libImage()的数据是在链接动态库的时recursiveLoadLibraries中的setLibImage保存的image
  • 整个过程是一个递归的过程,先初始化最底层的依赖库,再逐步初始化到自己。
  • 调用notifySingle最终调用到了objc中所有的+load方法。这里第一个notifySingle调用的是+load方法,第二个notifySingle由于参数是dyld_image_state_initialized不会调用到+load方法。这里的dyld_image_state_dependents_initialized意思是依赖文件初始化完毕了,可以初始化自己了。
  • 调用doInitialization最终调用了c++的系统构造函数。先调用的是libSystem_initializer -> objc_init进行注册回调。在回调中调用了map_imagesload_images(+load)。这里的load_images是调用一些加载一些系统库,比如:libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib

4.5: notifySingle函数解析

notifySingle是一个函数指针,在setContext函数里赋值。

image.png

继续查看notifySingle函数的实现。

image.png

  • notifySingle中找不到load_images的调用(第2dyld初探中探索过,notifySingle之后调用的就是load_images,后续探索)。
  • notifySingle函数里当state == dyld_image_state_dependents_initialized(依赖库初始化完成)之后执行了一个函数回调sNotifyObjCInit

全局搜索sNotifyObjCInit函数,发现是在registerObjCNotifiers函数里赋值的。

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
    // record functions to call
    //第一个参数 map_images
    sNotifyObjCMapped   = mapped;
    //第二个参数 load_images
    sNotifyObjCInit     = init;
    //第三个参数 unmap_image
    sNotifyObjCUnmapped = unmapped;

    // call 'mapped' function with all images mapped so far
    try {
        //赋值后马上回调 map_images
        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)
    // 在所有已初始化的映像调用“init”函数
    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);
            //调用一些系统库的 load_images。
            (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
        }
    }
}
  • 记录要调用的函数,sNotifyObjCInit函数赋值来自于第二个参数。
  • 赋值后就调用了notifyBatchPartial函数(内部调用了sNotifyObjCMapped函数)。
  • 循环调用load_images,这里调用的是依赖的系统库libdispatch.dylib,libsystem_blocks.dylib,libsystem_dnssd.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib

全局搜索registerObjCNotifiers函数,发现是在_dyld_objc_notify_register函数里调用的。

//_objc_init中调用的。
//单个镜像文件的加载来到了这里->_dyld_objc_notify_register,打符号断点查看被objc-os.mm中 _objc_init 调用。
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函数,发现在dyld源码中没有调用这个函数的地方。

为了继续探索,在项目工程中添加_dyld_objc_notify_register符号断点,查看函数调用栈。

image.png

根据函数调用栈可知调用顺序为:dyld-ImageLoaderMachO::doInitialization->dyld-ImageLoaderMachO::doModInitFunctions->libSystem.B.dylib-libSystem_initializer->libdispatch.dylib-libdispatch_init->libdispatch.dylib-_os_object_init->libobjc.A.dylib-_objc_init->libdyld.dylib-_dyld_objc_notify_register

  • libSystem_initializer函数在libSystem.B.dylib系统库中。
  • libdispatch_init函数和_os_object_init函数在libdispatch.dylib系统库中。
  • _objc_init函数在libobjc.A.dylib系统库中,也就是我们最熟悉的objc源码。

查看objc源码里_objc_init函数的实现。

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

    //_objc_init 调用dyldAPIs.cpp 中_dyld_objc_notify_register,第二个参数是load_images
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}
  • _objc_init函数调用了_dyld_objc_notify_register函数。
  • 第一个参数是map_images,赋值给sNotifyObjCMapped
  • 第二个参数是load_images,赋值给sNotifyObjCInit
  • 第三个参数是unmap_image,赋值给sNotifyObjCUnmapped

这三个参数将在后面详细介绍是如何与dyld进行交互的。

4.6: ImageLoaderMachO::doInitialization函数解析

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

    // mach-o has -init and static initializers
    // 调用image里面的一些初始化方法
    doImageInit(context);
    // 调用c++构造函数(libSystem_initializer也属于c++构造函数)
    doModInitFunctions(context);
    
    CRSetCrashLogMessage2(NULL);
    
    return (fHasDashInit || fHasInitializers);
}
  • doImageInit函数调用Mach-O里面的一些初始化方法。
  • doModInitFunctions函数调用libSystem_initializer在内的c++构造函数(__attribute__((constructor))修饰的c函数)。

image.png

  • 会对__mod_init_funcs section里的c++构造函数进行验证,然后调用。
  • 在所有库加载前必须先加载libSystem系统库,调用libSystem_initializer函数(libSystem_initializer函数是__attribute__((constructor))修饰的c++构造函数)。

Mach-O验证c++构造函数分析

在项目中加上一些c++构造函数然后查看Mach-O文件。

__attribute__((constructor)) void funcA() {
    printf("\n ---funcA--- \n");
}

__attribute__((constructor)) void funcB() {
    printf("\n ---funcB--- \n");
}

image.png

5:反推objcdyld的关联

从上面的符号断点函数调用栈看到了在_dyld_objc_notify_register函数和doModInitFunctions函数之间还有非dyld库的函数调用,现在就来反推梳理下这个流程。

在最了解的objc库里的_objc_init函数里打上断点,查看函数调用栈。

image.png

对于doModInitFunctions函数和_objc_init函数之间的流程是未知的,最好的方式就是从_objc_init函数开始反推调用流程。

5.1: _os_object_init函数解析

从上面的函数调用栈可知_objc_init函数是被libdispatch.dylib中的_os_object_init函数调用的。下载libdispatch最新的libdispatch-1271.120.2源码,搜索_os_object_init函数。

void
_os_object_init(void)
{
    //_objc_init调用
    _objc_init();
    Block_callbacks_RR callbacks = {
        sizeof(Block_callbacks_RR),
        (void (*)(const void *))&objc_retain,
        (void (*)(const void *))&objc_release,
        (void (*)(const void *))&_os_objc_destructInstance
    };
    _Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
    const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
    if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
    v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
    if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
    v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
    if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}
  • 发现_objc_init函数确实是_os_object_init函数调用的。

5.2: libdispatch_init函数解析

同时libdispatch_init函数也调用了_os_object_init函数。

image.png

  • 线程相关处理。
  • 设定主线程之后调用了_os_object_init等一系列函数。

5.3: libSystem_initializer函数解析

libdispatch_init函数是被libSystem.dylib库中的libSystem_initializer函数调用的。下载libSystem最新的Libsystem-1292.120.1源码,搜索libSystem_initializer函数。

image.png

  • 其中调用了libdispatch_init函数,同样还调用了__malloc_init_dyld_initializer以及_libtrace_init等函数。

5.4: ImageLoaderMachO::doModInitFunctions函数解析

libSystem_initializer函数是dyld中的ImageLoaderMachO::doModInitFunctions函数调用的,这样整个流程就连起来了。

ImageLoaderMachO::doModInitFunctions中发现了如下代码:

image.png

  • libSystem库必须第一个被初始化。这也能被理解,因为要初始化dispatch以及objc。其它image都依赖它。
  • 获取c++构造函数,然后调用。

ImageLoaderMachO::doModInitFunctions函数里并没有libSystem_initializer函数的明确调用,但是通过断点读取寄存器的值确实可以读取到。

image.png

前面已经分析过了doModInitFunctions中是对c++构造函数的调用。libSystem_initializer正好是c++构造函数:

image.png

这样整个调用流程就连起来了。只不过libSystem_initializer这个c++构造函数被先调用。

6: dyld注册objc回调简单分析

上面的分析在_objc_init函数中调用了_dyld_objc_notify_register函数进行回调注册,然后调用到dyld::registerObjCNotifiers函数中有如下赋值:

    //第一个参数 map_images
    sNotifyObjCMapped   = mapped;
    //第二个参数 load_images
    sNotifyObjCInit     = init;
    //第三个参数 unmap_image
    sNotifyObjCUnmapped = unmapped;

下面就来详细分析这三个回调的逻辑。

6.1: sNotifyObjCMapped(map_images)

sNotifyObjCMappeddyld中的调用只在notifyBatchPartial函数中:

image.png

notifyBatchPartial函数的调用则在notifyBatch函数、registerImageStateBatchChangeHandler函数以及registerObjCNotifiers函数中。根据前面分析的流程是在registerObjCNotifiers注册回调之后就在里面调用了。

image.png

objc源码map_images函数中添加断点:

image.png

通过函数调用栈也可以验证在注册回调之后马上就调用了map_images函数。

map_images函数中直接加锁调用了map_images_nolock函数,其中进行了类的加载相关的操作,后续单独发文分析。

6.2: sNotifyObjCInit(load_images)

sNotifyObjCInitdyld中的调用分为以下情况:

  1. notifySingleFromCache中。
  2. notifySingle中。
  3. registerObjCNotifiersnotifySingleFromCachenotifySingle逻辑基本相同,无非就是有没有缓存的区别。 registerObjCNotifiers是在注册回调函数的时候直接进行的回调。

objc源码load_images函数中添加断点:

image.png

通过函数调用栈也可以验证系统的基础库在注册回调之后马上就调用了load_images函数。

objc库开始走的是notifySingle回调逻辑:

image.png

6.2.1: load_images函数

sNotifyObjCInit其实就是load_images,它的实现如下:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        //加载所有分类
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        //准备所有load方法
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    //调用 + load 方法
    call_load_methods();
}
  • 加载所有分类。
  • 准备所有load方法。
  • 调用call_load_methods函数。

6.2.2: prepare_load_methods函数

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t const *classlist = _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 准备主类的load方法
        schedule_class_load(remapClass(classlist[i]));
    }
    
    // 获取非懒加载的分类列表
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue// category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        // 迫使主类实现(如果没实现的话)
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        // 准备分类的load方法
        add_category_to_loadable_list(cat);
    }
}
  • 准备主类的load方法。
  • 准备分类的load方法。

6.2.3: schedule_class_load函数

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize
    
    if (cls->data()->flags & RW_LOADED) return;
    
    // Ensure superclass-first ordering
    // 递归准备类的load方法,直到父类nil
    schedule_class_load(cls->getSuperclass());
    
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
  • 递归准备类的load方法,直到父类nil。

6.2.4: add_class_to_loadable_listadd_category_to_loadable_list函数

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();
    // 主类获取load方法
    method = cls->getLoadMethod();
    if (!method) return// Don't bother if cls has no +load method

    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    // 容量不足,扩容,首次为16字节
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    // loadable_classes里存的是struct loadable_class,里面有2个成员变量,cls和method
    // 存一个,已用容量也就是下标++
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    // 分类获取load方法
    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    // 容量不足,扩容,首次为16字节
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }
    // loadable_categories里存的是struct loadable_category,里面有2个成员变量,cat和method
    // 存一个,已用容量也就是下标++
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};
  • 获取load方法。
  • 容量不足,就扩容,首次为16字节,后续为现有容量 * 2 + 16字节。
  • 将对应的数据添加进loadable_classesloadable_categories中。

6.2.5: objc_class::getLoadMethod_category_getLoadMethod函数

IMP 
objc_class::getLoadMethod()
{
    runtimeLock.assertLocked();

    const method_list_t *mlist;

    ASSERT(isRealized());
    ASSERT(ISA()->isRealized());
    ASSERT(!isMetaClass());
    ASSERT(ISA()->isMetaClass());

    // 获取类方法列表
    mlist = ISA()->data()->ro()->baseMethods();
    if (mlist) {
        // 遍历所有方法,通过字符串比对找到load方法
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            // 匹配load
            if (0 == strcmp(name, "load")) {
                return meth.imp(false);
            }
        }
    }
    
    return nil;
}
IMP 
_category_getLoadMethod(Category cat)
{
    runtimeLock.assertLocked();

    const method_list_t *mlist;
    // 获取分类的类方法列表
    mlist = cat->classMethods;
    if (mlist) {
        // 遍历分类类方法列表,通过字符串比对找到load方法
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            // 匹配load
            if (0 == strcmp(name, "load")) {
                return meth.imp(false);
            }
        }
    }

    return nil;
}
  • 通过字符串比对获取load方法。

6.2.6: call_load_methods函数

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();
    // 循环调用load方法,load方法在这一刻被调用
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            // 调用每个类的load方法
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        // 调用分类load,这里也就说明分类的load在所有类的load方法调用后才调用(针对image而言)
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}
  • 调用call_class_loads加载类的+ load方法。
  • 接着调用call_category_loads加载分类的+ load方法。这里也就说明分类的load在所有类的load方法调用后才调用(针对image而言)。

到这里就调用了+ load方法,这也就是+ load方法在main函数之前被调用的原因。

6.2.7: call_class_loads函数

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    //清空值
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        //从classes中获取method
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        //调用load
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}
  • loadable_classes中循环取到load方法进行调用。

6.2.7: call_category_loads函数

static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        //从cats中取出load
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;
        }
    }
……
}
  • 分类load的调用也是从loadable_categories循环取load方法进行调用。分类中内部处理逻辑更多一些。

所以在调用完+ load方法以及c++构造函数才进行主程序入口main函数的调用。可以通过汇编断点验证:

image.png

这样就和开头的时候对应上了。那么如果修改main函数的名称,编译的时候就报错了。

根据以上分析可以看到dyld是按image list顺序从第1image调用runInitializers(可以看做是以image分组)。再调用下一个imagerunInitializers最后再调用主程序(下标为0)的runInitializers。在runInitializers内部先调用所有类的+load,再调用所有分类的+ load,最后调用c++的构造函数。

objc中调用load方法,dyld中调用doModInitFunctions函数调用c++构造函数。

6.3: sNotifyObjCUnmapped(unmap_image)

sNotifyObjCUnmappeddyld中只有removeImage函数进行了调用:

image.png

removeImage函数被checkandAddImagegarbageCollectImages_dyld_link_moduleNSUnLinkModule函数调用。

  • checkandAddImage:检查此加载的image与任何现有image的安装路径,相同就直接返回,不同就添加。 garbageCollectImages:在link等其它异常以及回收的时候调用。 _dyld_link_module:旧版本兼容。 NSUnLinkModule:断开链接时调用。

6.3.1: unmap_image中调用了unmap_image_nolock,核心代码如下:

void
unmap_image_nolock(const struct mach_header *mh)
{
 ……
    header_info *hi; 
 ……
    //释放类,分类相关资源。
    _unload_image(hi);

    // Remove header_info from header list
    //移除remove Header
    removeHeader(hi);
    free(hi);
}
  • 卸载、释放类、分类相关资源。
  • 移除Header信息。

7. dyld3闭包模式分析

7.1: 分析引入

关于闭包模式在开启闭包模式的情况下就直接return了,所以核心逻辑就在launchWithClosure中了:

static bool launchWithClosure(const dyld3::closure::LaunchClosure* mainClosure,
                              const DyldSharedCache* dyldCache,
                              const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide,
                              int argc, const char* argv[], const char* envp[], const char* apple[], Diagnostics& diag,
                              uintptr_t* entry, uintptr_t* startGlue, bool* closureOutOfDate, bool* recoverable)
{
    ……
    // run initializers
    CRSetCrashLogMessage("dyld3: launch, running initializers");
    libDyldEntry->runInitialzersBottomUp((mach_header*)mainExecutableMH);
    //dyld::log("returned from runInitialzersBottomUp()\n");
    ……
}

launchWithClosure中发现了runInitialzersBottomUp的调用:

void AllImages::runInitialzersBottomUp(const closure::Image* topImage)
{
    // walk closure specified initializer list, already ordered bottom up
    topImage->forEachImageToInitBefore(^(closure::ImageNum imageToInit, bool& stop) {
        // get copy of LoadedImage about imageToInit, but don't keep reference into _loadedImages, because it may move if initialzers call dlopen()
        uint32_t    indexHint = 0;
        LoadedImage loadedImageCopy = findImageNum(imageToInit, indexHint);
        // skip if the image is already inited, or in process of being inited (dependency cycle)
        if ( (loadedImageCopy.state() == LoadedImage::State::fixedUp) && swapImageState(imageToInit, indexHint, LoadedImage::State::fixedUp, LoadedImage::State::beingInited) ) {
            // tell objc to run any +load methods in image
            if ( (_objcNotifyInit != nullptr) && loadedImageCopy.image()->mayHavePlusLoads() ) {
                dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)loadedImageCopy.loadedAddress(), 0, 0);
                const char* path = imagePath(loadedImageCopy.image());
                log_notifications("dyld: objc-init-notifier called with mh=%p, path=%s\n", loadedImageCopy.loadedAddress(), path);
                // +load
                (*_objcNotifyInit)(path, loadedImageCopy.loadedAddress());
            }

            // run all initializers in image
            // c++构造函数
            runAllInitializersInImage(loadedImageCopy.image(), loadedImageCopy.loadedAddress());

            // advance state to inited
            swapImageState(imageToInit, indexHint, LoadedImage::State::beingInited, LoadedImage::State::inited);
        }
    });
}
  • _objcNotifyInit最终调用到了+ load方法。
  • runAllInitializersInImage调用c++构造函数。
void AllImages::runAllInitializersInImage(const closure::Image* image, const MachOLoaded* ml)
{
    image->forEachInitializer(ml, ^(const void* func) {
        Initializer initFunc = (Initializer)func;
#if __has_feature(ptrauth_calls)
        initFunc = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)initFunc, 0, 0);
#endif
        {
            ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)ml, (uint64_t)func, 0);
            //c++构造函数
            initFunc(NXArgc, NXArgv, environ, appleParams, _programVars);

        }
        log_initializers("dyld: called initialzer %p in %s\n", initFunc, image->path());
    });
}

真机添加_dyld_objc_notify_register符号断点调试,发现_dyld_objc_notify_register函数会先尝试调用dyld3::_dyld_objc_notify_register,如果条件不满足就查找_dyld_objc_notify_register函数调用

image.png image.png

查看相关源码:

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    if ( gUseDyld3 )
        // dyld3,使用这里注册回调
        return dyld3::_dyld_objc_notify_register(mapped, init, unmapped);

    DYLD_LOCK_THIS_BLOCK;
    typedef bool (*funcType)(_dyld_objc_notify_mapped, _dyld_objc_notify_init, _dyld_objc_notify_unmapped);
    static funcType __ptrauth_dyld_function_ptr p = NULL;

    if(p == NULL)
        // 查找_dyld_objc_notify_register函数
        dyld_func_lookup_and_resign("__dyld_objc_notify_register", &p);
    // 调用_dyld_objc_notify_register函数
    p(mapped, init, unmapped);
}
  • gUseDyld3不为NULL就走dyld3的流程,否则就走dyld2的流程,真机调试的结果是走了dyld2的流程,现在来分析下dyld3的流程。

7.2: dyld3流程分析

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)

{
    log_apis("_dyld_objc_notify_register(%p, %p, %p)\n", mapped, init, unmapped);

    gAllImages.setObjCNotifiers(mapped, init, unmapped);
}
  • 调用gAllImages.setObjCNotifiers函数注册回调。
void AllImages::setObjCNotifiers(_dyld_objc_notify_mapped map, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmap)

{
    // dyld3注册的三个回调函数指针与dyld2不同
    _objcNotifyMapped   = map;
    _objcNotifyInit     = init;
    _objcNotifyUnmapped = unmap;

    // We couldn't initialize the objc optimized closure data in init() as that needs malloc but runs before malloc initializes.
    // So lets grab the data now and set it up
    
    ...
    
    if ( !mhs.empty() ) {
        // objc map(映射)的回调
        (*map)((uint32_t)mhs.count(), &paths[0], &mhs[0]);
        if ( log_notifications("dyld: objc-mapped-notifier called with %ld images:\n", mhs.count()) ) {
            for (uintptr_t i=0; i < mhs.count(); ++i) {
                log_notifications("dyld:  objc-mapped: %p %s\n",  mhs[i], paths[i]);
            }
        }
    }
}
  • 复制objc的三个回调,dyld3注册的三个回调函数指针与dyld2不同。
  • 进行objc map(映射)的回调,即调用_objcNotifyMapped回调。

7.2.1 _objcNotifyMapped(map_images)

_objcNotifyMapped函数是被AllImages::runImageCallbacks函数调用的。

image.png

AllImages::runImageCallbacks函数是被AllImages::applyInitialImages函数和AllImages::loadImage函数调用的。

AllImages::applyInitialImages函数: image.png

AllImages::applyInitialImages函数是被_dyld_initializer函数调用的。 image.png

  • 5.3小节知道_dyld_initializer函数是被libSystem.dylib库中的libSystem_initializer函数调用的。而由于_dyld_initializer是在libdispatch_init之前调用的,所以这个时候应该还没有注册回调。

AllImages::loadImage函数:

image.png

  • 调用AllImages::runImageCallbacks函数。
  • 调用AllImages::runInitialzersBottomUp函数,间接调用到_objcNotifyInit函数。

AllImages::loadImage函数是被AllImages::dlopen函数调用的。

image.png

  • 调用AllImages::loadImage函数。
  • 调用AllImages::runInitialzersBottomUp函数,间接调用到_objcNotifyInit函数。

7.2.2: _objcNotifyInit(load_images)

_objcNotifyInit函数是被AllImages::runInitialzersBottomUp函数调用的。

image.png

继续查找AllImages::runInitialzersBottomUp函数的调用者。

image.png

  • 结合7.17.2.1小节得出AllImages::runInitialzersBottomUp函数是被launchWithClosure函数、AllImages::loadImage函数和AllImages::dlopen函数调用的。

7.2.3: _objcNotifyUnmapped(unmap_image)

_objcNotifyUnmapped函数是被AllImages::removeImages函数调用的。

image.png

AllImages::removeImages函数是被AllImages::garbageCollectImages函数调用的。

image.png

AllImages::garbageCollectImages函数是被AllImages::decRefCount函数调用的。

image.png

AllImages::decRefCount函数调用分两种情况:

macOS情况下是被dlclose函数调用的。

image.png

macOS情况下是被NSUnLinkModule函数调用的。

image.png

由于真机和模拟器以及Mac都没有办法进入闭包模式调试验证。并且闭包模式代码逻辑可读性比较差,所以这里只是根据源码得出的结论,不一定成立。

7.3: dyld3闭包模式流程图

dyld3闭包模式流程图.jpg

8: dyld简介

启动时间(Startup Time):main函数执行之前所用的时间。
启动收尾(Lacunch Closure):启动应用程序必须的所有信息。

dyld发展到如今已经有3个大版本了,接下来将对dyld的演进过程做简单总结

8.1: dyld 1.0 (1996–2004)

  • 包含在NeXTStep 3.3中一起发布,在这之前NeXT使用静态二进制数据。
  • dyld1的历史早于标准化POSIX dlopen()调用。
  • dyld1是在大多数使用c++动态库的系统之前编写的。
    c++有许多的特性比如其初始化器排序方式等在静态环境中工作良好,但是在动态环境中可能降低性能。因此大型c++代码库导致dyld需要完成大量的工作,速度变慢。
  • macOS Cheetah(10)中增加了预绑定技术。
    预绑定为系统中所有的dylib和你的程序找到固定地址。dyld将会加载这些地址的所有内容。加载成功会编辑所有这些二进制数据以获得所有预计算地址。然后下次当它将所有数据放入相同地址时不必进行任何其它额外的工作。这样会大幅提高速度,但是这也意味着每次启动时会编辑你的二进制数据。从安全性来说这样并不是很好的做法。

8.2: dyld2.0 (2004-2007)

  • 随着macOS Tiger发布。
  • dyld2dyld的完全重写版本。
  • 正确支持c++初始化器语义,扩展了mach-o格式并且更新了dyld
  • 具有完整的本机dlopendlsym实现,具有正确的语义,弃用了旧版API(旧版API仍然仅位于macOS中)。
  • dyld2的设计目标是提高速度,因此仅进行有限的健全性检查(以前恶意程序并不多)。
  • dyld有一些安全性问题,对一些功能性改进提高它在现在平台上的安全性。
  • 由于速度大幅提升可以减少预绑定工作量。不同于dyld1编辑你的程序数据,dyld2仅编辑系统库。可以仅在软件更新时做这些事情。因此在软件更新时可能会看到“优化系统性能”之类的文字,这时就是在更新预绑定。

8.2.1: dyld2.x(2007-2017)

  • 增加更多的架构和平台。

    • x86x86_64arm64
    • iOS、 tvOSwatchOS
  • 提升安全性

    • 增加代码签名和ASLR
    • mach-o header 边界检查,避免恶意二进制数据的加入。
  • 提升性能

    • 使用共享缓存替换预绑定。

8.2.2: 共享缓存(shared cache)

共享缓存(dyld预连接器)最早被引入iOS3.1 & macOS Snow Leopard,完全取代预绑定。

  • 它是一个单文件,含有大多数系统dylib
    由于合并成一个文件,因此可以进行优化

    • 重新调整二进制数据以提高加载速度(重新调整所有文本段和所有数据段重写整个符号表以减小大小)。
    • 允许打包二进制数据段节省大量ram
    • 预生成数据结构供dyldobjc使用,在运行时使用让我们不必在应用启动时做这些事情。这样也会节约更多ram和时间。
  • 共享缓存在macOS上本地生成运行dyld共享代码大幅优化系统性能。其它平台由Apple提供。

8.3: dyld3.0(2017-)

dyld3是全新的动态连接器,2017(iOS11)年所有系统程序都默认使用dyld3,第三方在2019(iOS13)年完全取代dyld2
dyld3主要做了以下三方面的改进:

  1. 性能,提高启动速度。dyld3可以帮助我们获得更快的程序启动和运行速度。
  2. 安全性。dyld2增加的安全性很难跟随现实情形增强安全性。
  3. 可测试性和可靠性。
    XCTest依赖于dyld的底层功能,将它们的库插入进程。因此不能用于测试现有的dyld代码。这让难以测试安全性和性能水平。
    dyld3将大多数dyld移出进程,现在大多数dyld只是普通的后台程序。可以使用标准测试工具进行测试。另外也允许部分dyld驻留在进程中,驻留部分尽可能小,从而减少程序的受攻击面积。

8.4: dyld2与dyld3加载对比

dyld3&dyld2官网对比图.png

8.4.1: dyld2流程

  • Parse mach-o headers & Find dependencies:分析macho headers,确认需要哪些库。递归分析依赖的库直到获得所有的dylib库。普通iOS程序需要3-600dylib,数据庞大需要进行大量处理。
  • Map mach-o files:映射所有macho文件将他们放入地址空间(映射进内存)。
  • Perform symbol lookups:执行符号查找。 比如使用printf函数,将会查找printf是否在库系统中,然后找到它的地址,将它复制给应用程序中的函数指针。
  • Bind and rebase:绑定和基址重置。复制这些指针,所有指针必须使用基地址(ASLR的存在)。
  • Run initializers:运行所有初始化器。这之后就开始准备执行main函数。

8.4.2: dyld3流程

dyld3整个被分成了3个流程:

  • dyld3是一个进程外macho分析器和编译器(对应上图中红色部分)。

    • 解析所有搜索路径、rpaths、环境变量。
    • 分析macho二进制数据。
    • 执行所有符号查找。
    • 利用上面的这些结果创建闭包处理。
    • 它是一个普通的后台程序,可以进行正常测试。
    • 大多数程序启动会使用缓存,始终不需要调用进程外macho分析器或编译器。
    • 启动闭包比macho更简单,它们是内存映射文件,不需要用复杂的方式进行分析,可以简单的验证它们,作用是为了提高速度。
  • dyld3也是一个进程内引擎。

    • 检查闭包是否正确。

    • 使用闭包映射所有dylibs

    • 绑定和基址重置。

    • 运行所有初始化器,然后跳转主程序main()

    ⚠️dyld3不需要分析macho Headers或者执行符号查找。在App启动时没有这个过程,因此极大的提升了程序的启动速度。

  • 启动闭包缓存服务

    • 系统app闭包模式构建在共享缓存中。
    • 第三方应用在安装时构建,在软件更新时重新构建。
    • macOS上后台进程引擎可以在后台进程被调用,在其它平台上不需要这么做。

详细情况参考官方:wwdc2017-413(App Startup Time: Past, Present, and Future)

9: 总结

9.1: dyld调用流程

dyld加载流程.jpg

9.2: 核心流程文字总结

  • dyld动态链接器:记载所有库和可执行文件。
  • dyld加载流程:
    • 系统内核调用_dyld_start
      • 重定位dyld(rebaseDyld)
      • 初始化dyld_subsystem_init
      • 调用dyld::_main函数(dyld::_main):
        • 加载共享缓存(mapSharedCache
          • 实际调用loadDyldCache分为三种情况
            • 仅加载到当前进程调用mapCachePrivate。不放入共享缓存,仅自己使用。
            • 已经加载过不进行任何处理。
            • 当前进程第一次加载调用mapCacheSystemWide
          • dyld2/dyld3ClosureMode闭包模式)加载程序(iOS11引入dyld3闭包模式,闭包模式加载速度更快,效率更高。iOS13后动态库和三方库都使用ClosureMode加载):
            • dyld3:
              • 查找/创建mainClosure
              • 通过launchWithClosure启动主程序,成功后返回result(主程序入口main)。逻辑和dyld2启动主程序逻辑基本相同。
            • dyld2:启动主程序
              • 实例化主程序instantiateFromLoadedImage(实际上是创建image
                • 调用sniffLoadCommands生成相关信息,比如compressed。根据compressed来确定使用哪个类来实例化。
                  • compressed是根据LC_DYLIB_INFOLC_DYLD_INFO_ONLY来获取的。
                  • segCount最多256个。
                  • libCount最多4096个。
                • 实例化生成image,加入all images中。
              • 插入&加载动态库loadInsertedDylib
                • 根据上下文初始化配置调用load加载动态库
              • 链接主程序和链接插入动态库(link,主程序链接在前)
                • 修正ASLR、绑定noLazy符号、绑定弱符号
              • 初始化主程序initializeMainExecutable(核心方法)
                • 初始化image,下标从1开始,然后再初始化主程序(下标为0),调用dyld ImageLoader::runInitializers
                  • dyld ImageLoader::processInitializers:
                    • dyld ImageLoader::recursiveInitialization:
                      • dyld dyld::notifySingle:
                        • 这个函数执行一个回调
                        • 通过断点调试回调是_objc_init初始化赋值时传递的第二个参数load_images
                          • load_images中调用了call_load_methods函数
                            • call_class_loads:调用各个类的+load方法。
                            • call_category_loads:调用各个分类的+load方法。
                      • doInitialization
                        • 最终会调用到doModInitFunctions
                          • 内部会调用全局c++构造函数(attribute((constructor))修饰的c函数)
                          • 会首先调用libsystem_initializer构造和上进心回调的注册
                            • 回调有三个参数(map_imagesload_imagesunmap_image
                              • map_images进行类的加载,在回调函数注册后就马上调用
                              • load_imagesnotifySingle循环中调用
                              • unmap_image在异常/回收/检查镜像文件时调用
              • 找到主程序入口LC_MAIN,然后返回主程序入口(main
  • loadc++构造函数、main调用总结:
    • dyld初始化image是按Link Binary With Libraries顺序逐个初始化的,从下标1开始,最后在初始化主程序(下标为0),可以理解为是按image进行分组的。
    • image内部是先加载所有类的+load方法,再加载分类的+load方法,最后加载c++全局构造函数(类load->分类load->c++构造函数)。+load方法是objc中调用的,c++全局构造函数是在dyld中调用的(在不考虑二进制重排等的优化下,image内部的顺序默认是按Compile Sources中顺序进行的)。
    • main函数是在dyld返回入口函数(main)之后才调用的。