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++构造函数:func
main函数
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
赋值给sNotifyObjCMapped
load_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
流程