基本概念简介
dyld
dyld
全名The dynamic link editor
。它是苹果的动态链接器,是苹果操作系统一个重要组成部分,在应用被编译打包成可执行文件格式的Mach-O
文件之后 ,交由dyld
负责链接,加载程序。
dyld
是开源的,我们可以通过官网下载它的源码。并通过源码来阅读理解它的运行方式,了解系统加载动态库的细节。
共享缓存
由于iOS
系统中UIKit / Foundation
等系统库每个应用都会通过dyld
加载到内存中,因此,为了节约内存空间,苹果将这些系统库放在了一个地方:动态库共享缓存区 (dyld shared cache)
。同理,在Mac OS
中也一样有一个动态库的共享缓存区。
有了共享缓存区,类似NSLog
的函数实现地址,就不会在我们自己的工程的Mach-O
中,那么问题来了,当我们的工程想要调用NSLog
方法 , 如何能找到其真实的实现地址呢?
在工程编译时,所产生的
Mach-O
可执行文件中会预留出一段空间,这个空间其实就是符号表
,存放在_DATA数据段
中(因为_DATA
段在运行时是可读可写的),在工程运行时,dyld
根据Load Commands
中列出的动态库,去做绑定操作,将方法的真实地址写到_DATA段
的符号表
中。
ASLR
ASLR
的全名:Address Space Layout Randomization
,地址空间配置随机加载;是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术;iOS4.3开始引入了ASLR技术。
ASLR
的作用是地址空间配置随机加载,利用随机方式配置数据地址空间,使某些敏感数据(例如APP登录注册、支付相关代码)配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击。
dyld 源码探索
本节的重点是探索dyld,因此如何从函数调用栈中找到dyld的入口函数这里只简单描述。
【步骤1】在ViewController
类中增加load
方法,并在load
方法中设置断点。
【步骤2】当断点停下时,使用lldb指令bt
查看函数调用栈。
【步骤3】从函数调用栈中可以看到应用程序启动的时候,最先执行的是_dyld_start
,通过lldb指令bt + up/down
可以来到入口函数_dyld_start
处。
【步骤4】上图第 11 行:call
就是调用函数的指令(类似bl),这个函数也就是我们App
开始的地方。
dyldbootstrap :: start
源码如下:
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
rebaseDyld(dyldsMachHeader);
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
源码说明:
1️⃣ 重定向dyld
,在磁盘上,dyld DATA段中的所有指针都被链接在一起。它们需要被修正成真正的指针来运行。这一步必须在使用任何全局变量之前完成。
2️⃣ 对栈溢出
进行保护
3️⃣ 初始化dyld
4️⃣ 计算主程序的ALSR
5️⃣ 初始化完成后调用dyld
的main
函数,即:dyld::_main
注意:
Slide
这个其实就是ALSR
,说白了就是通过一个随机值来实现地址空间配置随机加载
。当进程开始运行时,在存储器中所能够使用与控制的地址空间内,对进程地址进行随机分配,这样可以使某些攻击者无法事先获知地址,攻击者难以通过固定地址获取函数或者内存值进行攻击
镜像的Slide值 = 镜像的mach_header结构体指针 - 镜像文件中第一个__TEXT代码段描述的结构体struct segmeng_command中的vmaddr数据成员的值。
dyld::_main
dyld::_main
源码太长,这里就不完全复制了。这个函数就是加载App的主要函数。
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
//此处省略代码
}
_main函数的流程:
1️⃣ 准备工作
① 设置HostCPU等信息
② 设置上下文信息
setContext
③ 检测进程是否受限,并在上下文中做出对应处理
configureProcessRestrictions
。苹果进程受AFMI
保护(Apple Mobile File Integrity苹果移动文件保护)④ 配置相关环境变量,并根据环境变量的配置对上下文信息进行更新。
2️⃣ 加载共享缓存
① 在
checkSharedRegionDisable
函数中检查共享缓存的禁用状态。注意:iOS中是不允许禁用共享缓存
。-
② 当共享缓存未被禁用时,需要加载共享缓存
mapSharedCache -> loadDyldCache
。这里又分为三种情况:- 仅加载到当前进程
mapCachePrivate
- 共享缓存如果是第一次加载,则进行加载操作
mapCacheSystemWide
- 共享缓存不是第一次被加载,则说明共享缓存已经被加载,那将不做任务处理。
- 仅加载到当前进程
3️⃣ dyld3 加载流程
在iOS 11后,引入了dyld13的闭包模式,以回调的方式加载,该方法加载更快、效率更高。
在iOS 13 后,动态库和第三方库也使用闭包模式加载。
① 判断当前是否是闭包模式
sClosureMode == ClosureMode::Off
:非闭包模式
sClosureMode == ClosureMode::On
:闭包模式-
② 检查共享缓存中是否存在主程序闭包,若存在,则直接接入第⑤步。
-
③ 当共享缓存中没有闭包,或者共享缓存中的闭包无效,则去启动缓存中查找主程序闭包,若存在,则直接进入第⑤步
-
④ 当启动缓存中也不存在主程序闭包时,则构建一个新的主程序启动闭包
-
⑤ 启动主程序闭包
-
⑥ 若主程序闭包启动失败(闭包过期等原因),则又重新构建一个新的主程序启动闭包,并再次启动它
-
⑦ 闭包启动成功,返回
main
函数地址
⑧ 闭包启动失败,则说明dyld3闭包启动不了,则尝试使用dyld2启动程序。
4️⃣ dyld2 加载流程
-
① 将
dyld
的UUID
添加到非共享缓存镜像UUID列表
中。
-
② 实例化主程序
-
进入
instantiateFromLoadedImage
函数,其内部调用instantiateMainExecutable
返回image对象
。
-
继续进入
instantiateMainExecutable
函数,该函数里面两个操作。- 调用
sniffLoadCommands
函数,解析Mach-O
获取一些参数值。
compressed:判断
Mach-O
是Compressed还是Classic类型
segCount:Segment
总数
libCount:需要加载的动态库的数量
codeSigCmd:代码签名信息
encryptCmd:代码的加密信息注意,在函数的结果处有这么一段代码
程序的Segment
总数,不能超过255
程序的依赖库
总数,不能超过4095
- 根据
compressed
结果,执行相应的程序完成主程序的实例化。
- 调用
主程序实例化完成之后,需要将
image
对象添加至image列表
中。从此处可以看出,在image列表
中第一个image
对象就是主程序。
-
-
③ 检测代码,检查设备、系统版本等
-
④ 判断
DYLD_INSERT_LIBRARIES
环境变量是否有设置值。若有,则遍历DYLD_INSERT_LIBRARIES
,依次加载DYLD_INSERT_LIBRARIES
变量中的动态库。加载使用loadInsertedDylib
函数。
-
⑤ 链接主程序
- 记录起始时间,用于记录各步骤的时间间隔
- 递归加载主程序依赖的库.完成之后发通知
- 修正ASLR
- 绑定NoLazy符号
- 绑定弱符号
- 递归应用插入的动态库
- 注册
- 记录结束时间
- 计算时间差,当项目配置环境变量,用于显示各步骤耗时
-
⑥ 链接插入的动态库,这个操作必须在链接主程序之后,被插入的库(例如,libSystem)就不会出现在程序使用的库的前面。
-
⑦ 绑定插入的动态库
-
⑧ 绑定弱符号引用
-
⑨ 运行所有的初始化方法
-
⑩ 通知监控进程即将进入main()函数,返回main()函数地址
至此_main函数
执行完成,并返回main()函数
地址。
总结
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函数
-
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函数-
dyld加载顺序:
- load方法
- C++构造函数
- main()函数