美文网首页
dyld加载流程探索

dyld加载流程探索

作者: 灰溜溜的小王子 | 来源:发表于2020-10-16 17:35 被阅读0次
    main函数之前底层做了那些准备?
    准备

    创建一个工程,在ViewController中重写了load方法,在main中加了一个C++方法,即kcFUnc如图

    main中添加c++方法
    viewController中添加load方法并打印
    打印结果如下:
    打印结果
    结果:在main函数中打印之前首先打印了viewController 中load方法其次打印了c++函数,那么为什么会出现这种情况?为了探索这个问题,在C++函数执行打印下断点,结果如图

    结论: 在main函数执行之前需要dyld参与
    那么什么是dyld

    dyld(the dynamic link editor)苹果的动态链接器,是苹果操作系统的重要组成部分,在app被编译打包成可执行文件格式的Mach-O文件后,交由dyld负责连接,加载程序。

    app编译成可执行文件Mach-O格式流程 编译器原理

    Mach-O编译过程.jpg
    静态库 和 动态库
    • 静态库:在链接阶段,会将可汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的

      • 好处:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行

      • 缺点:由于静态库会有两份,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大

    • 动态库:程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入

      • 优势 :减少打包之后app的大小:因为不需要拷贝至目标程序中,所以不会影响目标程序的体积,与静态库相比,减少了app的体积大小
        共享内存,节约资源:同一份库可以被多个程序使用
        通过更新动态库,达到更新程序的目的:由于运行时才载入的特性,可以随时对库进行替换,而不需要重新编译代码
      • 缺点:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行

    1.从打印的堆栈信息来看程序是从dyld中的_dyld_start开始的所以下载dyld源码在dyld中全局搜索_dyld_start

    image.png
    2.搜索dyldbootstrap找到uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[], const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)函数,其核心是返回值的调用了dyld的main函数,其中macho_header是Mach-O的头部,而dyld加载的文件就是Mach-O类型的,即Mach-O类型是可执行文件类型,由四部分组成:Mach-O头部、Load Command、section、Other Data,可以通过MachOView查看可执行文件信息Mach-O类型是可执行文件类型
    uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                    const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
    {
    ....省略部分代码
        // now that we are done bootstrapping dyld, call dyld's main
        uintptr_t appsSlide = appsMachHeader->getSlide();
        return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
    }
    

    3.第二步返回main方法上方注释"现在我们完成了引导程序,请调用dyld的main进入dyld::_main()"函数,由于此函数信息较长,可调重要参数进行分析:

    • 第一步: 环境变量配置,准备可执行文件cdHash
    • 检查环境
    #if __MAC_OS_X_VERSION_MIN_REQUIRED
        if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
            pruneEnvironmentVariables(envp, &apple);
            // set again because envp and apple may have changed or moved
            setContext(mainExecutableMH, argc, argv, envp, apple);
        }
        else
    #endif
        {
            //检查环境
            checkEnvironmentVariables(envp);
            //将其设置默认值
            defaultUninitializedFallbackPaths(envp);
        }
    #if __MAC_OS_X_VERSION_MIN_REQUIRED
    
    • 获取当前运行环境的架构信息
    getHostInfo(mainExecutableMH, mainExecutableSlide);
    
    • 第二步共享缓存,检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKit、CoreFoundation
      共享缓存
    • 第三步主程序的初始化,调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象
    • 第四步插入动态库,遍历调用loadInsertedDylib函数
    • 第五步链接主程序
    • 第六步链接动态库
    • 第七步弱符号绑定
    image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
    
    • 第八步: 初始化程序
    initializeMainExecutable(); 
    
    • 第九步寻找主程序入口即main函数,从Load Command读取LC_MAIN入口,如果没有,就读取LC_UNIXTHREAD,这样就来到了日常开发中熟悉的main函数

    主要分析第三步和第八步

    第三步:主程序初始化

    -sMainExecutable表示主程序变量,查看其赋值,是通过instantiateFromLoadedImage静态函数初始化

    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
    
    • 进入instantiateFromLoadedImage源码,其中创建一个ImageLoader实例对象,通过instantiateMainExecutable方法创建 instantiateFromLoadedImage
    • 进入instantiateMainExecutable源码其作用是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序。其中sniffLoadCommands函数时获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验 instantiateMainExecutable
    第八步:执行初始化方法
    • 进入initializeMainExecutable源码,主要是循环遍历,都会执行runInitializers方法 initializeMainExecutable源码
    • 进入runInitializers函数,找到如下源码,其核心代码是processInitializers函数的调用 runInitializers函数
    • 进入processInitializers函数的源码实现,其中对镜像列表调用recursiveInitialization函数进行递归实例化
    • 全局搜索recursiveInitialization(const
      recursiveInitialization
      这里需要探索两个函数一部分是notifySingle函数,一部分是doInitialization函数
    notifySingle函数
    • 全局搜索notifySingle函数,其重点是
    (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
    
    • 全局搜索sNotifyObjCInit并没有发现函数的实现,但是在registerObjCNotifiers函数中对其进行了赋值
      sNotifyObjCInit赋值
    • 全局搜索registerObjCNotifiers函数的调用,间接探究对sNotifyObjCInit的赋值
    
    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中并没有搜索到函数_dyld_objc_notify_register函数的调用,此函数需要在libobjc源码中搜索.

    • 在objc源码中搜索_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images,而load_images会调用所有的+load方法。所以综上所述,notifySingle是一个回调函数 _objc_init源码
    load_images函数实现
    • 进入函数体
    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);
            prepare_load_methods((const headerType *)mh);
        }
    
        // Call +load methods (without runtimeLock - re-entrant)
        call_load_methods();
    }
    
    • 进入call_load_methods 函数内部,可以发现其核心是通过do-while循环调用+load方法(上方注释可以品一品)
    void call_load_methods(void)
    {
      ...
        // Re-entrant calls do nothing; the outermost call will finish the job.
        if (loading) return;
        loading = YES;
        void *pool = objc_autoreleasePoolPush();
        do {
            // 1. Repeatedly call class +loads until there aren't any more
            while (loadable_classes_used > 0) {
                call_class_loads();
            }
            // 2. Call category +loads ONCE
            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 函数(上方注释可以品一品)
      call_class_loads 函数
      所以,load_images调用了所有的load函数,以上的源码分析过程正好对应堆栈的打印信息
    doInitialization 函数
    • dyld中接着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);
    }
    

    此处又分了两个部分doImageInit(context)函数doModInitFunctions(context)函数

    第一部分doImageInit
    • 进入doImageInit函数其核心主要是for循环加载方法的调用,这里需要注意的一点是,libSystem的初始化必须先运行( libSystem initializer must run first)
      doImageInit函数
    第二部分doModInitFunctions
    • 进入doModInitFunctions源码实现,这个方法中加载了所有Cxx文件,在C++方法加断点,bt打印堆栈进行验证
      堆栈信息验证doModInitFunctions方法加载对C++函数

    但是_objc_init函数什么时间调用还是未曾可知,load_images函数还是未被注册,也即是程序运行+load无从说起

    _objc_init
    • 可执行objc源码工程objc_init下断点打印堆栈信息
      objc_init执行之前的堆栈信息情况
      流程:doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

    故最终dyld加载流程,如下图所示,图中也诠释了前文中的问题:为什么是load-->Cxx-->main的调用顺序

    dyld加载流程

    相关文章

      网友评论

          本文标题:dyld加载流程探索

          本文链接:https://www.haomeiwen.com/subject/eulopktx.html