iOS 底层探究:dyld加载分析上

769 阅读8分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

1.应用程序加载原理

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

编译过程

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

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

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

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

2. 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的。

3. 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);
            }
        }
        
        // <rdar://problem/12186933> 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/dyld3( ClosureMode闭包模式)加载程序: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"中配置)

image.png ASLRimage list 第0个主程序第一个地址。

image.png