iOS-dyld

dyld

在 MacOS 和 iOS 上,可执行程序的启动依赖于 xnu 内核进程运作和动态链接加载器 dyld。dyld 全称 the dynamic link editor,即动态链接器,其本质是 Mach-O 文件,是专门用来加载动态库的库。

在 xnu 内核为程序启动做好准备后,执行由内核态切换到用户态,由 dyld 完成后面的加载工作:dyld 会将 App 依赖的动态库加载到内存并做一些初始化的工作。

dyld源码下载地址

_dyld_start之前

我们都知道我们在APP中通过打断点最早能追溯到的代码是_dyld_start,大多数的博客分析也是从_dyld_start开始的,但是_dyld_start之前又发生什么了呢?主要是完成了以下三件事:

  1. 内核为App fork一个新的进程
  2. 解析加载App主二进制文件
  3. 根据App的LoadCommand解析出dyld,并解析加载dyld
▼ execve       // 用户点击了app, 用户态会发送一个系统调用 execve 到内核,然后内核 fork 一个新的进程。
  ▼ __mac_execve  // 创建线程
    ▼ exec_activate_image // 在 encapsulated_binary 这一步会根据image的类型选择imgact的方法
      ▼ exec_mach_imgact
        ▼ load_machfile
          ▶︎ parse_machfile  //解析主二进制macho
          ▼ load_dylinker // 解析完 macho后,根据macho中的 LC_LOAD_DYLINKER 这个LoadCommand来启动这个二进制的加载器,即 /usr/bin/dyld
            ▼ parse_machfile // 解析 dyld 这个mach-o文件,这个过程中会解析出entry_point
        ▼ activate_exec_state
          ▶︎ thread_setentrypoint // 设置entry_point。

如果我们在 _dyld_start 打一个断点,然后 image dump sections 一下看看,可以看到此时确实只加载了App的二进制和dyld两个文件,其他动态库还未被加载进来(这正是dyld后续要做的工作)

_dyld_start

_dyld_start再通过 start 跳转到 _main 函数中,然后开始表演时刻。main函数大概有900行代码,主要做了以下事情:

  1. 设置运行环境
  2. 加载共享缓存
  3. 实例化主程序
  4. 加载插入的动态库
  5. 链接主程序
  6. 链接插入的动态库
  7. 弱符号绑定
  8. 初始化动态库、主程序
  9. 查找并返回主程序函数入口

下面针对其中的几个环境展开分析一下

加载共享缓存

一个iOS APP要启动起来大概要加载四百多个动态库,要加载这个四百多个动态库要消耗大量的时间和内存空间。共享缓存机制的引入就是为了解决这个问题的。从iOS 3.1开始,所有的系统库文件都被打包合并成一个大的缓存文件中,而原来的动态库文件则被去除了,系统直接去缓存文件中加载动态库,该共享缓存文件保存在/System/Library/Caches/com.apple.dyld/目录下

iOS这里主要做了以下三件事:

  1. 检测共享缓存是否可用
  2. 如果可用,映射共享缓存到共享区
  3. 添加 dyld 的 UUID 到缓存列表

其中核心逻辑就是映射共享缓存到共享区

static void mapSharedCache()
{
    dyld3::SharedCacheOptions opts;
    opts.cacheDirOverride   = sSharedCacheOverrideDir;
    opts.forcePrivate       = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);
    
    
#if __x86_64__ && !TARGET_IPHONE_SIMULATOR
    opts.useHaswell         = sHaswell;
#else
    opts.useHaswell         = false;
#endif
    opts.verbose            = gLinkContext.verboseMapping;
    
    // 加载 dyld 缓存
    loadDyldCache(opts, &sSharedCacheLoadInfo);
    
    // update global state
    // 更新进程的全局状态信息
    if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
        gLinkContext.dyldCache                              = sSharedCacheLoadInfo.loadAddress;
        dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
        dyld::gProcessInfo->sharedCacheSlide                = sSharedCacheLoadInfo.slide;
        dyld::gProcessInfo->sharedCacheBaseAddress          = (unsigned long)sSharedCacheLoadInfo.loadAddress;
        sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
        dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
    }
}

bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
    results->loadAddress        = 0;
    results->slide              = 0;
    results->errorMessage       = nullptr;
    
#if TARGET_IPHONE_SIMULATOR
    // simulator only supports mmap()ing cache privately into process
    // 模拟器只支持 mmap(内存映射) 缓存到当前进程
    return mapCachePrivate(options, results);
#else
    if ( options.forcePrivate ) {
        // mmap cache into this process only
        // 只加载 mmap(内存映射) 缓存到当前进程
        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
}
链接主程序

链接主程序是比较关键的一个步骤。它主要做了以下几件事情:

  1. 递归的加载主程序依赖的库
  2. 递归修正自己和依赖库的基地址(Rebase),因为 ASLR 的原因,需要根据随机 slide 修正基地址
  3. 递归绑定 noLazy 的符号表,lazy的符号会在运行时动态绑定(首次被调用才去绑定)
  4. 绑定弱符号表,比如未初始化的全局变量就是弱符号
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
    // 添加到已知镜像列表中。这在创建 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
        // 调用 ImageLoader::link() 链接
        image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
    }
    catch (const char* msg) {
        // 标记 image 为未使用,处理
        garbageCollectImages();
        throw;
    }
}
    
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
    //dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
    
    // clear error strings
    (*context.setErrorStrings)(0, NULL, NULL, NULL);
    
    // 起始时间。用于记录时间间隔
    uint64_t t0 = mach_absolute_time();
    // ①、递归加载主程序依赖的库,完成之后发送一个状态为 dyld_image_state_dependents_mapped的通知。
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
    context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
    
    // we only do the loading step for preflights  只做预检的装载步骤
    if ( preflightOnly )
        return;
    
    uint64_t t1 = mach_absolute_time();
    // 清空 image 层级关系
    context.clearAllDepths();
    // 递归更新 image 层级关系
    this->recursiveUpdateDepth(context.imageCount());
    
    __block uint64_t t2, t3, t4, t5;
    {
        dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
        t2 = mach_absolute_time();
        // ②、递归修正自己和依赖库的基地址,因为 ASLR 的原因,需要根据随机 slide 修正基地址。
        this->recursiveRebase(context);
        context.notifyBatch(dyld_image_state_rebased, false);
    
        t3 = mach_absolute_time();
        if ( !context.linkingMainExecutable )
            // ③、递归绑定 noLazy 的符号表,lazy的符号会在运行时动态绑定(首次被调用才去绑定)
            this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
    
        t4 = mach_absolute_time();
        if ( !context.linkingMainExecutable )
            // ④、绑定弱符号表,比如未初始化的全局变量就是弱符号。
            this->weakBind(context);
        t5 = mach_absolute_time();
    }
    
    if ( !context.linkingMainExecutable )
        context.notifyBatch(dyld_image_state_bound, false);
    uint64_t t6 = mach_absolute_time(); 
    
    std::vector<DOFInfo> dofs;
    // ⑤、递归获取/注册程序的 DOF 节区,dtrace 会用其动态跟踪。
    this->recursiveGetDOFSections(context, dofs);
    // 注册
    context.registerDOFs(dofs);
    uint64_t t7 = 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);
    }
    
    // clear error strings
    (*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;
}

这里同时还计算各步骤的耗时。如果想获取这些耗时,只需要在环境变量中添加 DYLD_PRINT_STATISTICS 就可以了。

如果我们在link处断点以下,可以发现第一次执行完link后,进程中的image数量由2个(主程序和dyld)变成了100多个,这多出来的100多个就是主程序递归依赖的。刚才我们说有400多个,另外的300后面还会继续调用link来加载。

初始化动态库、主程序

然后先初始化动态库,然后初始化主程序。例如objc runtime的初始化就是在此时进行的,我们可以通过堆栈来看一下:

动态库和主程序的初始化是从 initializeMainExecutable -> runInitializers -> processInitializers -> recursiveInitialization 递归初始化当前 image 所依赖的库。

void initializeMainExecutable()
{
    // record that we've reached this step。开始初始化标识
    gLinkContext.startedInitializingMainExecutable = true;
    
    // run initialzers for any inserted dylibs
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        for(size_t i=1; i < rootCount; ++i) {
            // 初始化动态库
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }
    
    // run initializers for main executable and everything it brings up
    // 初始化主程序
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
    
    // register cxa_atexit() handler to run static terminators in all loaded images when this process exits
    if ( gLibSystemHelpers != NULL ) 
        (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
    
    // dump info if requested
    if ( sEnv.DYLD_PRINT_STATISTICS )
        ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
    if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
        ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                      InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    // 递归锁
    recursive_lock lock_info(this_thread);
    recursiveSpinLock(lock_info);
    
    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        uint8_t oldState = fState;
        // break cycles
        // 退出递归循环
        fState = dyld_image_state_dependents_initialized-1;
        try {
            // initialize lower level libraries first
            // 先初始化低级别的库
            for(unsigned int i=0; i < libraryCount(); ++i) {
                ImageLoader* dependentImage = libImage(i);
                if ( dependentImage != NULL ) {
                    // don't try to initialize stuff "above" me yet
                    // 不要试图初始化级别高于我的
                    if ( libIsUpward(i) ) {
                        uninitUps.images[uninitUps.count] = dependentImage;
                        uninitUps.count++;
                    }
                    else if ( dependentImage->fDepth >= fDepth ) {
                        dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
                    }
                }
            }
            
            // record termination order. 记录终止命令
            if ( this->needsTermination() )
                context.terminationRecorder(this);
            
            // let objc know we are about to initialize this image
            uint64_t t1 = mach_absolute_time();
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            // 
            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
            
            // initialize this image
            // 真正初始化镜像
            bool hasInitializers = this->doInitialization(context);
    
            // let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_initialized, this, NULL);
            
            if ( hasInitializers ) {
                uint64_t t2 = mach_absolute_time();
                timingInfo.addTime(this->getShortName(), t2-t1);
            }
        }
        catch (const char* msg) {
            // this image is not initialized
            fState = oldState;
            recursiveSpinUnLock();
            throw;
        }
    }
    
    recursiveSpinUnLock();
}

recursiveInitialization 中调用 doInitialization 来初始化镜像:

bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
    CRSetCrashLogMessage2(this->getPath());
    
    // mach-o has -init and static initializers
    doImageInit(context);
    doModInitFunctions(context);
    
    CRSetCrashLogMessage2(NULL);
    
    return (fHasDashInit || fHasInitializers);
}
  1. doImageInit 执行 LC_ROUTINES_COMMAND segment 中保存的函数。
  2. doModInitFunctions执行 __DATA 中 __mod_init_func section 中保存的函数。这个 section 中保存的是 C++ 的构造函数及带有 attribute((constructor)) 的 C 函数。

从下图中我们可以看到 libSystem_initializer -> libdispatch_init -> _os_object_init -> _objc_init 的执行路径

查找主程序入口

最后dyld通过分析主程序的macho_header找到主程序的main函数入口,并返回给上一级函数(_dyld_start)进行一个jmp操作。

void* ImageLoaderMachO::getEntryFromLC_MAIN() const
{
    const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
    const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
    const struct load_command* cmd = cmds;
    for (uint32_t i = 0; i < cmd_count; ++i) {
        if ( cmd->cmd == LC_MAIN ) {
            entry_point_command* mainCmd = (entry_point_command*)cmd;
            void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
            // <rdar://problem/8543820&9228031> verify entry point is in image
            if ( this->containsAddress(entry) )
                return entry;
            else
                throw "LC_MAIN entryoff is out of range";
        }
        cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    }
    return NULL;
}
    0x100015020 <+32>: callq  0x100015062               ; dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*)
    0x100015025 <+37>: movq   -0x8(%rbp), %rdi
    0x100015029 <+41>: cmpq   $0x0, %rdi
    0x10001502d <+45>: jne    0x10001503f               ; <+63>
    0x10001502f <+47>: movq   %rbp, %rsp
    0x100015032 <+50>: addq   $0x8, %rsp
    0x100015036 <+54>: movq   $0x0, %rbp
    0x10001503d <+61>: jmpq   *%rax
    0x10001503f <+63>: addq   $0x10, %rsp
    0x100015043 <+67>: pushq  %rdi
    0x100015044 <+68>: movq   0x8(%rbp), %rdi
    0x100015048 <+72>: leaq   0x10(%rbp), %rsi
    0x10001504c <+76>: leaq   0x8(%rsi,%rdi,8), %rdx
    0x100015051 <+81>: movq   %rdx, %rcx
    0x100015054 <+84>: movq   (%rcx), %r8
    0x100015057 <+87>: addq   $0x8, %rcx
    0x10001505b <+91>: testq  %r8, %r8
    0x10001505e <+94>: jne    0x100015054               ; <+84>
    0x100015060 <+96>: jmpq   *%rax

后续计划

  1. 我们分析完dyld的过程后,后续我们再来看看objc_init又都干了些什么?
  2. 链接主程序,链接动态库是具体怎么完成的?fishHook又是怎么实现的?

参考资料

Dyld系列之一:_dyld_start之前
dyld

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值