[iOS] dyld加载流程

本文的目的主要是分析 dyld的加载流程,了解一下在main 函数之前,底层还做了哪些事情。

0. 引子

  • 创建一个项目,在ViewController 中重写了load 方法,在 main中加了一个 C++方法,即kcFunc,请问下面的输出顺序是什么?

    image.jpeg

  • 运行程序,查看load、kcFunc、main的打印顺序:

load ->C++方法 -> main函数

问题:为什么是这么一个顺序呢?main 不是入口函数吗?为什么不是 main最先执行?下面我们就探索一下main函数之前,系统还做了哪些事情。

1. 编译过程 & 库的概念

在分析 app启动之前,我们需要先了解一下iOS编译过程以及动态库静态库

1.1 编译过程

编译过程如下图所示:


image.jpeg

主要分为以下几步:

  • 源文件:载入.h .m .cpp等文件
  • 预处理:替换宏,删除注释,展开头文件,产生.i文件
  • 编译:将.i文件转换为汇编语言(进行词法分析、语法分析和语义分析,源代码优化),产生.s文件
  • 汇编:将汇编文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
1.2 静态库 & 动态库
1.2.1 静态库

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

  • 优点:编译完成后,库文件实际上就没啥作用了,目标程序没有外部依赖,直接就可以运行
  • 缺点:由于静态库会有两份,所以会导致目标程序体积增大,对内存、性能、速度消耗很大
1.2.2 动态库

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

  • 优点:
    减少打包之后的 app 的大小
    共享内存,节省资源,同一份库可以被多个程序使用;
    通过更新动态库,达到更新程序的目的,由于运行时才载入的特性,可以随时对库进行替换,而不需要重新编译代码
  • 缺点:
    动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行。
1.3 静态库 & 动态库 图示
image.png

2. dyld

根据dyld源码,以及libobjclibSystemlibdispatch 源码协同分析。

2.1 什么是 dyld?

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

共享缓存机制:在iOS系统中,每个程序依赖的动态库都需要通过 dyld 一个一个加载到内存,然而,很多系统库几乎都是每个程序都会用到的,如果每个程序运行的时候都重复的去加载一次,肯定会运行缓慢,所以为了优化启动速度,提高程序性能,就有了共享缓存机制。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着。

所以App 的启动流程图如下:

image.png

2.2 App启动的起始点
  • 在前文的demo中,在load 方法处加一个断点,通过bt命令查看堆栈信息
    image.jpeg

从上图中可以看出,最开始是从dyld中的_dyld_start开始的,所以需要下载 dyld 源码来进行分析。

  • 也可以通过xcode左侧的堆栈信息来找到入口
    image.jpeg

3. dyld流程分析

3.1 _dyld_start

dyld-750.6源码中搜索_dyld_start,发现其在dyldStartUp.s文件中,查找arm64架构,如下:

image.png

发现其调用了dyldbootstrap 命名空间下的start方法:

    // call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)

3.2 dyldbootstrap::start

源码中搜索找到dyldbootstrap命名空间,在这个文件中查找start方法,源码如下:

uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

    // Emit kdebug tracepoint to indicate dyld bootstrap has started 
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    rebaseDyld(dyldsMachHeader);

    // kernel sets up env pointer to be just past end of agv array
    const char** envp = &argv[argc+1];
    
    // kernel sets up apple pointer to be just past end of envp array
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;

    // set up random value for stack canary
    __guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
    // run all C++ initializers inside dyld
    runDyldInitializers(argc, argv, envp, apple);
#endif

    // 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);
}

这个方法的核心是在返回值那调用了 dyld::_main 函数,同时做了很多 dyld 初始化相关的工作:

  • rebaseDyld() dyld 重定位

ASLR:是Address Space Layout Randomization(地址空间布局随机化)的简称。App在被启动的时候,程序会被映射到逻辑地址空间,这个逻辑地址空间有一个起始地址,ASLR技术让这个起始地址是随机的。这个地址如果是固定的,黑客很容易就用起始地址+函数偏移地址找到对应的函数地址。

Code Sign:就是苹果代码加密签名机制,但是在Code Sign操作的时候,加密的哈希不是针对整个文件,而是针对每一个Page的。这个就保证了dyld在加载的时候,可以对每个page进行独立的验证。

正是因为ASLR使得地址随机化,导致起始地址不固定,以及Code Sign,导致不能直接修改Image。所以需要rebase来处理符号引用问题,Rebase的时候只需要通过增加对应偏移量就行了。Rebase主要的作用就是修正内部(指向当前Mach-O文件)的指针指向,也就是基地址复位功能。

  • mach_init()
    rebaseDyld 方法中进行 mach消息初始化
  • __gurad_setup()栈溢出保护
    相关了解:https://www.cnblogs.com/tcctw/p/11487645.html

补充:
macho_headerMach-O文件的头部,而 dyld 加载的文件就是 Mach-O文件。

3.3 dyld::_main

dyld::_main的源码实现很长,如果对 dyld加载流程不太了解,可以根据_main函数的返回值进行反推,这里分部分进行介绍。

  • [第一步:环境变量配置]
    根据环境变量设置相应的值以及获取当前运行架构。


    image.jpeg
  • [第二步:共享缓存]
    检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如 UIKit、CoreFoundation 等。

    image.jpeg

  • [第三步:主程序的初始化]
    调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象

    image.jpeg

  • [第四步:插入动态库]
    遍历 DYLD_INSERT_LIBARIES环境变量,调用loadInsertedDylib加载

    image.jpeg

  • [第五步:link主程序]


    image.jpeg
  • [第六步:link 动态库]


    image.jpeg
  • [第七步:弱符号绑定]


    image.jpeg
  • [第八步:执行初始化方法]


    image.jpeg
  • [第九步:寻找主程序入口,即 main 函数]
    Load Command 读取LC_MAIN入口,如果没有,就读取 LC_UNIXTHREAD,这样就来到了我们所熟悉的main函数了。

    image.jpeg

我们接下来主要分析一下[第三步] 和 [第八步]。

3.3.1 [第三步] 主程序初始化

sMainExecutable 表示主程序变量,查看其赋值,是通过instantiateFromLoadedImage方法初始化的。

image.jpeg

进入instantiateFromLoadedImage 源码,其中创建一个 ImageLoader 实例对象,通过 instantiateMainExecutable 方法创建:

image.jpeg

进入instantiateMainExecutable源码,其作用是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序。其中sniffLoadCommands函数时获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验:

image.jpeg

3.3.2 [第八步] 执行初始化方法

进入initializeMainExecutable源码,主要是循环遍历,都会执行runInitializers方法

image.jpeg

全局搜索runInitializers(cons,找到如下源码,其核心代码是 processInitializers 函数的调用

截屏2021-01-12 上午9.52.42.png

进入processInitializers函数的源码实现,其中对镜像列表调用 recursiveInitialization函数进行递归实例化:

image.jpeg

全局搜索recursiveInitialization(cons,其源码实现如下:

image.jpeg

在这里,我们分成两部分探索,一部分是notifySingle 函数,一部分是doInitialization函数,首先探索notifySingle 函数

3.3.2.1 notifySingle函数
全局搜索notifySingle(,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());

image.jpeg

全局搜索sNotifyObjCInit,发现没有找到该实现,但是有赋值操作

image.jpeg

搜索registerObjCNotifiers 在哪里调用了,发现在_dyld_objc_notify_register进行了调用:

image.jpeg

注意:_dyld_objc_notify_register的函数需要在objc源码中搜索

objc 源码中搜索_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是 objc中的load_images,而load_images会调用所有的+load方法,所以综上所述,notifySingle是一个回调函数,实质上就是objc 里面的loadImages 这个方法。

load 函数加载
下面我们进入 load_images的源码看看其实现,以此在证明 load_images中调用了所有的load函数。

通过objc 源码中_objc_init源码实现,进入load_images的源码实现:

image.jpeg

接着看call_load_methods源码实现,可以发现其核心是通过 do-while循环调用+load 方法的:

image.jpeg

进入call_class_loads源码实现,看到这里调用的 load方法证实了我们之前提到的类的load方法:

image.jpeg

所以,load_images调用了所有类的 load函数,以上源码分析过程正好对应堆栈的打印信息:

image.jpeg

总结:
load 的源码链为:_dyld_start ->dyldbootstrap::start -> dyld::_main -> dyld::initializeMainExecutable ->ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> dyld::notifySingle(是一个回调处理) -> sNotifyObjCInit ->load_images(libobjc.A.dylib)

那么,_objc_init又是什么时候调用的呢?

3.3.2.2 doInitialization 函数
走到objc_objc_init函数,发现走不通了,我们回退到 recursiveInitialization 递归函数的源码实现,继续查看doInitialization函数

image.jpeg

进入doInitialization 函数的源码实现:

image.jpeg

这里也需要分成两部分,一部分是 doImageInit 函数,另一部分是doModInitFunctions函数

  • 进入 doImageInit源码实现,其核心主要是for 循环加载方法的调用,这里需要注意的一点是,libSystem的初始化必须先运行

    image.jpeg

  • 进入doModeInitFuncations 源码实现,这个方法中加载了所有 Cxx文件

    image.png

我们在刚才测试程序的 C++方法处加一个断点,看下堆栈信息:

image.jpeg

到了这里还是没有找到_objc_init的调用,我们可以通过_objc_init加一个符号断点来查看调用_objc_init前的堆栈信息:

  • _objc_init加一个符号断点,查看_objc_init的堆栈信息:

    image.jpeg

    发现_objc_init的调用是在 libsystem 中。

  • libsystem 的源码中查找libSystem_initializer,查看其中的实现:

    image.jpeg

  • 根据前面的堆栈信息,我们发现走的是 libSytem_initializer中会调用libdispatch_init函数,而这个函数的源码是在libdispatch开源库中的,在libdispatch中搜索libdispatch_init:

    image.jpeg

进入_os_object_init源码实现,其源码实现调用了_objc_init函数:

image.jpeg

结合上面的分析,从初始化_objc_init注册的_dyld_objc_notify_register的参数 2,即load_images,到sNotifySingle-->sNotifyObjCInie=参数2sNotifyObjcInit()调用,形成了一个闭环。

所以可以简单的理解为 _objc_init中调用_dyld_objc_notify_register是注册一个回调函数,而sNotifySingle 是调用这个函数。

总结:
_objc_init的源码链:_dyld_start--> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

3.3.3 [第九步]寻找主函数入口
  • 汇编调试,可以看到,来到了[ViewController load]方法

    image.jpeg

  • 继续执行,来到kcFuncC++函数:

    image.jpeg

  • 点击 stepover,继续往下,跑完了整个流程,会回到_dyld_start,然后调用main()函数,通过汇编完成main的参数赋值工作:

    image.jpeg

3.4 dyld 源码实现:
image.jpeg
image.jpeg
3.5 总结

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

image.png

你可能感兴趣的:([iOS] dyld加载流程)