dyld
:动态链接器,加载所有的库和可执行文件
加载App时的函数调用栈
搭建空项目
dyldDemo
,在main
函数上设置断点
真机运行项目,只有
start
和main
两个函数调用栈,显然是不合理的
想查看完整的函数调用栈,需要在
main
函数调用前,在load
函数上设置断点打开
ViewController
,写入load
函数,设置断点
真机运行项目,使用
bt
命令,查看完整的函数调用栈dyldDemo`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:23:1 libobjc.A.dylib`load_images + 944 dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 464 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92 dyld`dyld::initializeMainExecutable() + 216 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, >dyld3::MachOLoaded const*, unsigned long*) + 396 dyld`_dyld_start + 56
一切的开端,由
dyldbootstrap
命名空间下的start
函数开始
start函数
打开
dyld
源码,搜索dyldbootstrap
,找到dyldInitialization.cpp
文件打开
dyldInitialization.cpp
文件,找到start
函数
1
:重定位dyld
,进程启动,它的虚拟内存地址就要进行重定位2
:对于栈溢出的保护3
:初始化dyld
4
:调用dyld
的_main
函数在
start
函数中,最为重要的就是最后一步,调用dyld
的_main
函数
打开
dyld2.cpp
文件,找到_main
函数
- 内核检查代码
1
:主程序可执行文件2
:设置HostCPU
等信息
3
:设置可执行文件的Header
,设置ASLR
- 前面的
100000000
是__PAGEZERO
中的4G
虚拟缓存- 后面的
4140000
是ASLR
随机地址,每次加载MachO
都不一样
setContext
:设置上下文
- 全部存储在
gLinkContext
对象中
1
:配置进程是否受限,苹果进程受AFMI
保护(Apple Mobile File Integrity
苹果移动文件保护)2
:判断是否强制使用dyld3
3
:判断环境变量,如果发生改变,再次调用setContext
设置上下文。否则检测环境变量,设置默认值
- 在项目中配置
DYLD_PRINT_OPTS
、DYLD_PRINT_ENV
环境变量,可以进行打印
1
:加载共享缓存,UIKit
、Foundation
等系统动态库,都存储在共享缓存中。在iOS
中,必须有共享缓存2
:调用mapSharedCache
函数,传递ASLR
加载共享缓存
进入
mapSharedCache
函数
- 调用
loadDyldCache
函数
进入
loadDyldCache
函数
1
:满足条件,依赖库只加载到当前进程2
:如果已经加载共享缓存,不做任何处理3
:首次加载,调用mapCacheSystemWide
函数
加载
App
之前,首先加载的就是共享缓存。每个App
都需要UIKit
、Foundation
等系统动态库,但程序之前的进程不互通,所以系统动态库存放在共享缓存中加载逻辑,根据上述三种情况进行判断
自己写的动态库和其他三方库,不会存储在共享缓存中
dyld3闭包模式
在
iOS11
后,引入dyld3
的闭包模式,以回调的方式加载,加载更快,效率更高在
iOS13
后,动态库和三方库,也使用闭包模式加载回到
_main
函数
1
:判断sClosureMode
,如果是闭包模式,执行else
代码分支2
:配置如何加载MachO
3
:闭包也是实例对象,优先从共享缓存中获取实例对象
1
:如果对象不为空,但对象已失效,重新将对象设置为nullptr
2
:再次判断对象是否为空,如果为空,在缓存中获取对象3
:如果缓存中未找到对象,调用buildLaunchClosure
函数创建
1
:判断对象不为空,调用launchWithClosure
函数启动,传入闭包对象,返回是否成功的结果2
:如果启动失败并且过期,再创建一次3
:判断再次创建的对象不为空,再次启动4
:如果启动成功,拿到主程序main
的函数,直接返回结果
dyld2流程
如果不是
dyld3
的闭包模式,进入dyld2
流程
1
:不使用dyld3
的闭包模式,将变量设置为0
,表示使用旧模式加载2
:把两个回调地址放到stateToHandlers
数组中3
:分配初始化空间,尽量分配足够大的空间,以供后续使用4
:把dyld
加入到UUID
的列表中
实例化主程序
不论执行
dyld2
还是dyld3
的流程,后面都会执行实例化主程序的代码实例化主程序,第一个靠
dyld
加载的就是主程序
- 调用
instantiateFromLoadedImage
函数,传入主程序的Header
,ASLR
,路径
进入
instantiateFromLoadedImage
函数
- 调用
instantiateMainExecutable
函数,返回image
对象
进入
instantiateMainExecutable
函数
- 调用
sniffLoadCommands
函数,读取Load Commands
数据
进入
sniffLoadCommands
函数
compressed
:分析MachO
文件获取的值segCount
:Segment
总数libCount
:依赖库总数codeSigCmd
:签名encryptCmd
:加密来到
sniffLoadCommands
函数结尾处
1
:程序的Segment
总数,不能超过255
2
:程序的依赖库总数,不能超过4095
回到
instantiateMainExecutable
函数
- 根据
compressed
判断,使用相应的子类实例化主程序,返回实例对象
回到
instantiateFromLoadedImage
函数
- 拿到实例化后的
image
对象- 将
image
对象添加到image
列表中- 返回
image
对象所以
image
列表中,第一个image
一定是主程序
回到
_main
函数
- 检测代码,检查设备、系统版本等
- 设置加载动态库的版本
加载动态库
1
:判断环境变量,是否有插入的动态库2
:如果有,遍历插入的动态库,依次调用loadInsertedDylib
函数3
:调用link
函数,链接主程序
进入
link
函数
1
:记录起始时间2
:递归加载主程序依赖的库,完成之后发通知3
:重定向,修正ASLR
4
:绑定非懒加载符号5
:绑定弱引用符号
1
:递归应用插入的动态库2
:注册3
:记录结束时间4
:计算时间差,当项目配置环境变量,用于显示各步骤耗时
回到
_main
函数
- 读取动态库使用
i + 1
,因为起始位置是主程序- 链接动态库
- 判断条件不满足,使用
goto
语句,跳回到reloadAllImages
代码处,重新加载镜像
- 循环绑定插入的动态库
- 绑定弱引用符号
- 初始化
main
方法
初始化主程序
进入
initializeMainExecutable
函数
1
:先遍历初始化动态库2
:调用runInitializers
函数,初始化主程序
进入
runInitializers
函数
- 调用
processInitializers
函数
进入
processInitializers
函数
- 调用
recursiveInitialization
函数
进入
recursiveInitialization
函数
- 调用
notifySingle
函数
函数调用栈顺序,
notifySingle
函数中,应该会调用load_images
函数
但是在
notifySingle
函数中,并没有发现load_images
函数的调用,因为load_images
是objc
中的函数libobjc.A.dylib`load_images + 944
在
dyld
中,是如何调用objc
的函数?进入
notifySingle
函数
- 如果
sNotifyObjCInit
不为空,使用回调指针,执行一个回调函数
搜索
sNotifyObjCInit
的赋值
- 由
registerObjCNotifiers
函数,将init
入参赋值给sNotifyObjCInit
搜索
registerObjCNotifiers
函数的调用
- 在
dyldAPIs.cpp
文件中,被_dyld_objc_notify_register
函数调用,init
为函数的入参
搜索
_dyld_objc_notify_register
函数,找不到任何调用的代码使用另一种方式寻找
在
dyldDemo
项目中,设置_dyld_objc_notify_register
符号断点
- 通过符号断点看出,回调函数是
_objc_init
初始化时赋值的
来到
objc
源码,搜索_objc_init
函数
- 调用
_dyld_objc_notify_register
函数,传入load_images
objc
中,调用dyld
中的_dyld_objc_notify_register
函数,传入load_images
函数
dyld
使用回调指针,调用objc
中的load_images
函数
进入
load_images
函数
- 调用
call_load_methods
函数
进入
call_load_methods
函数
- 调用
call_class_loads
函数,循环调用每个类中的load
方法
回到
dyld
源码回到
recursiveInitialization
函数,调用doInitialization
函数
- 调用
doInitialization
函数
进入
doInitialization
函数
- 调用
doModInitFunctions
函数
doInitialization
函数的作用?在
dyldDemo
项目中,打开main.m
文件,加入C++
构造函数:__attribute__((constructor)) void func1(){ printf("fun1 来了!"); } __attribute__((constructor)) void func2(){ printf("fun2 来了!"); }
编译项目,打开
MachO文件
MachO
中多出_mod_init_func
doInitialization
函数的作用,调用全局C++
对象的构造函数
回到
_main
函数
1
:读取MachO
的LC_MAIN
,找到主程序的main
函数地址2
:经过一系列判断,返回main
函数加载顺序:
load
方法(objc
调用),C++
构造函数(dyld
调用),main
函数(dyld
调用)
总结
dyld
:动态链接器,加载所有的库和可执行文件
start
函数
- 重定位
dyld
- 调用
_main
函数
_main
函数
- 内核检查,然后一系列设置,
HostCPU
、可执行文件的Header
、ASLR
、设置上下文、配置进程是否受限(AFMI
)- 加载共享缓存
- 选择
dyld3
或dyld2
- 实例化主程序
◦ 根据compressed
判断,使用相应的子类实例化主程序,返回实例对象
◦ 拿到实例化后的image
对象,将image
对象添加到image
列表中,返回image
对象
◦ 所以image
列表中,第一个image
一定是主程序- 加载动态库,优先插入的动态库,依次将
image
对象添加到image
列表中,使用环境变量DYLD_INSERT_LIBRARIES
- 链接主程序
◦ 递归加载主程序依赖的库,完成之后发通知
◦ 重定向,修正ASLR
◦ 绑定非懒加载符号
◦ 绑定弱引用符号
◦ 递归应用插入的动态库
◦ 注册- 初始化主程序,
initializeMainExecutable
函数
◦ 调用runInitializers
函数
◦ 调用processInitializers
函数
◦ 调用recursiveInitialization
函数- 返回主程序的入口函数,开始进入主程序的
main
函数
dyld3
闭包模式
- 优先从共享缓存中获取实例对象,不存在创建一个
- 调用
launchWithClosure
函数启动,传入闭包对象- 如果启动成功,拿到主程序
main
的函数,直接返回结果
dyld2
流程
- 把两个回调地址放到
stateToHandlers
数组中- 分配初始化空间,尽量分配足够大的空间,以供后续使用
- 把
dyld
加入到UUID
的列表中
recursiveInitialization
函数
- 调用
notifySingle
函数- 调用
doInitialization
函数
notifySingle
函数
- 如果
sNotifyObjCInit
不为空,使用回调指针,执行一个回调函数- 通过符号断点看出,回调是
_objc_init
函数初始化时赋值的
◦_objc_init
函数在objc
源码中
◦ 调用dyld
中的_dyld_objc_notify_register
函数,传入load_images
函数
◦ 调用call_class_loads
函数,循环调用每个类中的load
方法,动态库优先于主程序的load
方法执行
doInitialization
函数
- 调用
doModInitFunctions
函数,内部调用全局C++
对象的构造函数__attribute__((constructor))
的C函数
加载顺序:
load
方法(objc
调用),C++
构造函数(dyld
调用),main
函数(dyld
调用)