iOS 大师养成之路--应用程序的加载

694 阅读8分钟

1. XCode编译流程介绍

1.1 源文件导入

我们的写的代码,以OC为例子,一般都是a.h,a.m 类的文件。这个编译的顺序是怎么决定的呢,我们查看Xcode中的compile sources下面的列表就看到我们所有的.m文件,列表中的从上到下表示编译的先后顺序。那我们的.h文件到哪去了呢?如果你平时写代码时有观察的话你会发现我们的.m文件中会默认有一个import".h",说明.m文件在编译的时候会自动取导入.h中的内容。

1.2 预编译

在预编译阶段,编译器主要是做词法、语法分析,会帮我们做以下的事情

  • 将所有的“#define“删除,并且展开所有的宏定义。
  • 处理所有的条件预编译指令,比如”#if“、”#ifend“、”#elif“、”else“、”endif“。
  • 处理”#include“预编译指令,将被包含的文件插入到该预编译指令的位置。注意这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  • 删除所有的注释"//""/* */"。
  • 添加行号和文件名标识,便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  • 保留所有的#pragma编译器指令,因为编译器须要使用它们。

1.3 编译

在编译阶段,整个编译的过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生的相应的汇编代码文件。通常我们指的的编译也主要指的是这个阶段是整个程序构建的最核心部分。

1.4 汇编

在汇编阶段主要是编译后的汇编代码转变成机器可以执行的指令,几乎每一句汇编代码都对应着一条机器指令。简单来说就是对照着机器指令的词典翻译了一遍

1.5 链接

在链接阶段,编译器要做的事情就比较复杂了。我们在写代码的时候通常只写相关的业务代码,事实上我们写的代码的每一个.m文件都会编译成一个对应的.o文件,关键这些文件是不能独立执行的。我们可能还引用了许多三方库,还得把系统的动态库也加进来才能形成一个完整的可执行文件。这里不做过多的介绍如果大家有兴趣可以看看(《程序员的自我修养》)总结一下:

  • 把我们自己写的所有的.o文件都合并成一个.o文件
  • 把相关的静态库、动态库的拷贝进来同我们自己的这个.o文件再次合并
  • 把文件都合并了之后就存在地址和空间分配、符号决议、重定位等其他操作

1.6 生成可执行文件

经过链接阶段之后就会生成一个可执行的mach-o文件(以OC 为例,其他语言的未必就是这个格式)

2. APP加载流程

既然可执行文件都是些mach-o二进制文件,那么这些格式化的二进制文件如何在启动的时候进行相关的合作分工达到各司其职协同作战的效果。我在这用先用通俗的语言描述以下这个过程,随后我们再来实际的源码中来梳理整个流程

  • systemlib系统支持库的准备-->我们所有代码运行的底层依赖库
  • dispatchlib GCD分发库的准备-->iOS多线程的底层支撑
  • objclib 所有类的准备-->我们代码得以正确执行的先决条件
  • main()函数入口-->准备条件完毕之后的程序启动入口,我们编写代码的开始起作用的位置

2.1 APP启动

2.1.1 从启动到main()之前的那些勾当

我首先,我准备了一个调试工程,同时还找了几份源码,资料在最后会贴上,有兴趣的读者可以自己去执行一遍研究一下整个流程。我再main()执行之前下了个断点,得到下面这个截图 Screen Shot 2021-07-07 at 8.45.15 AM.png 发现并无可研究的地方,但是没有比main()更早的执行函数了吗?答案是肯定有,看下面的log,在main()之前执行了ViewController的load方法 Screen Shot 2021-07-07 at 8.49.31 AM.png 我们再来看看有没有比load更早的有哪些,不看不知道一看吓一跳。原来在load之前还有这么多不为人知的勾当。

2.1.2 load方法之前的故事

我们下面通过在源码中的主流程探索来还原这个过程(源码完全吃透理解可是一个大工程,这里我只给读者展示关键的流程验证) Screen Shot 2021-07-07 at 8.52.22 AM.png

Screen Shot 2021-07-07 at 9.11.16 AM.png 在dyldlib库的源码中找 _dyld_start (通过在dyld的源码中搜索,我们找到arm64架构的这一段汇编代码) Screen Shot 2021-07-07 at 9.02.56 AM.png

dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) (通过搜索 start( ) Screen Shot 2021-07-07 at 9.05.43 AM.png

dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) (如果已经编译好了,点击就可以跳转到此函数)

Screen Shot 2021-07-07 at 9.06.45 AM.png 点击跳转到dyld::initializeMainExecutable() Screen Shot 2021-07-07 at 9.08.06 AM.png 找到关键调用函数ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) Screen Shot 2021-07-07 at 9.13.35 AM.png 找到下一个调用栈函数ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) Screen Shot 2021-07-07 at 9.15.34 AM.png 接着找下一个ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) Screen Shot 2021-07-07 at 9.16.44 AM.png 接着找函数dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) 实现的位置,但是,线索在这好像断了。*sNotifyObjectInit只知道有这个函数指针,当不知道是在哪赋值的 Screen Shot 2021-07-07 at 9.28.20 AM.png 接着在dyld的源码中全局找函数指针的赋值位置,位置找到了又有新的问题了哪是谁调起了registerObjCNotifiers函数Screen Shot 2021-07-07 at 9.31.25 AM.png 还是在dyld中找registerObjCNotifiers的调用位置,发现还是苹果的老套路做了一个中间层API承接。然后接下来我们就怎么页搜不到_dyld_obc_notify_register的调用位置了。--只能说明一个问题是别的库调用这个方法,看名字也有点道理注册通知肯定是给别人用的嘛。 Screen Shot 2021-07-07 at 9.33.17 AM.png 我们再回过头来看之前的堆栈截图,猜想应该是libobjc库调用的_dyld_obc_notify_register方法。 Screen Shot 2021-07-07 at 9.37.56 AM.png 来去到libobjc库中去查找调用位置,果然一搜就搜到了,同时还把load_images的位置也找到了 Screen Shot 2021-07-07 at 9.43.04 AM.png 点击load_images,查看这个方法的实现源码,发现了call_load_methods看注释,call +load methods。oh!! 终于来到了load方法 Screen Shot 2021-07-07 at 9.46.29 AM.png

2.1.3 objc_init 调用流程探索

一切看是水到渠成,但是好像又有新的疑问?

  • _dyld_obc_notify_register这个函数不是load这个流程中自己调用的,而且应该是先与load注册的,哪到底在哪注册的?
  • _dyld_obc_notify_register是在objc_init函数中被调用起来的,objc_init的这一流程又是在哪个位置开始走的?

带着这两点疑问我们再来回看一下整个从_dyld_start开始的分析流程中是不是遗留了什么?我们先从objc_init Screen Shot 2021-07-07 at 10.18.25 AM.png 我们再回到recursiveInitialization这个步骤中,发现这个doInitialization的操作在发通知之前 Screen Shot 2021-07-07 at 10.19.41 AM.png 我们来摸索一下这个doInitialization的流程 Screen Shot 2021-06-28 at 9.54.06 AM.png 点击进入下一步doModInitFunctions步骤,在这里我们发现这里必须是libsystem最先执行否则程序就会崩溃 Screen Shot 2021-07-07 at 10.29.27 AM.png 接下来我们去libsystem库中找下一步执行流程,的确是libdispatch_init Screen Shot 2021-06-28 at 10.15.32 AM.png 然后再从libdispatch库中找下一个执行点 Screen Shot 2021-06-28 at 10.27.07 AM.png 点击_os_object_init,查看具体执行代码 太兴奋了!找到了我们想要找到的点_objc_init Screen Shot 2021-06-28 at 10.30.07 AM.png 好了看到这估计读者们都有一个大概的头绪了,我用一个脑图给大家总结一下 Screen Shot 2021-07-07 at 11.18.08 AM.png

2.1.4 main()函数是怎么被调起的

最后不知道各位读者有没有这样的疑问,我的main()函数是怎么调起来的,虽然我们已经知道了objc_init以及load函数的的调用流程。放心,已经这么兴奋了可不能掉线,再来摸索一波,我们再看一下当我们把断点放在main()这里的时候,查看一下堆栈信息 Screen Shot 2021-07-07 at 11.29.33 AM.png 从上面的堆栈截图发现是在_start方法中直接调起的,🤦‍♂️看样子_start中还有我们没关注的地方。再回到_start方法中店家直接return的_main函数中发现了一些惊喜的信息。居然就在我们刚探索完的流程后面 Screen Shot 2021-07-06 at 11.15.08 AM.png 我们来看一下这个主程序入口是怎么操作的 Screen Shot 2021-07-06 at 11.18.22 AM.png 诶,Macho段中读的,怎么搞?? 不要怂,我们就看Macho是不是有这个段。把可执行的Macho拖到烂苹果中我发现了对应的LC_MAINScreen Shot 2021-07-06 at 10.51.49 AM.png 但是,怎么说明就是main()函数呢?我们再看看fuction段 Screen Shot 2021-07-06 at 10.58.14 AM.png 简直一模一样的地址,总算完成了到main()的整个流程探索。恭喜各位读者又有了新的发现。

3. 源码资料

github.com/KClichen/ap… 所有分析过程用的源码和工具都在这里,有兴趣的读者可以自行探索一下