1. 程序加载原理
系统内核将可执行文件从磁盘中加载到内存中,内存中的二进制文件,我们称之为image镜像文件
之后,系统会加载动态链接器dyld。dyld只会负责动态库的加载,主程序也会作为镜像形式被dyld管理起来
dyld从可执行文件的依赖开始, 递归加载所有依赖的动态库。无论是动态链还是App本身的可执行文件,它们都是image镜像,而每个App都是以image为单位进行加载的
2. 编译过程

- 源文件:
.h、.m、.cpp等文件 - 预编译:替换宏,删除注释,展开头文件,词法分析、语法分析,生成
.i文件 - 编译:转换成汇编语言,生成
.s文件 - 汇编:把汇编语言文件转换为机器码文件,产生
.o文件 - 链接:对
.o文件中引用其他库的地方进行引用,生成最后的可执行文件
3. 静态库 & 动态库
库(Library):就是⼀段编译好的⼆进制代码,可以被系统加载,加上头⽂件就可以供别⼈使⽤
常⽤库⽂件格式:.a、.dylib、.framework、.xcframework、.tdb
3.1 什么时候会⽤到库?
- 某些代码需要给别⼈使⽤,但是我们不希望别⼈看到源码,就需要以库的形式进⾏封装,只暴露出头⽂件
- 对于某些不会进⾏⼤改动的代码,我们想减少编译的时间,就可以把它打包成库。因为库是已经编译好的⼆进制,编译的时候只需要
Link⼀下,不会浪费编译时间
3.2 什么是链接?
库(Library)在使⽤的时候需要链接(Link)
链接的⽅式有两种:
- 静态
- 动态
3.3 什么是静态库?
静态库即静态链接库:可以简单的看成⼀组⽬标⽂件的集合。即很多⽬标⽂件经过压缩打包后形成的⽂件。Windows下的.lib,Linux和Mac下的.a,Mac独有的.framework
缺点:浪费内存和磁盘空间,模块更新困难
3.4 什么是动态库?
与静态库相反,动态库在编译时并不会被拷⻉到⽬标程序中,⽬标程序中只会存储指向动态库的引⽤。等到程序运⾏时,动态库才会被真正加载进来。格式有:.framework、.dylib、.tbd
缺点:会导致⼀些性能损失。但是可以优化,⽐如延迟绑定(Lazy Binding)技术
4. dyld
4.1 dyld是什么?
dyld:动态链接器,加载所有的库和可执行文件
libdyld.dylib:给我们的程序提供在Runtime期间能使⽤动态链接功能
4.2 加载程序的过程
- 调⽤
fork函数,创建⼀个process(进程)调⽤execve或其衍⽣函数,在该进程上加载,执⾏我们的Mach-O⽂件 - 将⽂件加载到内存
- 开始分析
Mach-O中的mach_header,以确认它是有效的Mach-O⽂件 - 验证通过,根据
mach_header解析load commands。根据解析结果,将程序各个部分加载到指定的地址空间,同时设置保护标记 - 从
LC_LOAD_DYLINKEN中加载dyld dyld开始⼯作
4.3 dyld的⼯作是什么?
- 执⾏⾃身初始化配置加载环境
LC_DYLD_INFO_ONLY - 加载当前程序链接的所有动态库到指定的内存中
LC_LOAD_DYLIB - 搜索所有的动态库,绑定需要在调⽤程序之前⽤的符号(⾮懒加载符号)
LC_DYSYMTAB - 在间接符号表(
indirect symbol table)中,将需要绑定的导⼊符号真实地址替换LC_DYSYMTAB - 向程序提供在
Runtime时使⽤dyld的接⼝函数(存在libdyld.dylib中,由LC_LOAD_DYLIB提供) - 配置
Runtime,执⾏所有动态库/image中使⽤的全局构造函数 dyld调⽤程序⼊⼝函数,开始执⾏程序LC_MAIN
4.4 程序启动的初始方法
在ViewController中,加入load方法
@implementation ViewController+ (void)load{NSLog(@"ViewController load方法");}@end
在main.m中,加入C++构造函数
__attribute__((constructor)) void func(){printf("\n C++构造函数:%s \n",__func__);}
在main函数中,增加NSLog打印
int main(int argc, char * argv[]) {NSString * appDelegateClassName;@autoreleasepool {NSLog(@"main函数");// Setup code that might create autoreleased objects goes here.appDelegateClassName = NSStringFromClass([AppDelegate class]);}return UIApplicationMain(argc, argv, nil, appDelegateClassName);}-------------------------//输出结果:ViewController load方法C++构造函数:funcmain函数
load方法→C++构造函数→main函数
main函数为程序入口,但load方法和C++构造函数的执行时机比main函数更早,它们是被谁调用的?
在load方法中设置断点,查看函数调用栈
应用启动时的初始方法,由dyld中的_dyld_start开始的
5. 源码分析
5.1 _dyld_start
打开dyld-852源码,搜索_dyld_start
_dyld_start由汇编代码实现,内部调用dyldbootstrap::start函数
5.2 dyldbootstrap::start
dyldbootstrap::start由C++代码实现
搜索dyldbootstrap,找到命名空间及start函数
来到start函数
- 重定位
dyld,进程启动,它的虚拟内存地址就要进行重定位 - 对于栈溢出的保护
- 初始化
dyld - 调用
dyld的_main函数
5.3 dyld::_main
5.3.1 【第一步】配置环境变量

- 内核检测
- 获取主程序可执行文件
- 获取当前架构的信息

- 设置
MachO Header和ASLR - 设置上下文,全部存储在
gLinkContext对象中

- 配置进程是否受限,苹果进程受
AFMI保护(Apple Mobile File Integrity苹果移动文件保护) - 判断是否强制使用
dyld3 - 判断环境变量,如果发生改变,再次调用
setContext设置上下文。否则检测环境变量,设置默认值

- 在项目中配置
DYLD_PRINT_OPTS、DYLD_PRINT_ENV环境变量,可以进行打印
5.3.2 【第二步】加载共享缓存

- 加载共享缓存,
UIKit、Foundation等系统动态库,都存储在共享缓存中。在iOS中,必须有共享缓存 - 检测共享缓存是否映射到公共区域,调用
mapSharedCache函数,传递ASLR
进入mapSharedCache函数
- 调用
loadDyldCache函数
进入loadDyldCache函数
- 满足条件,依赖库只加载到当前进程
- 如果已经加载共享缓存,不做任何处理
- 否则,首次加载,调用
mapCacheSystemWide函数
加载App之前,首先加载的就是共享缓存。每个App都需要UIKit、Foundation等系统动态库,但程序之前的进程不互通,所以系统动态库存放在共享缓存中
自己写的动态库和其他三方库,不会存储在共享缓存中
5.3.3 【第三步】实例化主程序
中间不论执行dyld2还是dyld3的流程,后面都会执行实例化主程序的代码
dyld加载的第一个image镜像就是主程序
- 调用
instantiateFromLoadedImage函数,传入主程序的MachO Header,ASLR,路径,创建一个ImageLoader实例对象
进入instantiateFromLoadedImage函数
- 调用
instantiateMainExecutable函数,为主可执行文件创建映像,返回一个ImageLoader类型的image对象
进入instantiateMainExecutable函数
- 调用
sniffLoadCommands函数,获取MachO类型文件的Load Command的相关信息,并对其进行各种校验
进入sniffLoadCommands函数
compressed:分析MachO文件获取的值segCount:Segment总数libCount:依赖库总数codeSigCmd:签名encryptCmd:加密
来到sniffLoadCommands函数结尾处
- 程序的
Segment总数,不能超过2552 - 程序的依赖库总数,不能超过
4095
回到instantiateMainExecutable函数
- 根据
compressed判断,使用相应的子类实例化主程序,返回实例对象
回到instantiateFromLoadedImage函数
- 拿到实例化后的
image对象 - 将
image对象添加到image列表中 - 返回
image对象
所以image列表中,第一个image一定是主程序
5.3.4 【第四步】加载插入的动态库
回到_main函数
- 检测代码,检查设备、系统版本等

- 设置加载动态库的版本

- 判断环境变量,是否有插入的动态库
- 如果有,遍历插入的动态库,依次调用
loadInsertedDylib函数
5.3.5 【第五步】链接主程序

- 调用
link函数,链接主程序
进入link函数
- 记录起始时间
- 递归加载主程序依赖的库,完成之后发通知
- 重定向,修正
ASLR - 绑定非懒加载符号
- 绑定弱引用符号

- 递归应用插入的动态库
- 注册
- 记录结束时间
- 计算时间差,当项目配置环境变量,用于显示各步骤耗时
5.3.6 【第六步】链接插入的动态库

- 循环绑定插入的动态库
5.3.7 【第七步】绑定弱引用符号

- 绑定弱引用符号
5.3.8 【第八步】初始化main方法

5.3.9 【第九步】返回主程序入口

- 读取
MachO的LC_MAIN,找到主程序的main函数地址 - 返回
main函数
6. 初始化main方法的流程分析
6.1 initializeMainExecutable函数

- 初始化插入的动态库
- 初始化主程序
6.2 runInitializers函数

- 调用
processInitializers函数
6.3 processInitializers函数

- 对
images调用recursiveInitialization函数,进行递归实例化
6.4 recursiveInitialization函数

- 调用
notifySingle函数
6.5 调用objc中的load_images函数
从函数调用栈来看,在notifySingle函数中,会调用load_images函数
但是在notifySingle函数中,并没有找到load_images的相关线索。并且load_images是objc的函数,在dyld中又是如何调用的?
frame #1: 0x000000019cb943bc libobjc.A.dylib`load_images + 944
6.5.1 notifySingle函数

- 如果
sNotifyObjCInit不为空,使用回调指针,执行一个回调函数
6.5.2 registerObjCNotifiers函数
搜索sNotifyObjCInit,找到它在项目中被赋值的地方
找到registerObjCNotifiers函数,将参数init赋值给sNotifyObjCInit
6.5.3 _dyld_objc_notify_register函数
搜索registerObjCNotifiers,找到函数的调用者
找到_dyld_objc_notify_register函数
搜索_dyld_objc_notify_register,项目中无法找到该函数的调用者
使用符号断点寻址新的线索
设置_dyld_objc_notify_register符号断点,运行项目
- 找到函数调用者,在
objc源码中,被_objc_init调用
6.5.4 _objc_init函数
打开objc4-818.2源码,搜索_objc_init
找到_objc_init函数
6.5.5 load_images函数

- 准备
load方法 - 调用
load方法
6.5.6 prepare_load_method函数
准备load方法
- 整理所有类的
load方法 - 整理所有分类的
load方法
6.5.7 schedule_class_load函数
整理所有类的load方法
- 递归
Superclass,确保父类优先 - 整理所有类的
load方法
add_class_to_loadable_list函数
- 获取
cls的load方法,不存在返回 - 存在记录到数组中,
loadable_classes_used为下标,存储cls和load方法
getLoadMethod函数
- 循环方法列表,方法名称为
load,返回imp
6.5.8 add_category_to_loadable_list函数
整理所有分类的load方法
- 获取分类的
load方法,不存在返回 - 存在记录到数组中,
loadable_categories_used为下标,存储分类和load方法
_category_getLoadMethod函数
- 循环分类下的方法列表,方法名称为
load,返回imp
6.5.9 call_load_methods函数

- 循环调用类和分类的
load方法
sNotifyObjCInit的赋值流程:libobjc:_objc_init→libdyld:_dyld_objc_notify_register→dyld::registerObjCNotifiers→sNotifyObjCInit = init(load_images)
我们找到了sNotifyObjCInit的赋值流程,但是_objc_init又是如何在dyld中被调用的?
6.6 doInitialization函数
打开dyld源码,回到recursiveInitialization函数
进入doInitialization函数
- 分别调用
doImageInit和doModInitFunctions函数
6.8.1 调用_objc_init函数
进入doImageInit函数
- 要求:必须先运行
libSystem初始化器
此时,还未找到_objc_init函数的相关代码,使用符号断点寻址新的线索
设置_objc_init符号断点,运行项目
打开Libsystem源码,搜索libSystem_initializer
进入libSystem_initializer函数
- 调用
libdispatch源码中的libdispatch_init函数
打开libdispatch源码,搜索libdispatch_init
进入libdispatch_init函数
进入_os_object_init函数
- 调用
libobjc源码中的_objc_init函数
_objc_init调用流程:_dyld_start→dyldbootstrap::start→dyld::_main→dyld::initializeMainExecutable→ImageLoader::runInitializers→ImageLoader::processInitializers→ImageLoader::recursiveInitialization→ImageLoaderMachO::doInitialization→ImageLoaderMachO::doModInitFunctions→libSystem:libSystem_initializer→libdispatch:libdispatch_init→libdispatch:_os_object_init→libobjc:_objc_init
6.8.2 调用C++构造函数
进入doModInitFunctions函数
- 遍历执行所有
C++构造函数
6.7 map_images & load_images
在objc源码中,传入_dyld_objc_notify_register函数中的map_images和load_images,它们在dyld中是如何被调用的?
参数经过_dyld_objc_notify_register传递给registerObjCNotifiers函数
进入registerObjCNotifiers函数
map_images赋值给sNotifyObjCMappedload_images赋值给sNotifyObjCInit
6.7.1 sNotifyObjCMapped的调用
搜索sNotifyObjCMapped,只找到一处调用代码
找到notifyBatchPartial函数
搜索notifyBatchPartial,找到函数的调用者
找到registerObjCNotifiers函数,在回调函数执行后,立刻就会调用sNotifyObjCMapped 
6.7.2 sNotifyObjCInit的调用
搜索sNotifyObjCInit,找到两处调用代码
1、找到registerObjCNotifiers函数,在回调函数执行后,立刻就会调用sNotifyObjCInit 
2、找到notifySingle函数,当state为dyld_image_state_dependents_initialized时,调用sNotifyObjCInit
6.7.3 处理系统库的调用流程
处理共享缓存中的系统库,会调用多次recursiveInitialization函数,此时sNotifyObjCInit被回调函数registerObjCNotifiers触发,时机在主程序的image之前,虽然调用load_images函数,但不触发load方法
处理系统库的调用流程:ImageLoader::recursiveInitialization→ImageLoaderMachO::doInitialization→ImageLoaderMachO::doModInitFunctions→libSystem:libSystem_initializer→libdispatch:libdispatch_init→libdispatch:_os_object_init→libobjc:_objc_init→libdyld:_dyld_objc_notify_register→dyld::registerObjCNotifiers→libobjc:load_images
6.7.4 load方法的调用流程
当recursiveInitialization函数处理主程序的image时,调用notifySingle函数,传入state为dyld_image_state_dependents_initialized,调用sNotifyObjCInit
load方法的调用流程:_dyld_start→dyldbootstrap::start→dyld::_main→dyld::initializeMainExecutable→ImageLoader::runInitializers→ImageLoader::processInitializers→ImageLoader::recursiveInitialization→dyld::notifySingle→load_images(回调函数:sNotifyObjCInit)
7. 返回主程序入口的流程分析
在_dyld_start汇编代码的结束位置
- 跳转至
x16寄存器 x16为main函数的地址
在dyld源码中,找到dyldStartup.s汇编代码
- 从
dyld中跳转到主程序入口
8. dyld2 & dyld3
8.1 dyld3闭包模式
在iOS11后,引入dyld3的闭包模式,以回调的方式加载,加载更快,效率更高
在iOS13后,动态库和三方库,也使用闭包模式加载

- 判断
sClosureMode,如果是闭包模式,执行else代码分支 - 配置如何加载
MachO

- 闭包也是实例对象,优先从共享缓存中获取实例对象
- 如果对象不为空,但对象已失效,重新将对象设置为
nullptr

- 再次判断对象是否为空,如果为空,在缓存中获取对象
- 如果缓存中未找到对象,调用
buildLaunchClosure函数创建

- 判断对象不为空,调用
launchWithClosure函数启动,传入闭包对象,返回是否成功的结果 - 如果启动失败并且过期,再创建一次
- 判断再次创建的对象不为空,再次启动
- 如果启动成功,拿到主程序
main的函数,直接返回结果
8.2 dyld2流程
如果不是dyld3的闭包模式,进入dyld2流程
- 不使用
dyld3的闭包模式,将变量设置为0,表示使用旧模式加载 - 把两个回调地址放到
stateToHandlers数组中 - 分配初始化空间,尽量分配足够大的空间,以供后续使用
- 把
dyld加入到UUID的列表中
总结
程序加载原理:
- 系统内核将可执行文件从磁盘中加载到内存中
- 系统会加载动态链接器
dyld dyld从可执行文件的依赖开始, 递归加载所有依赖的动态库
编译过程:
- 源文件:
.h、.m、.cpp等文件 - 预编译:替换宏,删除注释,展开头文件,词法分析、语法分析,生成
.i文件 - 编译:转换成汇编语言,生成
.s文件 - 汇编:把汇编语言文件转换为机器码文件,产生
.o文件 - 链接:对
.o文件中引用其他库的地方进行引用,生成最后的可执行文件
静态库 & 动态库:
- 库(
Library):就是⼀段编译好的⼆进制代码,可以被系统加载,加上头⽂件就可以供别⼈使⽤ - 库在使⽤的时候需要链接(
Link) - 链接的⽅式有两种:静态、动态
- 静态库:可以简单的看成⼀组⽬标⽂件的集合
- 动态库:在编译时并不会被拷⻉到⽬标程序中,运⾏时才会被真正加载进来
dyld:
dyld:动态链接器,加载所有的库和可执行文件libdyld.dylib:给我们的程序提供在Runtime期间能使⽤动态链接功能
源码分析:
_dyld_start:由汇编代码实现
◦ 内部调用dyldbootstrap::start函数
dyldbootstrap::start:由C++代码实现
◦ 初始化dyld
◦ 调用dyld的_main函数
dyld::_main
◦ 【第一步】配置环境变量
◦ 【第二步】加载共享缓存
◦ 【第三步】实例化主程序
◦ 【第四步】加载插入的动态库
◦ 【第五步】链接主程序
◦ 【第六步】链接插入的动态库
◦ 【第七步】绑定弱引用符号
◦ 【第八步】初始化main方法
◦ 【第九步】返回主程序入口
初始化main方法的流程分析:
sNotifyObjCInit的赋值流程:libobjc:_objc_init→libdyld:_dyld_objc_notify_register→dyld::registerObjCNotifiers→sNotifyObjCInit = init(load_images)_objc_init调用流程:_dyld_start→dyldbootstrap::start→dyld::_main→dyld::initializeMainExecutable→ImageLoader::runInitializers→ImageLoader::processInitializers→ImageLoader::recursiveInitialization→ImageLoaderMachO::doInitialization→ImageLoaderMachO::doModInitFunctions→libSystem:libSystem_initializer→libdispatch:libdispatch_init→libdispatch:_os_object_init→libobjc:_objc_init- 处理系统库的调用流程:
ImageLoader::recursiveInitialization→ImageLoaderMachO::doInitialization→ImageLoaderMachO::doModInitFunctions→libSystem:libSystem_initializer→libdispatch:libdispatch_init→libdispatch:_os_object_init→libobjc:_objc_init→libdyld:_dyld_objc_notify_register→dyld::registerObjCNotifiers→libobjc:load_images load方法的调用流程:_dyld_start→dyldbootstrap::start→dyld::_main→dyld::initializeMainExecutable→ImageLoader::runInitializers→ImageLoader::processInitializers→ImageLoader::recursiveInitialization→dyld::notifySingle→load_images(回调函数:sNotifyObjCInit)
返回主程序入口的流程分析:
- 在
dyld中,由汇编代码实现,跳转到主程序入口
dyld2 & dyld3:
dyld3闭包模式dyld2流程
