11.iOS底层-dyld 加载流程

452 阅读9分钟

探究APP启动到main函数之间的操作流程。

应用程序加载 例子

  1. 创建了一个简单的工程,在入口main函数中加打印 1223333,同时加上一个 为kcFunc()的c++函数,在viewcontroller 加上了一个load()方法 image.png

image.png

2.运行结果发现

  • 1.调用了load,
  • 2.调用了c++函数
  • 3.最后才进入main 打印了1223333

image.png 3.这样的一个结果我不太明白为什么先调用了load,和c++ 函数才进入main呢,main是一个app的入口呀。说明在app启动之前,系统做了什么样的操作呢。

1.1、库

每个程序的运行都会依赖一些基础的库,比如说UIKit,CoreFoundation等。
库是一些可执行的二进制文件,能被操作系统加载到内存中。
库有两种形式,就是静态库(.a , .lib)和动态库(.so , .dll),两个库主要表现在链接的区别。

  • 静态库:静态库在编译时加载,在链接时会完整的复制到可执行文件中,此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的。
    • 优点:编译后的执行文件不需要外部库的支持,直接就能使用。
    • 缺点:有多个app使用就会被复制多份,不能共享且占用更多冗余内存。所有的函数都在库中,因此当修改函数时需要重新编译。

image.png

  • 动态库:程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时由系统动态加载到内存。
    • 优势:

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

image.png

编译过程

源文件:载入.h、.m、.cpp等文件
预编译:替换宏,删除注释,展开头文件,产生.i文件
编译:编译器将.i文件转换为汇编语言,产生.s文件
汇编:将汇编文件转换为机器码文件,产生.o文件
链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
其中编译过程如下图所示,主要分为以下几步: image.png

  • 生成的可执行文件 image.png

  • 把这个MachO文件,拖拽到MachOView里面就可以查看MachO的结构。 image.png

  • Header 头部,包含可以执行的CPU架构,比如x86,arm64

  • Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式

  • Data,数据,包含load commands中需要的各个段(segment)的数据,每一个Segment都得大小是Page的整数倍。

image.png

dyly

我理解为就是将可执行文件和手机系统的库产生链接,即可执行文件通过动态链接一些系统库,才能真正的在手机上运行。

dyld 全名 The dynamic link editor . 它是苹果的动态链接器,是苹果操作系统一个重要组成部分 ,在应用被编译打包成可执行文件格式的 Mach-O 文件之后 ,交由 dyld 负责链接 , 加载程序 。 image.png

  • runtime 注册回调函数:_dyld_objc_notify_register(&map_images, load_images, unmap_image);
  • image : 库 映射完成后的镜像文件,
    • 映射:从磁盘copy一份到内存
  • 做一个拓展小tip 找到系统的库文件。
    • 1.断点(随便打个断点)
    • 2.LLDB 输入指令 image list
    • 3.搜索 CoreFoundation image.png
    • 4.复制路径前往文件就可以找到系统的库, image.png
    • 5.我这个是在模拟器上的。

2 dyld流程

  • 1.在控制器前加+load方法,打上断点。LLDB bt 打印函数栈,再看左侧的栈打印,其实就是一一对应的。
  • 流程大致上就是
    • 1._dyld_start
    • 2.dyldbootstrap::start
    • 3.dyld::_main
    • 4.dyld::useSimulatorDyld 这个我猜测应该是我调用的时模拟器的,所以使用的是模拟器的dyld
    • 5.start_sim
    • 6.dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*)
    • 7.dyld::initializeMainExecutable()
    • 8.ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&)
    • 9.ImageLoader::processInitializers(
    • 10.ImageLoader::recursiveInitialization
    • 11.dyld::notifySingle
    • 12.load_images image.png
  • APP加载过程
    • 1.app启动
    • 2.加载libSystem
    • 3.Runtime向dyld注册回调函数
    • 4.加载新的image
    • 5.执行map_images、load_images
    • 6.调用main函数
  • 现在我们在main入口打上断点。过掉上个断点走到这里,这里是程序的入口,说明控制器的加载是在程序进来之前完成的。 image.png

目标 研究程序加载 从dyldstart -> main 中间所做的事情

image.png

  • 1.即弄明白这段是做了什么。 image.png
  • 2.下载dyld 852 源码地址
  • 3.load 函数在 main 函数调用之前 image.png
  • 4.断点load -> LLDB bt
    1. frame #12: 0x00000001130d4025 dyld_dyld_start + 37` 知道这个是 dyld的入口

1、找到目标

  • 1.查找汇编 _dyld_start image.png
  • 2.找到目标,不同架构,选择不同,但是主体流程相同。 image.png
  • 3.调用了这个函数 dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue) image.png
  • 4.接下来就去搜索dyldbootstrap这个命名空间,然后继续在其中找到start方法,发现其是返回了dyld::_main函数的结果。c++的语法问题。类似于 dyldbootstrap这个类的 start 方法。 image.png
  • 5.返回_main函数 image.png
  • 6.跳转到main image.png 1000+行代码选择性查看学习 image.png

2.进入了主要流程中的main函数

  • 第一步- 条件准备 : 环境、平台、版本、路径、主机信息。。。

      1. getHostInfo(mainExecutableMH, mainExecutableSlide); 架构信息的准备
      1. 镜像文件平台信息 image.png
      1. 文件路径 image.png
    • checkEnvironmentVariables(envp); //检查设置的环境变量

    • defaultUninitializedFallbackPaths(envp); //如果DYLD_FALLBACK为nil,将其设置为默认值

    • 如果设置了DYLD_PRINT_OPTS环境变量则打印,如果设置了DYLD_PRINT_ENV环境变量,则打印环境变量 image.png

    1. 加载共享缓存,系统级别处理。
    • 2.1调用checkSharedRegionDisable函数检查并加载共享缓存库(iOS无法禁用共享缓存库) image.png
    • mapSharedCache(mainExecutableSlide);检查共享缓存是否映射到共享区域 image.png
  • 3.addDyldImageToUUIDList(); 将dyly本身添加到UUID列表 image.png

  • 4.主程序初始化 sMainExecutable

    • 4.1我们从结果result 的反推得知由 sMainExecutable创建 image.png

    • 4.2.反向推到sMainExecutable创建通过查看源码查看 , 结合函数调用栈 , 我们跟进去调用流程 找到 sMainExecutableimage.png

      • 加载可执行文件,并生成一个ImageLoader实例对象 镜像文件加载器 addImage(image) image.png
  • 5.加载动态库并链接动态库

    • 5.1loadInsertedDylib(*lib); 加载插入的动态库,遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载插入的动态库。 image.png

    • 5.2.链接库

      • 遍历 :sInsertedDylibCount
      • ImageLoader* image = sAllImages[i+1];
      • link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1); link插入的动态库
      • this->recursiveApplyInterposing(context); 插入任何动态加载的镜像文件 image.png

      image.png

      image.png

  • 6.link主程序 image.png

  • 7.weakBind 弱引用绑定主程序 image.png

  • 8.主程序初始化运行,运行所有初始化程序 image.png

  • 9.寻址主程序入口 image.png

  • 10.监听dyly的main notifyMonitoringDyldMain(); image.png


3.主程序运行流程 initializeMainExecutable();

现在我们连探究主程序初始化到底做了什么
sMainExecutable表示主程序变量,查看其赋值,是通过instantiateFromLoadedImage方法初始化

1.instantiateFromLoadedImage->instantiateMainExecutable

  • 进入instantiateFromLoadedImage函数查看 image.png

  • 进入instantiateMainExecutable函数查看 image.png

  • 其中sniffLoadCommands中是获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验。确定此mach-o文件是否具有压缩的LINKEDIT 以及 段数 image.png

  • 进入sniffLoadCommands中函数查看 image.png

2.主程序运行

主程序初始化完成后,进入主程序的运行,我们返回代码重新到这里initializeMainExecutable(); image.png

  • 进入initializeMainExecutable()查看
    • 插入的动态库遍历调用runInitializers
    • 主程序调用runInitializers image.png
  • 全局搜索进入runInitializers()查看 image.png
  • 1.初始化准备:processInitializers 准备镜像文件准备 image.png
    • 1.进入processInitializers函数的源码实现,其中对镜像列表调用recursiveInitialization函数进行递归实例化 image.png
    • 2.recursiveInitialization 底层的依赖文件加载 image.png
    • 3.两个操作,1context.notifySingle->2.doInitialization,然后notifySingle image.png

  • 先查找这个context.notifySingle
  • 找到这个(*notifySingle)(dyld_image_states, const ImageLoader* image, InitializerTimingList*); image.png
  • 全局搜索找到这里 image.png
  • 全局搜索sNotifyObjCInit,发现没有找到实现,有赋值操作 image.png image.png image.png
  • 查看registerObjCNotifiers的调用时机 _dyld_objc_notify_register函数调用

image.png

  • 在objc源码库的objc_init中调用这个方法。 image.png

  • 整理一下顺序

    • context.notifySingle -> (*sNotifyObjCInit)
    • sNotifyObjcInit 是在 registerOBjCNotifiers函数调用的init赋值的,即:sNotifyObjCInit = init
    • _dyly_objc_notify_register 调用了 registerObjCNotifiers, init = init
    • _dyly_objc_notify_register 传递了三个参数进来
      • &map_images
      • load_images
      • unmap_image
    • 得到 load_images = init = sNotifyObjcInit

load_images

  • load_images -> call_load_methods() image.png
  • prepare_load_methods
    • classref_t const *classlist = _getObjc2NonlazyClassList(mhdr, &count);取出所有加载进去的类列表

    • 遍历:schedule_class_load

      • 递归父类:schedule_class_load(cls->superclass)
      • add_class_to_loadable_list 把这个类的load方法加到list中
    • classref_t *classlist = getObjc2NonlazyClassList(mhdr,&count);

image.png

image.png

image.png

  • 进入查看call_load_methods循环调用了call_calss_loads()还有 call_category_loads();分类的load image.png

  • 进入call_calss_loads()查看 了解到这里调用的load方法证实我们前文提及的类的load方法,

image.png

  • objc_autoreleasePoolPush() 压栈自动释放池

  • class_class_loads()

  • call_category_loads()

  • objc_autoreleasePoolPop(pool);出栈

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

image.png

map_images

-> map_images_nolock -> _read_images

  • 类 gdb_objc_readied_classes
  • 方法 nameSelectors
  • 协议 protocol_map
  • 主要作用
    • 初始化类 : realizeClass 设置rw/ro
    • 处理分类: 把分类中的方法、协议、属性添加到类中
    • 加载相应的哈希表

现在只有知道开始调用objc_init才能给这个sNotifyObjcInit赋值才能走通context.notifySingle

  • 全局搜索是完全没有的 image.png
  • 所以先放一下看看那个doInitialization

doInitialization

  • 现在重新回去来到 this->doInitialization(context) 镜像文件初始化 看1657行, image.png

  • 进入doInitialization查看 发现调用了两个方法。1.doImageInit(),2.doModInitFunctions()咱也不知道他们干了啥都看下 image.png

  • 进入doImageInit(context)查看 找到这个 Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide); //Mach-O地址偏移,得到一个函数方法,libSystemInitialized libSystem 初始化必须提前 不懂放弃 image.png

  • 进入doModInitFunctions(context); C++函数处理 - libSystem.B.dylib 'libSystem_initializer:-> - libdispatch.dylib 'libdispatch_init:-> - _os_object_init(libdispatch.dylib)-> - _objc_init(libobjc)

  • 这里的堆栈信息打印了提示我们看这个。
    image.png 进入doModInitFunctions源码实现,这个方法中加载了所有Cxx文件

image.png

  • libsystem中查找libSystem_initializer,查看其中的实现\

image.png

image.png

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

image.png

image.png

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

image.png

  • 所以整体思路可以查看函数调用栈来解释这个流程,以及找到相对应的源码进行学习。

4.流程图解释

cb6f98595737db6ffbe8547a17764090.png 参考博客-月月大佬