iOS dyld

一、应用程序加载原理

在分析dyld加载应用程序之前,先清楚以下基本概念。
库:可执行的二进制文件,可以被系统加载到内存。库分为静态库和动态库,动态和静态库的区别是链接的区别。

编译过程

image.png

源文件->预编译->编译->汇编->链接->可执行文件(MachO格式)。

动态库:动态链接。只会存在一份,在内存中共享。减少了包的体积大小。这里有完全动态特性的就是系统的动态库了。
静态库:静态链接。静态库在装载的时候会重复,浪费了空间。

那么这些库是怎么加载到内存中的呢?
是通过dyld动态链接器加载到内存中的。整个过程大概如下:

image.png

dyldthe dynamic link editor)动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后交由dyld负责余下的工作。
这篇文章将详细分析整个dyld加载过程。

二、dyld 初探

既然是dyld加载的库,那么在加载完成后肯定会进入main函数,那么在main函数上打个断点看下调用:

image.png

可以看到是libdyld.dylib start:调用的main函数。给start下个符号断点并没有进入断点。那么证明在底层的符号不是start。实现一个+ load方法打个断点发现如下调用栈:

image.png

可以看到是dyld _dyld_start发起的调用。opensoure上直接下载dyld-852源码。

搜索_dyld_start发现这个入口是在汇编中,其中主要是调用了dyldbootstrap::start

image.png

最终跳转了返回的LC_MAIN

也可以通过断点查看汇编调用确定:


image.png

dyldbootstrapc++的命名空间,start是其中的函数。搜索后发现dyldbootstrap::startdyldInitialization.cpp中,这也就是函数开始的地方。接下来结合源码分析怎么从start调用到loadmain方法,以及dyld是如何加载images的。

三、dyld源码分析

3.1 dyldbootstrap::start(dyldInitialization.cpp

可以通过搜索dyldbootstrap命名空间找到start源码。核心代码如下:

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);
    //栈溢出保护
    __guard_setup(apple);
    //初始化dyld
    _subsystem_init(apple);

    //偏移
    uintptr_t appsSlide = appsMachHeader->getSlide();
    //调用dyld main函数
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

start主要做了以下几件事:

  • 告诉debug server dyld启动
  • 重定位dyld
  • 栈溢出保护
  • 初始化dyld
  • 调用dyld main函数

其中start只做了一些配置和初始化的工作,核心逻辑在main函数中,start返回了main函数的返回值。

3.2 dyld::_main(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);
    }
……
    //主程序可执行文件 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;
    }
    //获取主程序Header,Slide(ASLR的偏移值)
    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);

……

    //检查共享缓存是否可用,到了这里只读了主程序还没有加载主程序。iOS必须有共享缓存。
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
        if ( sSharedCacheOverrideDir)
            mapSharedCache(mainExecutableSlide);
#else
        //加载共享缓存方法
        mapSharedCache(mainExecutableSlide);
#endif
……
    }
……
#if !TARGET_OS_SIMULATOR
    //dyld3 ClosureMode模式,iOS11引入ClosureMode,iOS13后动态库和三方库都使用ClosureMode加载。
    if ( sClosureMode == ClosureMode::Off ) {
        //ClosureMode off打印log,往 if-else 后面走了
        if ( gLinkContext.verboseWarnings )
            dyld::log("dyld: not using closures\n");
    } else {
        //启动模式 闭包模式 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

……
        //实例化主程序,加入到allImages(第一个靠dyld加载的image就是主程序)
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        //代码签名
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

#if TARGET_OS_SIMULATOR
        // check main executable is not too new for this OS
        //检查主程序是否属于当前系统
        {……}
#endif
……

#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
……
        //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;

        // link main executable
        //记录链接主程序
        gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
        if ( mainExcutableAlreadyRebased ) {
            // previous link() on main executable has already adjusted its internal pointers for ASLR
            // work around that by rebasing by inverse amount
            sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
        }
#endif
        //链接主程序
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        sMainExecutable->setNeverUnloadRecursive();
……
        if ( 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 SUPPORT_ACCELERATE_TABLES
        //判断条件不满足,持续 goto reloadAllImages
        if ( (sAllCacheImagesProxy != NULL) && ImageLoader::haveInterposingTuples() ) {
            // Accelerator tables cannot be used with implicit interposing, so relaunch with accelerator tables disabled
            ImageLoader::clearInterposingTuples();
            // unmap all loaded dylibs (but not main executable)
            for (long i=1; i < sAllImages.size(); ++i) {
                ImageLoader* image = sAllImages[i];
                if ( image == sMainExecutable )
                    continue;
                if ( image == sAllCacheImagesProxy )
                    continue;
                image->setCanUnload();
                ImageLoader::deleteImage(image);
            }
            // note: we don't need to worry about inserted images because if DYLD_INSERT_LIBRARIES was set we would not be using the accelerator table
            sAllImages.clear();
            sImageRoots.clear();
            sImageFilesNeedingTermination.clear();
            sImageFilesNeedingDOFUnregistration.clear();
            sAddImageCallbacks.clear();
            sRemoveImageCallbacks.clear();
            sAddLoadImageCallbacks.clear();
            sAddBulkLoadImageCallbacks.clear();
            sDisableAcceleratorTables = true;
            sAllCacheImagesProxy = NULL;
            sMappedRangesStart = NULL;
            mainExcutableAlreadyRebased = true;
            gLinkContext.linkingMainExecutable = false;
            resetAllImages();
            goto reloadAllImages;
        }
    #endif
……
        //绑定主程序
        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);
            }
        }
        
        //  do weak binding only after all inserted images linked
        //弱引用绑定主程序,所有镜像文件绑定完成后进行。
        sMainExecutable->weakBind(gLinkContext);
        gLinkContext.linkingMainExecutable = false;

        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
        // run all initializers
        //初始化主程序,到目前为止还没有执行到主程序中的代码。
        initializeMainExecutable(); 
……

        {
            // 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函数主要是返回了主程序的函数入口(main),主要流程如下:

  • 配置环境,获取主程序HeaderSlideASLR
  • 加载共享缓存:mapSharedCache。这个时候只读了主程序还没有加载主程序。iOS必须有共享缓存。
    checkSharedRegionDisable方法中说明了iOS必须有共享缓存:
    image.png
  • dyld2/dyld3ClosureMode闭包模式)加载程序: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,然后返回主程序。

DYLD_PRINT_OPTSDYLD_PRINT_ENV环境变量配置,可以打印环境变量配置(在"Scheme -> Arguments -> Environment Variables"中配置):

环境变量配置

ASLRimage list0个主程序第一个地址。
ASLR

关于dyld2/dyld3更多信息将在后面做进一步总结。

3.3 mapSharedCache加载共享缓存

共享缓存专门缓存系统动态库,如:UIKitFoundation等。(自己的库、三方库不行)
mapSharedCache真正调用的是loadDyldCache

static void mapSharedCache(uintptr_t mainExecutableSlide)
{
    ……
    //真正调用的是loadDyldCache
    loadDyldCache(opts, &sSharedCacheLoadInfo);
    ……
}

3.3.1 loadDyldCache

bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
    results->loadAddress        = 0;
    results->slide              = 0;
    results->errorMessage       = nullptr;

#if TARGET_OS_SIMULATOR
    // simulator only supports mmap()ing cache privately into process
    return mapCachePrivate(options, results);
#else
    if ( options.forcePrivate ) {
        // mmap cache into this process only
        //仅加载到当前进程
        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
}

loadDyldCache3个逻辑:
1.仅加载到当前进程调用mapCachePrivate。不放入共享缓存,仅自己使用。
2.已经加载过不进行任何处理。
3.当前进程第一次加载调用mapCacheSystemWide

动态库的共享缓存在整个应用的启动过程中是最先被加载的。

3.4 instantiateFromLoadedImage 实例化主程序(创建image

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中。

实际上实例化真正调用的是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);

    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个。

3.5 loadInsertedDylib 插入&加载动态库

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

3.6 ImageLoader::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
        image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
    }
}
  • link最终调用的是ImageLoader::link

ImageLoader::link

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();
        //Rebase修正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 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
  • 绑定NoLazy符号。
  • 绑定弱符号。
  • 注册。
  • 记录时间,可以通过配置看到dyld应用加载时长。

3.7 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打印相关信息。

dyld ImageLoader::runInitializers(ImageLoader.cpp)

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然后调用processInitializers

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.
    if ( ups.count > 0 )
        processInitializers(context, thisThread, timingInfo, ups);
}
  • 最终调用了recursiveInitialization

ImageLoader::recursiveInitialization(ImageLoader.cpp)

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
            //先初始化下级lib
            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();
}
  • 整个过程是一个递归的过程,先调用依赖库的,再调用自己的。
  • 调用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

c++系统构造函数

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

⚠️这里也就说明了对于同一个image而言,+ load方法是比c++构造函数更早调用的。

dyld::notifySingledyld2.cpp
notifySingle对应一个函数,在setContext的时候赋值:

image.png

//调用到objc里面去
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
……

    if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
        uint64_t t0 = mach_absolute_time();
        dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
        //回调指针 sNotifyObjCInit 是在 registerObjCNotifiers 中赋值的。这里执行会跑到objc的load_images中
        (*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);
        }
    }
……
}
  • notifySingle中找不到load image的调用(从堆栈信息中可以看到notifySingle之后是load image)。
  • 这个函数执行一个回调sNotifyObjCInit,条件是statedyld_image_state_dependents_initialized

搜索下回调sNotifyObjCInit的赋值操作,发现是在registerObjCNotifiers中赋值的

registerObjCNotifiers

//谁调用的 registerObjCNotifiers ? _dyld_objc_notify_register。这里赋值了三个参数 _dyld_objc_notify_mapped,_dyld_objc_notify_init,_dyld_objc_notify_unmapped
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
    }

    //  call 'init' function on all images already init'ed (below libSystem)
    for (std::vector::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());
        }
    }
}
  • registerObjCNotifiers赋值来自于第二个参数_dyld_objc_notify_init
  • 赋值后里面调用了notifyBatchPartial(内部调用了sNotifyObjCMapped)。
  • 循环调用load_images,这里调用的是依赖的系统库的libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib

搜索发现是_dyld_objc_notify_register调用的registerObjCNotifiers

_dyld_objc_notify_registerdyldAPIs.cpp

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

可以看到是被_objc_init调用的。

_objc_init的调用在objc-os.mm中,查看源码:

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进行交互的。

ImageLoaderMachO::doInitialization(ImageLoaderMachO.cpp)

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

    // mach-o has -init and static initializers
    doImageInit(context);
    //加载c++构造函数
    doModInitFunctions(context);
    
    CRSetCrashLogMessage2(NULL);
    
    return (fHasDashInit || fHasInitializers);
}

加上以下代码查看MachO文件:

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

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

会发现MachO中多了__mod_init_func

image.png

  • 调用doModInitFunctions函数加载c++构造函数(__attribute__((constructor))修饰的c函数)

ImageLoaderMachO::doModInitFunctions

image.png

  • 内部是对macho文件的一些读取操作。
  • 会进行__mod_init_func section的确认,与上面的验证符合。
  • 加载前必须加载完libSystem库。

四、反推objc与dyld的关联

在上面的符号断点过程中可以看到在_dyld_objc_notify_registerdoModInitFunctions之间还有非dyld的库。
_objc_init中打个断点有如下调用栈:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
  * frame #0: 0x00000001002d2d44 libobjc.A.dylib`_objc_init at objc-os.mm:925:9
    frame #1: 0x000000010044b0bc libdispatch.dylib`_os_object_init + 13
    frame #2: 0x000000010045bafc libdispatch.dylib`libdispatch_init + 282
    frame #3: 0x00007fff69543791 libSystem.B.dylib`libSystem_initializer + 220
    frame #4: 0x000000010002f1d3 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 535
    frame #5: 0x000000010002f5de dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
    frame #6: 0x0000000100029ffb dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 493
    frame #7: 0x0000000100029f66 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
    frame #8: 0x00000001000280b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
    frame #9: 0x0000000100028154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
    frame #10: 0x0000000100016662 dyld`dyld::initializeMainExecutable() + 129
    frame #11: 0x000000010001bbba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
    frame #12: 0x0000000100015227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
    frame #13: 0x0000000100015025 dyld`_dyld_start + 37
image.png

对于doModInitFunctions后面的流程是未知的。从doModInitFunctions->_objc_init流程是未知的。那么最好的方式就是从_objc_init反推调用到它的整个流程。

4.1 _os_object_init

_objc_init是被_os_object_init调用的,这个函数在libdispatch.dylib中。下载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
}

发现确实是在_os_object_init中直接调用了_objc_init()
接着在libdispatch_init中发现了_os_object_init的调用:

image.png

  • 其中进行了TLS键值处理以及线程处理。

4.2 libSystem_initializer

libSystem_initializer是在libSystem库中,下载libSystem最新源码1292.120.1
同样直接搜索libSystem_initializer

image.png

  • 其中直接调用了libdispatch_init,同样还调用了__malloc_init_dyld_initializer以及_libtrace_init
    libSystem_initializerImageLoaderMachO::doModInitFunctions调用的,这样整个流程就回到了dyld中。整个流程就串起来了。

doModInitFunctions中发现了如下代码:

image.png

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

那么libSystem_initializer是在哪里调用的呢?在doModInitFunctions中并没有看到libSystem_initializer的调用。但是断点读取确实读取到了:

image.png

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

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

image.png

libSystemc++构造函数在dyldlibobjcFoundationc++构造函数之后,主程序之前执行。

五、 dyld注册objc回调简单分析

通过上面的分析在_objc_init中调用了_dyld_objc_notify_register进行回调注册,有如下赋值:

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

接下来将详细分析这3个回调的逻辑。

5.1 sNotifyObjCMapped(map_images)

sNotifyObjCMappeddyld中的调用只在notifyBatchPartial中:

image.png

notifyBatchPartial的调用是在registerObjCNotifiersregisterImageStateBatchChangeHandler、以及notifyBatch中。那么根据之前的分析这里的调用就是registerObjCNotifiers注册回调后就在里面调用了。

objc源码map_images中打个断点:

image.png

可以验证在注册回调后立马调用了map_images

map_images中直接加锁调用了map_images_nolock,其中进行了类的加载相关的操作。这块逻辑将单独写篇文章进行分析。

5.2 sNotifyObjCInit(load_images)

sNotifyObjCInitdyld中的调用分为以下情况:
1.notifySingleFromCache中。
2.notifySingle中。
3.registerObjCNotifiers
notifySingleFromCachenotifySingle逻辑基本相同,无非就是有没有缓存的区别。
registerObjCNotifiers是在注册回调函数的时候直接进行的回调。直接在load_images中打个断点可以跟踪到如下信息:

image.png

可以看到系统的基础库在注册回调后就马上进行了load_images的调用。

而对于其他库是通过notifySingle走的回调逻辑:

image.png

5.2.1 load_images(objc-runtime-new.mm

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

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方法。

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

add_class_to_loadable_list & add_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());
    }
    //空间不足开辟空间
    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));
    }
    //将load方法添加到loadable_classes中。相当于一个下标中存储的是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));
    }
    
    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中
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
  • 通过字符出那比较获取load方法。
  • 空间不足的情况下开辟空间吗,每次开辟的空间大小为(2倍+16)* 16 字节。
struct loadable_class {
  Class cls;  // may be nil
  IMP method;
};
  • 将对应的数据添加进loadable_classesloadable_categories中。

⚠️加载过程中类和分类是有区分的。为什么区分将在后续的文章中详细分析。

getLoadMethod

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

    const method_list_t *mlist;

    //递归所有的baseMethods,查找load方法。
    mlist = ISA()->data()->ro()->baseMethods();
    if (mlist) {
        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) {
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            if (0 == strcmp(name, "load")) {
                return meth.imp(false);
            }
        }
    }

    return nil;
}
  • load方法获取是通过字符出那比较获取的。

5.2.2 call_load_methods (objc-loadmethod.mm)

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();
    
    //循环调用 call_class_loads,类的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方法,这也就是+ loadmain之前被调用的原因。

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方法进行调用。

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++构造函数才返回LC_MAIN进行main函数的调用。可以通过汇编断点验证:

image.png

这样就和开头的时候对应上了。那么如果修改main函数的名称,编译的时候就报错了。主程序的入口main是写死的,可以通过Hook去操作main隐藏自己的逻辑。

根据以上分析可以看到dyld是按image list顺序从第1image调用runInitializers(可以看做是以image分组)。再调用下一个imagerunInitializers最后再调用主程序(下标为0)的runInitializers。在runInitializers内部先调用所有类的+load,再调用所有分类的+ load,最后调用c++的构造函数。
objc中调用loaddyld中调用doModInitFunctions
⚠️如果在+ load中做了防护,那么可以通过在+ load执行前断住外部符号做处理。这样就可以绕过防护了。
防护最重要的就是不让别人找到防护的逻辑,只要能找到那么破解就很容易了。
案例分析:你真的了解dyld么?

5.3 sNotifyObjCUnmapped(unmap_image)

sNotifyObjCUnmappeddyld中只有removeImage进行了调用:

image.png

removeImagecheckandAddImagegarbageCollectImages_dyld_link_module调用。

  • garbageCollectImages:在link等其它异常以及回收的时候调用。
  • checkandAddImage:检测加载的image不在镜像列表中的时候直接删除。
  • _dyld_link_module:暂时不确定是哪里调用的。

5.3.1 unmap_image

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信息。

六 、dyld3闭包模式分析

关于闭包模式在开启闭包模式的情况下就直接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)
{
    ……
    libDyldEntry->runInitialzersBottomUp((mach_header*)mainExecutableMH);
    ……
}

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调用的:

image.png

但是最终的回调以及调用方确是dyld2的逻辑。看下源码:

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    if ( gUseDyld3 )
        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_func_lookup_and_resign("__dyld_objc_notify_register", &p);
    p(mapped, init, unmapped);
}

那么就说明gUseDyld3NULL,走了dyld2的逻辑。但是如果走dyld3可以得到以下信息,注册的三个回调函数指针与dyld2名称不同:

_objcNotifyMapped   = map;
_objcNotifyInit     = init;
_objcNotifyUnmapped = unmap;
  • _objcNotifyInit已经清楚了是在runInitialzersBottomUp中调用的。
  • _objcNotifyUnmapped是在garbageCollectImages ->removeImages中调用的。
  • _objcNotifyMapped是在runImageCallbacks中调用的,它有两个调用方applyInitialImages以及loadImage
    • applyInitialImages是被_dyld_initializer调用的。_dyld_initializer在第四部分已经明确了是在libSystem_initializer中调用的。而由于_dyld_initializer是在libdispatch_init之前调用的,所以这个时候应该还没有注册回调。
    • loadImage是在dlopen中调用的。

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

dyld3闭包模式调用流程

七、dyld简介

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

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

7.1 dyld 1.0 (1996–2004)

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

7.2 dyld2.0 (2004-2007)

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

7.2.1dyld2.x(2007-2017)

  • 增加更多的架构和平台。
    • x86x86_64arm64
    • iOStvOSwatchOS
  • 提升安全性
    • 增加代码签名和ASLR
    • mach-o header 边界检查,避免恶意二进制数据的加入。
  • 提升性能
    • 使用共享缓存替换预绑定。

7.2.2 共享缓存(shared cache)

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

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

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

7.3 dyld3.0(2017-)

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

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

7.4 dyld2与dyld3加载对比

dyld2&dyld3官网对比图

7.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函数。

7.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)

八、总结

8.1dyld调用流程

dyld调用流程

8.2 核心点文字总结

DYLD加载流程文字版总结

你可能感兴趣的:(iOS dyld)