本文我们主要探索应用程序的加载流程,也就是main方法之前,链接器都做了什么。了解这些对我们项目的启动优化有很大帮助。
编译过程和库
编译的过程
我们知道库是一种可执行文件,从源代码到可执行文件工经历了下面几个步骤:
源文件:主要就是我们写的代码,.h、.m、.cpp等文件。预编译:主要处理哪些源代码文件中以#开始的预编译指令,比如#include、#define、删除所有的注释//和/* */、添加行号和文件名标识、保留所有的#pragma编译期指令、产生.i文件。编译:将预处理完的文件进行词法分析、语义分析及优化后输出汇编代码文件即.s文件。汇编:将汇编代码转变成机器可以执行的指令即.o文件。链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
动态库和静态库
我们项目中经常会使用到动态库和静态库,它们的区别是:
- 静态库:在链接阶段会将汇编生成的目标文件和引用的库一起链接打包到可执行文件中。即静态库在
链接阶段就被载入了。- 优点:编译完成之后的目标程序没有
外部依赖,可以直接运行。 - 缺点:静态库可以会有多份,会导致
目标程序体积增加,对内存、性能、速度消耗较大。
- 优点:编译完成之后的目标程序没有
- 动态库:程序编译并不会链接到目标代码中,在程序可执行文件里面会保留对动态库的引用,在程序
运行时才被载入,苹果大部分官方的库都是动态库。- 优点:
- 可以减少
App包体积大小:因为不需要拷贝到目标程序中,所以不会影响目标程序的体积. - 共享内存,节约资源:同一份库可以被多个程序使用。
- 可以更新动态库,而目标程序不需要重新编译:这是因为动态库
运行时才载入,可以随时对库进行替换,而·不需要重新编译代码。
- 可以减少
- 缺点:由于是
运行时载入会带来一部分性能损失,使用动态库使得程序依赖于外部环境,如果环境缺少了动态库程序就无法运行。
- 优点:
dyld是什么
dyld是动态链接器,目前最新的版本是dyld3,我们首先看一下dyld的版本演变。在dyld之前,NeXT使用的是静态二进制数据。
dyld版本演变
dyld1.0(1996-2004)- 包含在
NeXTStep 3.3中 - 历史早于标准化
POSIX dlopen()的调用 macOS 10之前编写第三方包装器用来以支持标准Unix软件,但是这些包装器并不能完美的支持相同的语义,在边界情况不能正常工作。- 在大多数使用
C++动态库的系统之前编写的。 - 在
mac OS 10.0增加了预绑定。使用预绑定技术为系统中所有的dylib和我们的程序找到固定地址,动态加载器将会加载这些地址的所有内容。
- 包含在
dyld2.0(2004-2007)- 包含在
macOS Tiger中 - 相比
1.0版本是完全重写(Complete rewrite)的。 - 支持了
C++初始化语义,扩展了mach-o格式。 - 有完整的本地(native)
dlopen和dlsym的实现。 2.0版本设计的目标是提高速度,仅进行有限的及安全性检查。- 提高了安全性。
- 减少
预编译的工作量(时长)
- 包含在
dyld2.x(2007-2017)- 增加了更多的基础架构和平台,比如
x86、x86_64、arm、arm64、iOS、tvOS、watchOS。 - 增强了安全性。增加
代码签名和ASLR(地址空间配置随机加载),增加了mach-o头文件中的项目边界检查功能它可以避免恶意二进制数据的加入。 - 增强了性能:用
共享缓存代替了预绑定。共享缓存是一个包含大部分系统dylib的单文件(Single file),可以节省大量内存,它实际是预链接库。
- 增加了更多的基础架构和平台,比如
dyld3(2017-至今)- 完全改变
动态链接器的概念 - 默认适用于大部分
Apple OS系统应用。 - 完全替代了
dyld2.x。 - 提高了性能,尽量提高启动速度和运行速度。
- 提高安全性:将大多数
dyld移出进程,允许部分dyld驻留在进程之中,驻留部分尽可能小,从而减少受攻击的面积。 - 可测试性和可靠性
- 完全改变
dyld 2和dyld 3加载过程的区别
dyld 2的加载过程Parse mach-o headers:分析mach-o文件,通过分析mach-o文件弄清楚需要那些库,这些库可能需要其他库,所以会进行递归分析,直到获得所有dylib的完整图。一般普通iOS程序需要3-600个dylib,数据庞大,需要进行大量的处理。Map mach-o files:映射所有mach-o文件,将它们放入地址空间Perform symbol lookups:执行符号查找,例如程序使用了printf函数,将会查找printf是否在库系统中,然后找到它的地址,将它复制到你的程序中的函数指针。Bind and rebase:绑定和基址重置,复制3步的指针,由于使用随机地址,所有指针必须使用基址。Run initializers:运行初始化器,接下来准备执行main函数。
流程图如下图,其中红色表示影响性能和安全性的步骤:
dyld3加载过程dyld3包括三个部分:An out of process MachO parser/compiler:进程外mach-o分析器和编译器。Resolves all search paths, @rpaths, environment variables:解析所有搜索路径、rpaths、环境变量。Parses the mach-o binaries:分析mach-o二进制数据Perform symbol lookups:执行符号查找,例如程序使用了printf函数,将会查找printf是否在库系统中,然后找到它的地址,将它复制到你的程序中的函数指针。Creates a launch closure with results:创建收尾处理
An in-process engine that runs launch:进程内引擎执行启动收尾处理,进驻在内存中。Validates launch closure:检查启动收尾处理是否正确。Maps in all dylibs:映射到所有的dylib中Applies fixups:应用修正Run initializers:运行初始化器,接下来准备执行main函数。
A launch closure caching service:启动收尾缓存服务。大部分程序启动会使用缓存但始终不需要调用进程外mach-o分析器和编译器,启动收尾比mach-o更简单,启动收尾文件是内存映射文件,不需要用复杂的方法进行分析从而提高速度。流程图(来源WWDC ppt)如下:
注:本小节内容来源于WWDC2017 App Startup Time: Past, Present, and Future感兴趣的童鞋可以查看视频。
dyld加载流程分析
通过上一小节我们其实对dyld的加载有一个初步的了解了,本小节主要通过看源码来探索一下加载流程。本小节需要的源码有:
dyldlibobjclibSystemlibdispatch
源码可直接去苹果Source Browser下载
dyldstart探索
通过
main函数因为我们程序的入口是
main函数,dyld是在main之前执行的,我们很容易想到在main函数打一个断点,然后查看调用堆栈信息来查看dyld的具体调用方法:1
2
3
4(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x000000010000339b DyldTest`main(argc=3, argv=0x00007ffeefbff500) at main.m:13:5
frame #1: 0x00007fff6e7d3cc9 libdyld.dylib`start + 1通过这个方式我们看到了
start,但是通过打符号断点并没有找到start方法,所以这种方式无效。通过
load方法根据我们的经验,我们知道
load方法是在main函数之前执行的,我们通过load方法能不能找到dyld的入口呢,心动不如行动,我们试一下,在ViewController类加入load方法,打上断点:1
2
3
4
5
6
7
8
9
10
11
12(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001000032d7 DyldTest`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:19:5
frame #1: 0x00007fff6d61e560 libobjc.A.dylib`load_images + 1529
frame #2: 0x000000010001626c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 418
frame #3: 0x0000000100029fe9 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 475
frame #4: 0x00000001000280b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #5: 0x0000000100028154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #6: 0x00000001000166a8 dyld`dyld::initializeMainExecutable() + 199
frame #7: 0x000000010001bbba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
frame #8: 0x0000000100015227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
frame #9: 0x0000000100015025 dyld`_dyld_start + 37通过这个堆栈我们看到了
_dyld_start就是dyld开始的函数,我们依次探讨堆栈里的方法。
_dyld_start
我们首先在dyld源码中搜索_dyld_start,发现是一段汇编代码
通过注释我们可以看到,调用的是dyldbootstrap的start函数。
dyldbootstrap::start
我们在源码中搜索dyldbootstrap找到命名空间,继续查找start函数
1 | // This is code to bootstrap dyld. This work in normally done for a program by dyld and crt. |
这个函数关键是最后一行,调用了dyld::_main。
dyld::_main
这个方法很长(900+行),可以从返回值倒退看这个方法都做了什么。方法太长我们省略大部分代码(因为返回值和mainExecutable相关,所以截取的代码基本都和mainExecutable相关):
1 | uintptr_t |
大体流程就是:
- 配置环境变量
- 检查共享缓存是否开启,以及共享缓存是否映射到共享区域
- 主程序初始化即
instantiateFromLoadedImage - 插入动态库
- link主程序
- link动态库
- 弱符号绑定
- 执行初始化方法
- 主程序入口
dyld::initializeMainExecutable
主要是循环遍历执行runInitializers
1 | void initializeMainExecutable() |
ImageLoader::runInitializers
核心代码是调用processInitializers方法
1 | void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo) |
ImageLoader::processInitializers
对镜像列表调用recursiveInitialization函数进行递归实例化
1 | void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread, |
ImageLoader::recursiveInitialization
主要是加载完镜像后通知出去
1 | void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize, |
dyld::notifySingle
1 | static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo) |
其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());,我们全局搜索sNotifyObjCInit并没有实现,但是有赋值操作:
1 | void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped) |
registerObjCNotifiers是在_dyld_objc_notify_register调用,而_dyld_objc_notify_register函数是在libobjc源码_objc_init代用的,所以sNotifyObjCInit的赋值的就是objc中的load_images,而load_images会调用所有的+load方法。所以综上所述,notifySingle是一个回调函数,所以我们继续看load_images方法
1 | void _objc_init(void) |
load_images
1 | load_images(const char *path __unused, const struct mach_header *mh) |
方法比较简单,主要调用了call_load_methods方法
1 | void call_load_methods(void) |
call_load_methods方法的核心就是循环调用load方法。
###总结
load的源码链为:_dyld_start –> dyldbootstrap::start –> dyld::_main –> dyld::initializeMainExecutable –> ImageLoader::runInitializers –> ImageLoader::processInitializers –> ImageLoader::recursiveInitialization –> dyld::notifySingle(是一个回调处理) –> sNotifyObjCInit –> load_images(libobjc.A.dylib)