本文我们主要探索应用程序的加载流程,也就是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
的加载有一个初步的了解了,本小节主要通过看源码来探索一下加载流程。本小节需要的源码有:
dyld
libobjc
libSystem
libdispatch
源码可直接去苹果Source Browser
下载
dyld
start探索
通过
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)