引言
本文主要探索dyld
的加载流程,了解应用程序在main
函数之前都做了什么准备工作,了解dyld
是什么,我们所编写的代码、framework
等是如何加载到内存里变活起来的。
dyld
dyld
(The dynamic link editor )是苹果的动态链接器
,是苹果操作系统的重要组成部分,在我们的代码被编译打包成可执行文件
的 Mach-O
文件之后 ,交由 dyld
负责链接 , 加载程序 。
dyld源码
本文用的是dyld-852
版本的源码。
探索1
main -> start符号断点,调用栈
我们新建一个iOS工程
,在main.m
中打个断点,运行项目,查看调用堆栈,如图所示:
查看左边,发现在
main函数
调用前,系统已经执行了start
函数。根据以往经验,我们可以选择的操作方式是查看汇编
和添加符号断点
。
图中我们看到了,
start
是在libdyld.dylib
这个库中调用的,dyld
是开源库,我们可以下载下来后,阅读源码。本文用的是dyld-852
版本的源码。
在添加
start
符号断点,运行后,发现符号断点并未停住,而是直接来到了main.m
的断点上。因此,start
并不是我们所需要的符号断点。仍需努力。
load方法
和__attribute__
在ViewController.m
中添加load方法
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
+(void)load {
NSLog(@"---%s---",__func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
在main.m
中添加__attribute__
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
NSLog(@"main 函数");
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
//确保此函数在 在main函数被调用之前调用
__attribute__ ((constructor))void before_main(){
printf("main 前 :%s\n",__func__);
}
这三者打印顺序如何呢?如图所示:
由图可知,三者执行的先后顺序为
load方法
-- __attribute__
--main函数
。因此在main
函数执行前的操作,可以从load
方法中添加断点开始。
补充:
__attribute__ ((constructor))void before_main(){}
是确保此函数在 在main
函数被调用之前调用。具体请查看这篇文章OC中的 _ attribute _。
load方法断点
在ViewController.m
中的+load
方法添加断点,运行后如图所示:
由图中我们可以看到左边的调用栈中,在
load
之前调用了load_images
和_dyld_start
。我们在控制台中输入bt
打印详细调用栈,如图所示:
由此可见,我们的app最开始,是由
_dyld_start
开始的。
dyld源码
下载好dyld-852
后,打开dyld
源码工程,由于工程底部所依赖的系统库太多(libdispatch,libsystem
),运行不起来无法调试。
dyldbootstrap::start
全局搜索_dyld_start
在
dyldStartup.s
中(.s是汇编文件后缀),可以看到有多个重复的.global _dyld_start
,分别点击后,可以看到是右边是由于架构判断,导致的重复,因此,我们挑arm64
架构的源码来阅读。
阅读汇编代码,阅读注释很重要,我们在
第240行
看到了call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
,call的意思是呼叫,调用。这里我们全局搜索dyldbootstrap::start
,发现搜不到我们想要的。我们全局搜索dyldbootstrap
,发现在dyldInitialization.cpp
的dyldbootstrap
是一个命名空间
。C++
语法中,有一个词叫namespace命名空间
,可当做类来阅读。namespace
内部成员和函数,相当于类的成员和方法。我们接着搜索start
,如图所示:
rebaseDyld()
:dyld重定位。这是苹果用来保证应用安全的技术,其中包括:ASLR
和 Code Sign
。
ASLR
: Address Space Layout Randomization(地址空间布局随机化)的简称。App在被启动的时候,程序会被映射到逻辑地址空间,这个逻辑地址空间有一个起始地址
,ASLR
技术让这个起始地址是随机
的。这个地址如果是固定的,攻击者很容易就用起始地址+函数偏移地址
找到对应的函数地址。
Code Sign
:代码加密签名机制,但是在 Code Sign操
作的时候,加密的哈希不是针对整个文件,而是针对每一个 Page
的。这个就保证了 dyld
在加载的时候,可以对每个 page
进行独立的验证。
正是因为 ASLR使得地址随机化,导致起始地址不固定,以及 Code Sign,导致不能直接修改 Images。所以需要 rebase来处理符号引用问题,Rebase的时候只需要通过增加对应偏移量就行了。Rebase主要的作用就是修正内部(指向当前 Mach-O文件)的指针指向,也就是基地址复位功能。
dyld::_main
dyld::_main
函数是我们此次研究的重头戏,点进去之后,发现它是一个800多行
代码的一个函数。
查看大量源码,分析流程的思路,需要整体把握,不需要一头扎进细节里,否则,将切身体会到“入门到放弃”的完整过程,迷失在未知的世界里。对于此类源码,我们可以根据
返回值
来倒推逻辑
。接下来,我们将逐步解析其内容。
1、通过
return
返回的是一个result
;
2、通过
result
的赋值,找到result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
以及其他有关sMainExecutable
的代码位置,我们由此可知,result
与sMainExecutable
有很大的关联和关系;
3、通过
sMainExecutable =
,找到sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
,官方注释为// instantiate ImageLoader for main executable
4、点击进入
instantiateFromLoadedImage
,发现内部添加了image镜像
,并返回此image镜像
。
5、进入
instantiateMainExecutable
查看如何获得image镜像
的,内部的segCount
,libCount
,data_command
,info_command
等等内容,可进入sniffLoadCommands
配合烂苹果MachOView
进行对照查看,可以事半功倍。
dyld::_main内部总结(借助注释来查看)
1.【第一步:条件准备】:环境配置、平台、版本、路径、主机信息等
2.【第二步:共享缓存配置】:
checkSharedRegionDisable
检查是否可用,通过mapSharedCache
映射共享缓存到贡献缓存区域
3.【第三步:实例化主程序】:通过
instantiateFromLoadedImage
实例化主程序,得到一个sMainExecutable
。将images
加入到dyld_all_image_info
表中。
4.【第四步:插入动态库】:
for循环 DYLD_INSERT_LIBRARIES
,调用loadInsertedDylib(*lib);
插入动态库。
5.【第五步:link主程序和动态库】:直接调用
link
方法,将sMainExecutable
链接,再for
循环将第四步
插入的动态库链接。
6.【第六步:weakBind弱引用绑定主程序】:
for循环sImageRoots.size()
,插入镜像符号;sMainExecutable->recursiveBindWithAccounting
绑定主程序;image->recursiveBind
绑定插入的动态库,并通知已经注册完毕。sMainExecutable->weakBind
弱引用绑定主程序。
7.【第七步:初始化主程序】:调用
initializeMainExecutable
初始化主程序。
8.【第八步:寻找main函数入口】:寻找
LC_MAIN
入口地址并执行,如果找不到,则寻找LC_UNIXTHREAD
,dyld
将start
设置成main()
。
【第七步:初始化主程序】initializeMainExecutable的分析
1.进入initializeMainExecutable
后,for循环
将插入的动态库初始化。然后执行runInitializers
初始化主程序。
2.进入
sMainExecutable->runInitializers
,找到processInitializers
函数,进入下一步。
3.进入
processInitializers
,主线为递归初始化images list
镜像列表里的所有镜像,并创建一个list列表
,用于存储为初始化的向上的依赖关系。ups > 0
的判断,是如果仍有依赖关系,将他们初始化。
4.全局搜索
recursiveInitialization
,找到ImageLoader里的recursiveInitialization 函数
。查看try{}
内部的实现:
1)
for循环
初始化更底层的库
2)
terminationRecorder
记录终止命令
3)
notifySingle
通知objc
记得要初始化这个image镜像。
4)
doInitialization
初始化这个image镜像
doInitialization
进入到doInitialization
函数,有两个函数调用
1.进入
doImageInit
,发现是 获取mach-o的init方法的地址并调用:
2.进入
doModInitFunctions
,解析并执行DATA,mod_init_func这个section中保存的函数(这里保存的是全局C++对象的构造函数以及所有带__attribute((constructor)的C函数),也就是我们之前在main.m
中写的before_main()
函数。并将libSystem库
中的libSystem_initializer
执行。
**陷入迷茫,无法继续下一步,返回到ImageLoader::recursiveInitialization
函数,探索notifySingle
notifySingle
1.点击进入notifySingle
,发现来到的是函数声明的地方,void (*notifySingle)(dyld_image_states, const ImageLoader* image, InitializerTimingList*);
2.全局搜索notifySingle(
,找到它的实现:
3.全局搜索
sNotifyObjCInit
,无实现函数,在registerObjCNotifiers函数
中找到 = init
赋值。
4.全局搜索
registerObjCNotifiers
,在dyldAPIs.cpp
中的_dyld_objc_notify_register
函数中,找到调用位置。
5.全局搜索
_dyld_objc_notify_register
,发现没有调用该函数的代码。细看类名dyldAPIs.cpp
,该类为dyld对外的接口类
。由于上面提到的时候,对通知objc
记得初始化该镜像,因此,我们将打开libobjc
源码。
6.打开objc4_818_2
工程,全局搜索_dyld_objc_notify_register
。在_objc_init
函数中,找到其调用位置,其中load_images
为dyld
中registerObjCNotifiers
为sNotifyObjCInit
赋值的参数。而load_images
,是一个函数,由此可知,dyld
通知objc
的方式,是通过load_images
函数进行回调的。也就是说,dyld
中的notifySingle
是一个回调函数。
load_images
进入load_images
,首先查找发现所有+load方法
,然后执行所有+load方法
进入
call_load_methods
中,内部为创建一个autoreleasepool
,并在autoreleasepool
中do-while
循环执行所有的+load
方法。
call_class_loads()
和call_category_loads()
都是最终调用(*load_method)(cls, @selector(load));
函数。
到此为止,
+load
的加载流程已经梳理通顺。对应上了我们前面的ViewController.m
中+load
方法。
+load流程
调用流程如图所示:
→
_dyld_start
(dyld
)
→
dyldbootstrap::start
(dyld
)
→
dyld::_main
(dyld
)
→
initializeMainExecutable
(dyld
)
→
ImageLoader::runInitializers
(dyld
)
→
ImageLoader::processInitializers
(dyld
)
→
ImageLoader::recursiveInitialization
(dyld
)
→
dyld::notifySingle
(dyld
,回调函数,sNotifyObjCInit = load_images函数
)
→
libobjc.A.dylib load_images
(objc
,接收回调,调所有+load
方法)
→
VC +load
。(开发者代码)
doInitialization未完成的流程
前面进入doInitialization
内部后,发现无法继续深入,此时,我们回到objc调试工程
,添加符号断点_objc_init
。如图所示
从调用栈中可以看到,
doModInitFunctions
到_objc_init
中缺失了两个流程。
_os_object_init
_os_object_init
位于libdispatch.dylib
库中,我们将下载libdispatch-1271.40.12源码
,解压后打开工程,全局搜索_os_object_init
发现
_os_object_init
是在libdispatch_init
调起的。
在之前的调用栈中,libdispatch_init
是由libSystem_initializer
调起的,此函数位于libSystem.B.dylib
库中。我们将下载Libsystem-1292.60.1源码解压后打开工程,全局搜索libdispatch_init
,确实位于libSystem_initializer
中调用了。
_objc_init流程
→_dyld_start
(dyld
)
→dyldbootstrap::start
(dyld
)
→dyld::_main
(dyld
)
→dyld::initializeMainExecutable
(dyld
)
→ImageLoader::runInitializers
(dyld
)
→ImageLoader::processInitializers
(dyld
)
→ImageLoader::recursiveInitialization
(dyld
)
→ImageLoaderMachO::doInitialization
(dyld
)
→ImageLoaderMachO::doModInitFunctions
(dyld
)
→libSystem_initializer
(libSystem
)
→libdispatch_init
(libdispatch
)
→_os_object_init
(libdispatch
)
→_objc_init
(libobjc
)
总结
本文Demo
dyld
主线加载流程流程图如下: