[iOS] dyld加载流程
本文的目的主要是分析 dyld
的加载流程,了解一下在main
函数之前,底层还做了哪些事情。
0. 引子
-
创建一个项目,在
image.jpegViewController
中重写了load
方法,在main
中加了一个C++
方法,即kcFunc
,请问下面的输出顺序是什么?
-
运行程序,查看
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.png2. dyld
根据dyld
源码,以及libobjc
、libSystem
、libdispatch
源码协同分析。
2.1 什么是 dyld
?
dyld(the dynamic link editor)
是苹果的动态链接器,是苹果操作系统的重要组成部分,在 app
被编译打包成可执行文件格式的 Mach-O
文件后,交由 dyld
负责链接,加载程序。
共享缓存机制
:在iOS
系统中,每个程序依赖的动态库都需要通过dyld
一个一个加载到内存,然而,很多系统库几乎都是每个程序都会用到的,如果每个程序运行的时候都重复的去加载一次,肯定会运行缓慢,所以为了优化启动速度,提高程序性能
,就有了共享缓存机制。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/
目录下,按不同的架构保存分别保存着。
所以App
的启动流程图如下:
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
架构,如下:
发现其调用了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 <rdar://46878536>
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_header
是Mach-O
文件的头部,而dyld
加载的文件就是 Mach-O文件。
3.3 dyld::_main
dyld::_main
的源码实现很长,如果对 dyld
加载流程不太了解,可以根据_main
函数的返回值进行反推,这里分部分进行介绍。
-
[第一步:环境变量配置]
根据环境变量设置相应的值以及获取当前运行架构。
image.jpeg -
[第二步:共享缓存]
image.jpeg
检查是否开启了共享缓存
,以及共享缓存是否映射到共享区域,例如UIKit、CoreFoundation
等。
-
[第三步:主程序的初始化]
image.jpeg
调用instantiateFromLoadedImage
函数实例化了一个ImageLoader
对象
-
[第四步:插入动态库]
image.jpeg
遍历DYLD_INSERT_LIBARIES
环境变量,调用loadInsertedDylib
加载
-
[第五步:link主程序]
image.jpeg -
[第六步:link 动态库]
image.jpeg -
[第七步:弱符号绑定]
image.jpeg -
[第八步:执行初始化方法]
image.jpeg -
[第九步:寻找主程序入口,即
image.jpegmain
函数]
从Load Command
读取LC_MAIN入口
,如果没有,就读取LC_UNIXTHREAD
,这样就来到了我们所熟悉的main
函数了。
我们接下来主要分析一下[第三步] 和 [第八步]。
3.3.1 [第三步] 主程序初始化
sMainExecutable
表示主程序变量,查看其赋值,是通过instantiateFromLoadedImage
方法初始化的。
进入instantiateFromLoadedImage
源码,其中创建一个 ImageLoader
实例对象,通过 instantiateMainExecutable
方法创建:
进入instantiateMainExecutable
源码,其作用是为主可执行文件创建映像,返回一个ImageLoader
类型的image
对象,即主程序。其中sniffLoadCommands
函数时获取Mach-O
类型文件的Load Command
的相关信息,并对其进行各种校验:
3.3.2 [第八步] 执行初始化方法
进入initializeMainExecutable
源码,主要是循环遍历,都会执行runInitializers
方法
全局搜索runInitializers(cons
,找到如下源码,其核心代码是 processInitializers
函数的调用
进入processInitializers
函数的源码实现,其中对镜像列表调用 recursiveInitialization
函数进行递归实例化:
全局搜索recursiveInitialization(cons
,其源码实现如下:
在这里,我们分成两部分探索,一部分是notifySingle
函数,一部分是doInitialization
函数,首先探索notifySingle
函数
3.3.2.1 notifySingle函数
全局搜索notifySingle(
,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
全局搜索sNotifyObjCInit
,发现没有找到该实现,但是有赋值操作
搜索registerObjCNotifiers
在哪里调用了,发现在_dyld_objc_notify_register
进行了调用:
注意:
_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
的源码实现:
接着看call_load_methods
源码实现,可以发现其核心是通过 do-while
循环调用+load
方法的:
进入call_class_loads
源码实现,看到这里调用的 load
方法证实了我们之前提到的类的load
方法:
所以,load_images
调用了所有类的 load
函数,以上源码分析过程正好对应堆栈的打印信息:
总结:
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
函数
进入doInitialization
函数的源码实现:
这里也需要分成两部分,一部分是
doImageInit
函数,另一部分是doModInitFunctions
函数
-
进入
image.jpegdoImageInit
源码实现,其核心主要是for
循环加载方法的调用,这里需要注意的一点是,libSystem的初始化必须先运行
-
进入
image.pngdoModeInitFuncations
源码实现,这个方法中加载了所有Cxx
文件
我们在刚才测试程序的 C++
方法处加一个断点,看下堆栈信息:
到了这里还是没有找到_objc_init
的调用,我们可以通过_objc_init
加一个符号断点来查看调用_objc_init
前的堆栈信息:
-
image.jpeg_objc_init
加一个符号断点,查看_objc_init
的堆栈信息:
发现_objc_init的调用是在 libsystem 中。 -
在
image.jpeglibsystem
的源码中查找libSystem_initializer
,查看其中的实现:
-
根据前面的堆栈信息,我们发现走的是
image.jpeglibSytem_initializer
中会调用libdispatch_init
函数,而这个函数的源码是在libdispatch
开源库中的,在libdispatch
中搜索libdispatch_init
:
进入_os_object_init
源码实现,其源码实现调用了_objc_init
函数:
结合上面的分析,从初始化_objc_init
注册的_dyld_objc_notify_register
的参数 2,即load_images
,到sNotifySingle
-->sNotifyObjCInie=参数2
到sNotifyObjcInit()
调用,形成了一个闭环。
所以可以简单的理解为 _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 [第九步]寻找主函数入口
-
汇编调试,可以看到,来到了
image.jpeg[ViewController load]
方法
-
继续执行,来到
image.jpegkcFunc
的C++
函数:
-
点击
image.jpegstepover
,继续往下,跑完了整个流程,会回到_dyld_start
,然后调用main()
函数,通过汇编完成main
的参数赋值工作:
3.4 dyld
源码实现:
image.jpeg
image.jpeg
3.5 总结
所以,综上所述,最终dyld
加载流程,如下图所示,图中也诠释了前文中的问题:为什么是load-->Cxx-->main
的调用顺序: