iOS应用加载流程
在iOS领域我们谈应用加载流程,就不得不谈一下dyld。
概述:DYLD(the dynamic link editor)苹果的动态连接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责其余的工作。而且它是开源的,任何人都可以从苹果官网下载来理解它的运作方式。dyld下载
DYLD的演化历程
dyld1(1996年~2004年)
早期的dyld,动态链接技术有个特性是预绑定技术,是为系统中所有dylib和应用程序找到固定的地址,dyld加载这些地址的所有内容,如果成功将会编辑所有这些二进制数据,以获得所有预计算的地址,然后下一次当它将所有数据放入相同的地址时不比进行任何额外的操作,这能提高速度,但是这也意味着每次启动时会编辑你的二进制数据,时间会变慢,而且每次放入相同的地址,从安全性来说就很不友好。
优点:1. 运行时快
缺点:
- 启动时间长
- 安全性差
dyld2(2004年~2007年)
dyld2是OS Tiger组成部分,完全重写了dyld,支持C++初始化语义,扩展了MachO格式,并且更新DYLD,具备完整的本机dlopen和dlsym实现,很好的支持C++,C库。与dyld不同,dyld2是编辑系统库,可仅在软件更新时做这些事没有时候在安装时,你会看到“优化系统库”之类的词,这其实是在更新预绑定。dyld2更多的是优化。
dyld2.x(2007年~2017年)
dyld2.x衍生了更多的架构和平台,增加代码签名和ASLR(地址空间配置随机加载)这意味着每次加载,它可能位于不同的地址,取消了预绑定,使用共享缓存替代。
image.png
dyld3(2017年~)
全新的动态链接器,全面取代dyld2,但是完全兼容dyld2.x,为了性能,安全性,可测试性和可靠性。
- 将大多数dyld移出进程,这时它只是普通的后台程序,可使用标准测试工具进行测试
- 允许部分dyld留住在进程中,但留驻的不多,减小了程序可攻击面积
dyld3包含三部分:
- 进程外MachO分析器和编译器
- 进程内引擎执行launch closure(启动收尾)
- 启动收尾缓存服务(launch closure cacheing service)
大多程序启动始终不需要调用进程外MachO分析器和编译器,启动收尾比MachO分析简单,他们是内存映射文件,无需复杂的方法进行分析。
dyld3
第一部分:
解析所有的搜索路径,@rpath,环境变量,然后分析二进制MachO,执行符号查找,利用这些结果创建启动closure;
第二部分:
读取并检查启动closure处理是否正确,映射所有库,applies fixups, 运行初始化器,跳转main()函数(dyld3不需要分析MachO头文件或执行符号查找,正因为它省去了这些耗时的操作,所以更快捷,启动速度更快)
第三部分:
dyld3是启动closure缓存服务。意思是系统程序将启动closure缓存到共享缓存。我们已经使用这个工具在系统中运行和分析每个MachO文件,直接将他们放入共享缓存,使他映射到缓存中,所有的dylib都使用它来启动,甚至不需要打开其他文件。对于第三方App,其在安装的时候,建立启动closure。
共享缓存机制
在iOS系统里面,每一个程序所有依赖的动态库都是通过dyld来一个一个加载到内存,那如果每一个程序所需要的动态库是相同的,比如系统库,几乎每一个APP都会用到,如果每个程序运行时都要重复去加载一次,势必造成手机运行缓慢,为了优化启动速度提高性能,共享缓存机制就应运而生。所有默认的动态链接库都被合成一个大的缓存文件,放在/System/Library/Caches/com.apple.dyld/目录下,按照不同的架构保存。
dyld加载流程
1、配置环境变量,获取当前运行架构
2、设置上下文信息,配置进程受限与否
3、加载可执行文件,生成一个ImageLoader实例对象
4、检查共享缓存是否映射到了共享区域
5、加载所有插入库
6、链接主程序
7、链接所有插入库,执行符号替换
8、执行初始化方法initializeMainExceutable
9、寻找主程序入口
结合最新的DYLD开源库dyld-832.7.3。
-
1、配置环境变量,获取当前运行架构
- 调用
checkEnvironmentVariables,根据环境变量设置相应的值 - 调用
getHostInfo获取运行架构信息- 打印参数配置
DYLD_PRINT_OPTS - 打印环境变量配置
DYLD_PRINT_ENV
image.png
- 打印参数配置
- 调用
-
2、设置上下文信息,配置进程受限与否
- 调用
setContext,设置上下文信息(包括调用的函数和参数) - 然后调用
configureProcssRestrictions,设置进程是否受限
- 调用
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
// Pickup the pointer to the exec path.
// 获取可执行文件的路径
sExecPath = _simple_getenv(apple, "executable_path");
// <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
if (!sExecPath) sExecPath = apple[0];
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
// <rdar://54095622> kernel is not passing a real path for main executable
if ( strncmp(sExecPath, "/var/containers/Bundle/Application/", 35) == 0 ) {
if ( char* newPath = (char*)malloc(strlen(sExecPath)+10) ) {
strcpy(newPath, "/private");
strcat(newPath, sExecPath);
sExecPath = newPath;
}
}
#endif
if ( sExecPath[0] != '/' ) {
// have relative path, use cwd to make absolute
// 将相对转换成绝对路径
char cwdbuff[MAXPATHLEN];
if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
// maybe use static buffer to avoid calling malloc so early...
char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
strcpy(s, cwdbuff);
strcat(s, "/");
strcat(s, sExecPath);
sExecPath = s;
}
}
// Remember short name of process for later logging
// 获取文件名字
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;
#if TARGET_OS_OSX && __has_feature(ptrauth_calls)
// on Apple Silicon macOS, only Apple signed ("platform binary") arm64e can be loaded
sOnlyPlatformArm64e = true;
// internal builds, or if boot-arg is set, then non-platform-binary arm64e slices can be run
if ( const char* abiMode = _simple_getenv(apple, "arm64e_abi") ) {
if ( strcmp(abiMode, "all") == 0 )
sOnlyPlatformArm64e = false;
}
#endif
// 配置进程是否受限
configureProcessRestrictions(mainExecutableMH, envp);
// Check if we should force dyld3. Note we have to do this outside of the regular env parsing due to AMFI
if ( dyld3::internalInstall() ) {
if (const char* useClosures = _simple_getenv(envp, "DYLD_USE_CLOSURES")) {
if ( strcmp(useClosures, "0") == 0 ) {
sClosureMode = ClosureMode::Off;
} else if ( strcmp(useClosures, "1") == 0 ) {
#if !__i386__ // don't support dyld3 for 32-bit macOS
sClosureMode = ClosureMode::On;
sClosureKind = ClosureKind::full;
#endif
} else if ( strcmp(useClosures, "2") == 0 ) {
sClosureMode = ClosureMode::On;
sClosureKind = ClosureKind::minimal;
} else {
dyld::warn("unknown option to DYLD_USE_CLOSURES. Valid options are: 0 and 1\n");
}
}
}
-
3、加载可执行文件,生成一个
ImageLoader实例对象- 调用
instatiateFromLoadedImage函数实例化ImageLoader对象。调用isCompatibleMachO判断文件架构是否和当前架构兼容,再调用ImageLoaderMachO::instantiateMainExcutable来加载文件生成实例,并将Image添加到全局sAllImages中。可以通过添加配置DYLD_PRINT_LIBRARIES查看当前加载的Image - 调用
sniffLoadConmands获取load command的相关信息,根据不同的文件类型调用不同的ImageLoaderMachO进行初始化. - 可以通过添加配置
DYLD_PRINT_SEGMENTS打印segment加载信息
- 调用
-
4、检查共享缓存是否映射到了共享区域
+调用mapShareCache()函数将共享缓存映射到共享区域,还做一些缓存文件库是否升级的判断和升级处理 -
5、加载所有插入库
- 遍历环境变量
DYLD_PRINT_LIBRARIES然后调用loadInsertedDylib加载,这里就可以添加自己的动态库,完成代码注入
- 遍历环境变量
-
6、链接主程序
- 内核调用
ImageLoader::link链接主程序
里面进行递归加载动态库,依赖库,对Image排序,递归rebase,递归符号绑定(一般只是非懒加载符号,当然如果满足一些条件,懒加载符号也会立即绑定),注册DOF节区等操作。
- 内核调用
- 7、链接所有插入库,执行符号替换
-
8、执行初始化方法
initializeMainExceutable-
cNotifyObjCinit调用objc中的load_images,load_images调用+load和constructor方法就在这里执行,可以通过+load断点,通过LLDB打印堆栈。_mod_init_funcssection的函数,就是constructor方法
-
-
9、寻找主程序入口
- 调用
getThreadPC,从load_Command读取LC_MAIN入口
- 调用
dyld
dyld(the dynamic link editor),Apple 的动态链接器,系统 kernel 做好启动程序的初始准备后,交给 dyld 负责,援引并翻译《 Mike Ash 这篇 blog 》对 dyld 作用顺序的概括:
- 从 kernel 留下的原始调用栈引导和启动自己
- 将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
- non-lazy 符号立即 link 到可执行文件,lazy 的存表里
- Runs static initializers for the executable
- 找到可执行文件的 main 函数,准备参数并调用
- 程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口
- 程序main函数 return 后执行 static terminator
- 某些场景下 main 函数结束后调 libSystem 的 _exit 函数
得益于 dyld 是开源的,github 地址,我们可以从源码一探究竟。
一切源于dyldStartup.s这个文件,其中用汇编实现了名为__dyld_start的方法,汇编太生涩,它主要干了两件事:
- 调用
dyldbootstrap::start()方法(省去参数) - 上个方法返回了 main 函数地址,填入参数并调用 main 函数
这个步骤随手就能验证出来,设置一个符号断点断在_objc_init:
image
这个函数是runtime的初始化函数,后面会提到。程序运行在很早的时候断住,这时候看调用栈:
image
看到了栈底的dyldbootstrap::start()方法,继而调用了dyld::_main()方法,其中完成了刚才说的递归加载动态库过程,由于libSystem默认引入,栈中出现了libSystem_initializer的初始化方法。
ImageLoader
当然这个 image 不是图片的意思,它大概表示一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,所以 ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。
两步走:
- 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻)
- 再从可执行文件 image 递归加载所有符号
当然所有这些都发生在我们真正的main函数执行前。
runtime 与 +load
刚才讲到 libSystem 是若干个系统 lib 的集合,所以它只是一个容器 lib 而已,而且它也是开源的,里面实质上就一个文件,init.c,由 libSystem_initializer 逐步调用到了 _objc_init,这里就是 objc 和 runtime 的初始化入口。
除了 runtime 环境的初始化外,_objc_init中绑定了新 image 被加载后的 callback:
可见 dyld 担当了 runtime 和 ImageLoader 中间的协调者,当新 image 加载进来后交由 runtime 大厨去解析这个二进制文件的符号表和代码。继续上面的断点法,断住神秘的 +load 函数:
image
清楚的看到整个调用栈和顺序:
- dyld 开始将程序二进制文件初始化
- 交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
- 由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
- runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法
至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)
简单总结
整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,
动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。
值得说明的是,这个过程远比写出来的要复杂,这里只提到了 runtime 这个分支,还有像 GCD、XPC等重头的系统库初始化分支没有提及(当然,有缓存机制在,它们也不会玩命初始化),总结起来就是 main 函数执行之前,系统做了茫茫多的加载和初始化工作,但都被很好的隐藏了,我们无需关心。
孤独的 main 函数
当这一切都结束时,dyld 会清理现场,将调用栈回归,只剩下:
image
孤独的 main 函数,看上去是程序的开始,确是一段精彩的终结
Reference
http://blog.sunnyxx.com/2014/08/30/objc-pre-main/
https://www.dllhook.com/post/238.html