dyld加载应用启动原理详解

我们都知道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函数
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容