上篇文章介绍了 MachO 文件的结构,你可能注意到其中的 LC_LOAD_DYLINKER 是 dyld, LC_MAIN 加载命令就是加载程序的主入口。这篇文章就详细讲讲 App 的加载过程。
MachO 可执行文件类型
Xcode build 出的 .app 包中可以看到一个 exec 可执行文件(所有 .o文件集合),同样是一个 MachO 文件,filetype 就是 MH_EXECUTE 类型。
MachOView中查看如下。
link map
MachO 中重要的信息都在 Section 中。
可以通过 Xcode 开启 Write Link Map File = YES 后生成的 txt 文件来辅助分析 Section。可以帮助你更好的了解 App 的加载。
Object files
这个部分包括的内容如下:
.o文件,也就是.m文件编译后的结果。
.a文件
需要link的framework
前面是文件的编号(section中用到),后面是文件的路径。
Sections
这个区域提供了各个段(Segment)和节(Section)在可执行文件中的位置和大小。这个区域完整的描述克可执行文件中的全部内容,对应 MachO 的 segment 和 section
其中,段分为两种__TEXT
代码段__DATA
数据段__text
节的地址是0x100001A50,大小是0x0002436D,二者相加的就是__stubs
的位置0x100025DBE。
Symbols
Section 部分将二进制文件进行了一级划分。而,Symbols 对 Section 中的各个段进行了二级划分,
例如,对于__TEXT __text
,表示代码段中的代码内容1
2
3
4
5
6
7
8
9
10
11# Symbols:
地址 大小 文件编号 方法名
# Address Size File Name
0x100001A50 0x00000120 [ 2] -[EasyViewController sectionSource]
0x100001B70 0x00001720 [ 2] -[EasyViewController dataSource]
0x100003290 0x00000610 [ 2] -[EasyViewController viewDidLoad]
0x1000038A0 0x00000080 [ 2] -[EasyViewController viewDidAppear:]
0x100003920 0x00000300 [ 2] -[EasyViewController viewWillAppear:]
...
0x100025A60 0x0000035D [ 17] _parseSystemVersionPList
0x100025DBE 0x00000006 [ 18] _CFRunLoopAddObserver // 这里开始是__stubs__Text __stubs
对于__Data __objc_var
搜索 0x100036F60 可以找到如下信息
我们在每次编译过后,生成的 dsym 中,就存储了16进制的函数地址映射。可以通过 MachoView 查看 SymbolString。SymbolString包含了方法段的启始地址。_DWARF __debbug_line
中存储了行号信息_DWARF __debbug_info
和 _DWARF __debbug_frame
dwarf-dump –lookup 就是通过 SymbolString 和 __debbug_line
和 _DWARF __debbug_info
等信息来获取崩溃信息。
实际测试还需要进一步对 DWARF 格式有更多的了解,后续再说。
dyld
App开始启动后,系统首先加载可执行文件 (所有 .o 文件集合),然后加载动态链接库 dyld,dyld是一个专门用来加载动态链接库的库,递归加载所有的依赖动态链接库。
动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,CoreFoundation等。
系统使用动态链接的好处:
- 代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份,方便缓存。
- 易于维护:由于被依赖的 lib 是程序执行时才链接的,所以这些 lib 很容易做更新
dyld(the dynamic link editor), Apple 的动态链接器,所有动态链接库和我们App中的静态库.a和所有类文件编译后的.o文件最终都是由dyld(the dynamic link editor),Apple的动态链接器来加载到内存中。每个image都是由一个叫做ImageLoader的类来负责加载(一一对应).
dyld 加载动态链接库的流程有:
- load dylibs image 读取库镜像文件: 分析所依赖的动态库 -> 找到动态库 MachO 文件 -> 读取 MachO 文件 -> 通过UUID验证文件- ->注册文件签名 -> 调用Segment
启动优化:少非系统库的依赖
合并非系统库
使用静态库,比如把代码加入主程序 - Rebase image & Bind image: ASLR(address space layout randomization 地址空间随机化,每个macho都随机了一个slide)使得可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,之所以需要Rebase,是因为刚刚提到的 ASLR 使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在
__LINKEDIT
中。可以通过MachOView查看:Dynamic Loader Info -> Rebase Info
rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。 rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算.
优化:减少Objc类数量, 减少selector数量
减少C++虚函数数量
加载完macho和动态链接库和进行了地址修正之后,dyld所做的事情完成了大部分. - Objc setup : dyld 回调 Objc Runtime,执行Setup
从
_DATA __objc_classlist
段中获取类信息,注册Objc到一个全局的类的映射表中。
从_DATA __objc_protolist
段中获取中获取Protocol、category等属性与类进行关联,把category的定义插入方法列表 (category registration)
保证每一个selector唯一 (selctor uniquing) nitializers
以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。执行+load方法,循环类和类扩展列表调用+load方法
执行c/c++初始化构造器, 如attribute((constructor)) void SomeInitializationWork()
初始化全局静态变量,非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度
优化:
不是必须在 +load方法中执行的任务放到initialize中
减少不必要的全局静态变量通过可执行文件的 LC_MAIN ,拿到entryoff 再加上MachO的首地址(内核传来的slide偏移)就得到了main函数地址。
主要流程总结
dyld 开始将程序二进制文件初始化
交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
runtime 接手后调用 mapimages 做解析和处理,接下来 loadimages 中调用 callloadmethods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法
至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)。整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存, 动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。