美文网首页iOS_UI
dyld加载应用启动原理详解

dyld加载应用启动原理详解

作者: 忻凯同学 | 来源:发表于2021-05-11 16:19 被阅读0次

    我们都知道APP的入口函数是main(),而在main()函数调用之前,APP的加载过程是怎样的呢?接下来我们一起来分析APP的加载流程。

    一. 准备工作

    由于load()main()调用更早,因此我们创建一个工程,在控制器中写一个load()函数,并断点运行,如下图:

    运行起来之后,可以清晰的看到比较详细的函数调用顺序,从_dyld_start()dyld:notifySingle(),频率出现最多的就是这个dyld,那么dyld是什么?它在做什么?

    简单来说dyld是一个动态链接器,用来加载所有的库和可执行文件。接下来我们将通过对dyld源码分析,去追踪dyld到底做了什么?

    _dyld_start

    二. dyld加载流程分析

    1. 首先下载dyld源码

    2. 打开dyld源码工程,根据上图dyldbootstrap::start为关键字搜索dyldbootstrap中调用的start(),如下图:

    dyldbootstrap::start

    3. 进入dyld的start函数

    其中rebaseDyld()分析如下:

    4. 进入dyld的main函数

    注:因为dyld::main()函数代码比较多,以下会分段介绍,也会介绍相对来说比较重要的函数。

    4.1 内核检测

    4.2 获取main执行文件的cdHash缓存区

    4.3 获取CPU信息

        // 获取CPU信息
        getHostInfo(mainExecutableMH, mainExecutableSlide);
    

    4.4 设置MachHeader和内存偏移量

        // 设置MachHeader和内存偏移量Slide
        uintptr_t result = 0;
        sMainExecutableMachHeader = mainExecutableMH;
        sMainExecutableSlide = mainExecutableSlide;
    

    4.5 设置上下文

        // 设置上下文,保存信息
        setContext(mainExecutableMH, argc, argv, envp, apple);
    

    4.6 配置进程限制

    4.7 检测环境变量

    4.8 打印环境配置信息

        // 打印环境配置信息
        if ( sEnv.DYLD_PRINT_OPTS )
            printOptions(argv);
        if ( sEnv.DYLD_PRINT_ENV ) 
            printEnvironmentVariables(envp);
    

    此处可以自己定义环境变量配置,回到刚才创建的新工程中,在Edit Scheme -> Run -> Arguments -> Environment Variables 添加两个参数 DYLD_PRINT_OPTSDYLD_PRINT_ENV,并设置测试value值,如下:

    运行程序,可看到如下打印信息:

    4.9 加载共享缓存(如果没有共享缓存,iOS将无法运行)

    主要函数mapSharedCache()如下:

    4.10 dyld配置

    (1) dyld3(闭包模式)

    iOS11版本之后,引入dyld3闭包模式(ClosureMode):加载速度更快,效率更高。

    开始执行闭包模式

    判断是否开启了闭包模式

    启动闭包模式加载

    其中launchWithClosure加载闭包,会把后面说到的dyld2的大部分流程都封装到launchWithClosure()这个函数里面了,这里不再细说launchWithClosure,因为在接下来的dyld2(非闭包)中会详细解释整个dyld加载的流程,也就是launchWithClosure实现过程。

    (2) dyld2(非闭包模式)

    开始执行闭包模式

    把dyld加入到UUID列表

        // 把dyld加入到UUID列表
        addDyldImageToUUIDList();
    

    配置缓存代理

    4.11 创建主程序的Image

    开始创建主程序的Image,通过instantiateFromLoadedImage(),调用instantiateMainExecutable(),实例化具体的Image类,最后生成的对象,设置到gLinkContext中。

    4.12 设置动态库的版本

        // 加载完共享缓存,设置动态库的版本
        checkVersionedPaths();
    

    4.13 加载插入的动态库

        // 加载插入的动态库
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                oadInsertedDylib(*lib);
        }
    

    4.14 链接主程序和动态库

    • 其中的link()函数,函数体调用了image->link(),函数具体如下:
    • 判断是否需要重新加载所有的Image

    4.15 绑定主程序和动态库

    4.16 初始化主程序

    根据Demo上的堆栈信息,如下:

    终于看到熟悉的函数了,那么dyld加载流程也快结束了。
    根据堆栈信息,获取函数调用层级关系。

        // 初始化主程序
        initializeMainExecutable(); 
    
    • 查找runInitializers()dyld::initializeMainExecutable() -> ImageLoader::runInitializers()
    加载主程序
    • 查找processInitializers()ImageLoader::runInitializers() -> ImageLoader::processInitializers()
    runInitializers
    • 查找recursiveInitialization()ImageLoader::processInitializers() -> ImageLoader::recursiveInitialization()
    processInitializers
    • 查找notifySingle()ImageLoader::recursiveInitialization() -> dyld::notifySingle()
    recursiveInitialization
    • 查找load_images():在 dyld::notifySingle()中并没有找到load_images(),但是找到了sNotifyObjCInit(),该字段是objc函数回调。在 dyld::notifySingle()中执行了这个回调,那就需要追溯到谁去注册的这个回调了。
    • 全局查找sNotifyObjCInit()赋值的地方。在registerObjCNotifiers()中赋值,如下:
    • 全局查找registerObjCNotifiers,在_dyld_objc_notify_register()中调用,且第二个参数是我们需要的。如下:
    • 全局查找_dyld_objc_notify_register(),并没有在dyld源码库里找到,此时需要在源工程中,打符号断点_dyld_objc_notify_register,重新编译执行,可以看到是_objc_init()调用了。此时只能去查找objc源码了。
    • objc源码分析,在objc-os.mm文件中找到_objc_init()函数,其中注册了_dyld_objc_notify_register回调。
    objc-os.mm

    其中第二个参数就是load_images(),在load_images()中也找到了call_load_methods()

    此时初始化程序还未执行完成,回到之前的 ImageLoader::recursiveInitialization()方法中。

    • 执行this->doInitialization()函数
    • 发送通知,初始化主程序完成。

    4.17 进入主程序

        // 通知此进程将要进入程序main()
        notifyMonitoringDyldMain();
    

    到此,start() -> main(),全部执行完毕。

    三. 总结

    • dyld(动态链接器):是苹果操作系统一个重要组成部分,加载所有的库和可执行文件。
    • dyld加载流程:
      • _dyld_start()开始 -> dyldbootstrap::start()
      • 进入dyld的main()
      • 检测内核,配置重定向信息:rebase_dyld()
      • 加载共享缓存
      • dyld2 / dyld3(闭包模式)
        • 实例化主程序
        • 加载动态链接库 (主程序和动态库的image都会加载allImage里面:loadAllImage,主程序在第0位置)
        • 链接主程序、动态库、绑定符号(非懒加载、弱符号)等
        • 最关键:初始化方法initializeMainExecutable()
          • ImageLoader::runInitializers()
          • ImageLoader::processInitializers()
          • ImageLoader::processInitializers()
          • ImageLoader::recursiveInitialization()
          • dyld::notifySingle()
            • 此函数执行一个回调_dyld_objc_notify_register()
            • 通过断点调试:此回调是_objc_init()初始化时赋值的一个函数load_images(),里面执行了call_load_methods()函数,其作用是循环调用各个类的方法。
          • doModInitFunctions()函数:内部会调用全局C++对象的构造函数 __attribute__((constructor))的C函数
        • 返回主程序入口,执行main函数

    相关文章

      网友评论

        本文标题:dyld加载应用启动原理详解

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