dailyLearning -- 程序启动原理

艾拉

一个 iOS App 的 main 函数位于 main.m 中,这是我们熟知的程序入口。但对 objc 了解更多之后发现,程序在进入我们的 main 函数前已经执行了很多代码,比如熟知的动态库的加载, runtime 的初始化, + load 方法等。本文将跟随程序执行顺序,刨根问底,从 dyld 到 runtime ,看看程序从启动到 main 函数执行之前都发生了什么。

main之前的加载过程
  • Mach-O 加载
  • 从 dyld 开始
    1. 加载程序相关依赖库, 并对这些库进行链接;
    2. 调用每个依赖库的初始化方法,在这一步,runtime被初始化;
    3. 将程序依赖的动态链接库递归加载进内存;
    4. runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法;
    5. dyld返回main函数地址,main函数被调用;
  • main函数

Mach-O 加载

关于 Mach-O 文件这里 有一篇 文章介绍 Mach-O 文件.
Mach-O文件格式是 OS X 与 iOS 系统上的可执行文件格式,像我们编译过程产生的 .O 文件,以及程序的可执行文件,动态库等都是Mach-O文件。它的结构如下:

dailyLearning -- 程序启动原理_第1张图片
mach-o文件

有如下几个部分组成:

Header:保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands的个数等等。
LoadCommands:可以理解为加载命令,在加载Mach-O文件时会使用这里的数据来确定内存的分布以及相关的加载命令。比如我们的main函数的加载地址,程序所需的dyld的文件路径,以及相关依赖库的文件路径。
Data: 这里包含了具体的代码、数据等等。

系统加载程序可执行文件后,通过分析文件来获得dyld所在路径来加载dyld,然后就将后面的事情甩给 dyld 了。

从 dyld 开始

  • dyld: (the dynamic link editor)动态链接器,其源码是开源的。
    dailyLearning -- 程序启动原理_第2张图片
    dyld::_main函数源码

    一切源于 dyldStartup.s 这个文件,其中用汇编实现了名为 __dyld_start 的方法,汇编太生涩,它主要干了两件事:

调用 dyldbootstrap::start() 方法(省去参数)
上个方法返回了main 函数地址,填入参数并调用main 函数
这个步骤随手就能验证出来,设置一个符号断点断在_objc_init:

dailyLearning -- 程序启动原理_第3张图片

这个函数是runtime的初始化函数,后面会提到。程序运行在很早的时候断住,这时候看调用栈:
dailyLearning -- 程序启动原理_第4张图片

看到了栈底的 dyldbootstrap::start() 方法,继而调用了 dyld::_main() 方法,其中完成了刚才说的递归加载动态库过程,由于 libSystem 默认引入,栈中出现了 libSystem_initializer 的初始化方法。

  • ImageLoader: 用于辅助加载特定可执行文件格式的类,程序中对应实例可简称为image(如程序可执行文件,Framework库,bundle文件)。

两步走:

  1. 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻);
  2. 再从可执行文件 image 递归加载所有符号;

当然所有这些都发生在我们真正的 main 函数执行前。

  • runtime 与 +load
    刚才说到 libSystem 是若干个系统 lib 的集合,所以它只是一个容器 lib 而已,而且它也是开源的,里面实质上就一个文件,init.c,由 libSystem_initializer 逐步调用到了 _objc_init,这里就是 objc 和 runtime 的初始化入口。

除了 runtime 环境的初始化外,_objc_init中绑定了新 image 被加载后的 callback:

dyld_register_image_state_change_handler(
dyld_image_state_bound, 1, &map_images);
dyld_register_image_state_change_handler(
dyld_image_state_dependents_initialized, 0, &load_images);

可见 dyld 担当了 runtime 和 ImageLoader 中间的协调者,当新 image 加载进来后交由 runtime 去解析这个二进制文件的符号表和代码。继续上面的断点法,断住神秘的 +load 函数:
dailyLearning -- 程序启动原理_第5张图片

清楚的看到整个调用栈和顺序:

dyld开始将程序二进制文件初始化
交由ImageLoader读取image,其中包含了我们的类、方法等各种符号
由于runtimedyld绑定了回调,当 image加载到内存后,dyld会通知 runtime 进行处理
runtime接手后调用map_images 做解析和处理,接下来 load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层级依次调用Class+load方法和其Category+load方法
至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime所管理,再这之后,runtime 的那些方法(动态添加Class、swizzle等等才能生效)

main函数

当所有的依赖库库的 lnitializer 都调用完后,dyld::main函数会返回程序的 main 函数地址,main 函数被调用,从而代码来到了我们熟悉的程序入口。

结语

  • 整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,
  • 动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。
    这里只是简单了概括了从程序启动->dyld加载依赖库->runtime初始化->main 的过程。

参考:
iOS 程序 main 函数之前发生了什么
iOS程序启动->dyld加载->runtime初始化(初识)

你可能感兴趣的:(dailyLearning -- 程序启动原理)