【iOS应用启动(一)】dyld与main函数

语言: CN / TW / HK

theme: channing-cyan

Hi 👋

| 我的个人项目 | 扫雷Elic 无尽天梯 | 梦见账本 | |:----:|:----:|:----:| |类型|游戏|财务| |AppStore|Elic|Umemi|

本文基于 dyld-832.7.3objc4-818.2 源码

前言

每个应用程序都会依赖很多的库,每当应用程序启动,都会将MachO中的可执行文件加载到内存中。

那么这个过程是怎样的呢?

iOS应用启动流程-简要加载过程.png

一、 切入点

应用程序的入口是 main 函数,那么 main 函数之前做了什么呢?

我们在 main 函数下个断点:

start-dyld01.png

(lldb) bt * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 * frame #0: 0x00000001001f9ef8 ObjcMsgSend`main(argc=1, argv=0x000000016fc0b878) at main.m:17:50 frame #1: 0x00000001a2d56140 libdyld.dylib`start + 4

发现并没有有用的信息。那么在main之前下断点试试吧。

start-dyld02.png

通过分析堆栈信息发现切入点 _dyld_start

二、 dyld-动态链接器

2.1 _dyld_start

dyld源码中找到了对应实现,为汇编代码,我们找到一处重要的注释:

start-dyld03.png

dyldbootstrap::start

start-dyld04.png

前面都是进行一些配置,最后一个看名字就很重要的 dyld::_main 我们进去看看

2.2 dyld::_main

一千多行代码,果然很重要。

通过注释我们也可以看出:

  • 这里是 dyld 的入口
  • 最终返回程序的 main() 函数

因为代码非常长,我对于关键点做了一些注释方便大家对照源码进行查看

start-dyld05.png

源码流程注释对照

  • 只要设置了这两个环境变量参数,在App启动时就会打印相关参数、环境变量信息

start-dyld17.png

  • 加载共享缓存

start-dyld16.png

  • 为主程序初始化 ImageLoader

start-dyld15.png

  • 现在共享缓存已经加载完毕了,设置版本化的dylib覆盖

start-dyld14.png

  • 加载所有插入的库,越狱插件在这里加入
  • 记录插入的库的数量,以便统一搜索将先查看插入的库,然后是main,然后是其他。

start-dyld13.png

  • 链接主程序

start-dyld12.png

  • 链接所有插入的库。链接主可执行文件后执行此操作,以使插入的dylib(例如libSystem)不在程序使用的dylib的前面

start-dyld11.png

  • 只有插入的库可以插入。绑定所有插入的库后,注册插入信息,以便链接工作

start-dyld10.png

  • 绑定并通知主要可执行文件,现在插入的已被注册
  • 绑定并通知现在已插入的已插入image已被注册

start-dyld09.png

  • 执行所有初始化方法
  • 通知任何监视过程此过程即将进入main()

start-dyld08.png

  • 查找main指针

start-dyld07.png

2.3 sMainExecutable

从上面的流程分析中可以看出,最终returnmain函数指针 来自于 sMainExecutable

可以定位 sMainExecutable 的初始化地方

start-dyld15.png

2.4 initializeMainExecutable 初始化主程序

核心源码及流程注释

```C++ void initializeMainExecutable() {

pragma mark - Ryukie 记录一下,进入初始化流程了

// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;

pragma mark - Ryukie 初始化所有插入的库

// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
    for(size_t i=1; i < rootCount; ++i) {
        sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
    }
}

pragma mark - Ryukie 运行主要可执行文件的初始化程序及其依赖的

// run initializers for main executable and everything it brings up 
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

...

} ```

结合前面我们在 +load 处断点的堆栈信息可以得到验证:

start-dyld18.png

主线过程我们就分析到这里,细节的流程后面的文章会继续进行分析

三、 总结&流程图

dyld:_main.png

  • 程序执行从 _dyld_start 开始
  • 进入 dyld_main 函数
  • 配置环境变量以及 rebase_dyld
  • 加载共享缓存
  • 系统的动态库都在这里了
  • dyld2/dyld(ClosureMode) 决定以哪种模式继续进行
  • 实例化主程序ImageLoaderMachO,加入到allImages
  • 到此共享缓存加载完毕,设置版本化的dylib覆盖
  • Now that shared cache is loaded, setup an versioned dylib overrides
  • 加载插入的库
  • 越狱插件在这里加入
  • 记录插入的库的数量,以便统一搜索将先查看插入的库,然后是main,然后是其他。
  • record count of inserted libraries so that a flat search will look at inserted libraries, then main, then others.
  • 链接主程序
  • 绑定符号(非懒加载、弱符号)等
  • 链接所有插入的库
  • 链接主可执行文件后执行此操作,以使插入的dylib(例如libSystem)不在程序使用的dylib的前面
  • 只有插入的库可以插入。绑定所有插入的库后,注册插入信息,以便链接工作
  • 绑定并通知主程序可执行文件,现在插入的已被注册
  • 绑定并通知现在已插入的库已插入的Image已被注册
  • 执行所有初始化方法:ImageLoader
  • 初始化所有插入的库、运行主要可执行文件的初始化程序及其依赖
  • ImageLoader:
    • runInitializers:
      • processInitializers:
      • ecursiveInitialization:
        • notifySingle:
        • 此函数执行一个回调,此回调是_objc_init初始化时服饰的一个函数load_images
          • load_images里执行class_load_metgods函数
          • class_load_metgods里调用call_class_loads函数:循环调用各个类的load函数
        • doModInitFunction
        • 内部会调用全局C++对象的构造函数__attribute__((constructor))的C函数
  • 通知任何监视过程此过程即将进入main()
  • 得到main()指针并return