iOS-应用程序的加载
资料准备:
1、dyld源码下载opensource.apple.com/
2、libdispatch源码下载opensource.apple.com/
3、libSystem源码下载opensource.apple.com/
前情提要:
在探索分析app启动之前,我们需要先了解iOS中App代码的编译过程以及动态库和静态库。
编译过程
1、预编译:处理代码中的#开头的预编译指令,比如删除#define
并展开宏定义,将#include
包含的文件插入到该指令位置等(即替换宏,删除注释,展开头文件,产生.i
文件)
2、编译:对预编译处理过的文件进行词法分析、语法分析和语义分析,并进行源代码优化,然后生成汇编代码(即将.i
文件转换为汇编语言,产生.s
文件)
3、汇编:通过汇编器将汇编代码转换为机器可以执行的指令,并生成目标文件.o
文件
4、链接:将目标文件链接成可执行文件.这一过程中,链接器将不同的目标文件链接起来,因为不同的目标文件之间可能有相互引用的变量或调用的函数,如我们经常调用Foundation
框架和UIKit
框架中的方法和变量,但是这些框架跟我们的代码并不在一个目标文件中,这就需要链接器将它们与我们自己的代码链接起来
流程如下
Foundation和UIKit这种可以共享代码、实现代码的复用统称为库——它是可执行代码的二进制文件,可以被操作系统写入内存,它又分为静态库和动态库
静态库
链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
如.a
、.lib
、非系统framework
都是静态库
动态库
链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。如.dylib
和.framework
都是动态库
dyld:
简介
dyld(The dynamic link editor)
是苹果的动态链接器,负责程序的链接及加载工作,是苹果操作系统的重要组成部分,存在于MacOS系统的(/usr/lib/dyld
)目录下.在应用被编译打包成可执行文件格式的Mach-O
文件之后 ,交由dyld
负责链接,加载程序。
整体流程如下
dyld_shared_cache
由于不止一个程序需要使用UIKit系统动态库,所以不可能在每个程序加载时都去加载所有的系统动态库.为了优化程序启动速度和利用动态库缓存,苹果从iOS3.1之后,将所有系统库(私有与公有)编译成一个大的缓存文件,这就是dyld_shared_cache
,该缓存文件存在iOS系统下的/System/Library/Caches/com.apple.dyld/
目录下
dyld加载流程:
在load
方法处加一个断点,点击函数调用栈/使用LLDB——bt
指令打印,都能看到最初的起点_dyld_start
_dyld_start
可以看到_dyld_start
是汇编写的,从注释中可以看出dyldbootstrap::start
方法就是最开始的start
方法。
dyldbootstrap::start
dyldbootstrap::start
其实C++的语法,其中dyldbootstrap
代表命名空间,start
则是这个命名空间中的方法。
可以看到start
这个方法的核心是dyld::main
dyld::main
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue) {
代码省略......
/// 环境变量的配置
// Grab the cdHash of the main executable from the environment
/// 从环境变量中获取主要可执行文件的cdHash
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
/// 根据Mach-O头部获取当前运行的架构信息
getHostInfo(mainExecutableMH, mainExecutableSlide);
代码省略......
/// 检查共享缓存是否开启,在iOS中必须开启
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
/// 检查共享缓存是否映射到了共享区域
mapSharedCache(mainExecutableSlide);
#endif
代码省略......
/// 加载可执行文件,并生成一个ImageLoder实例对象
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
代码省略......
/// 加载所有DYLD_INSERT_LIBRARIES指定的库
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
代码省略......
/// link主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
代码省略......
/// link动态库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing(gLinkContext);
}
}
}
代码省略......
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);
// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
}
}
代码省略......
/// 弱符号绑定
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
代码省略......
/// 执行初始化方法
// run all initializers
initializeMainExecutable();
代码省略......
/// 寻找m目标可执行文件ru入口并执行
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
}#### 1 环境变量配置
通过以上代码分析大体流程如下
1 环境变量配置
- 平台,版本,路径,主机信息的确定
- 从环境变量中获取主要可执行文件的
cdHash
checkEnvironmentVariables(envp)
检查设置环境变量defaultUninitializedFallbackPaths(envp)
在DYLD_FALLBACK
为空时设置默认值getHostInfo(mainExecutableMH, mainExecutableSlide)
获取程序架构
Xcode设置了这两个环境变量参数,在App启动时就会打印相关参数、环境变量信息
如下
2 共享缓存
checkSharedRegionDisable
检查是否开启共享缓存(在iOS中必须开启)mapSharedCache
加载共享缓存库,其中调用loadDyldCache
函数有这么几种情况:- 仅加载到当前进程
mapCachePrivate
(模拟器仅支持加载到当前进程) - 共享缓存是第一次被加载,就去做加载操作
mapCacheSystemWide
- 共享缓存不是第一次被加载,那么就不做任何处理
3 主程序的初始化
调用instantiateFromLoadedImage
函数实例化了一个ImageLoader
对象
通过instantiateMainExecutable
方法创建ImageLoader
实例对象
这里主要是为主可执行文件创建映像,返回一个ImageLoader
类型的image
对象,即主程序
.其中sniffLoadCommands
函数会获取Mach-O
类型文件的Load Command
的相关信息,并对其进行各种校验
4 插入动态库
遍历DYLD_INSERT_LIBRARIES
环境变量,调用loadInsertedDylib
加载,通过该环境变量我们可以注入自定义的一些动态库代码从而完成安全攻防,loadInsertedDylib
内部会从DYLD_ROOT_PATH
、LD_LIBRARY_PATH
、DYLD_FRAMEWORK_PATH
等路径查找dylib
并且检查代码签名,无效则直接抛出异常
5 link主程序
5 link动态库
6 弱符号绑定
7 执行初始化方法
initializeMainExecutable
源码主要是循环遍历,都会执行runInitializers
方法runInitializers(cons
其核心代码是processInitializers
函数的调用processInitializers
函数对镜像列表调用recursiveInitialization
函数进行递归实例化recursiveInitialization
函数其作用获取到镜像的初始化,核心方法有两个notifySingle
和doInitialization
notifySingle
函数,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
这句sNotifyObjCInit
只有赋值操作registerObjCNotifiers
发现在_dyld_objc_notify_register
进行了调用,这个函数只在运行时提供给objc
使用
在objc4
源码中查找_dyld_objc_notify_register
,发现在_objc_init
源码中调用了该方法,并传入了参数,所以sNotifyObjCInit
的赋值的就是objc
中的load_images
所以综上所述,notifySingle是一个回调函数
从load_images
中可以看到call_load_methods
方法调用call_load_methods
方法其核心是通过do-while
循环调用call_class_loads
方法call_class_loads
这里调用的load方法
就是类的load方法
至此也证实了load_images
调用了所有的load
函数。
doInitialization
中调用了两个核心方法doImageInit
和doModInitFunctions
doImageInit
其核心主要是for循环
加载方法的调用,这里需要注意的一点是libSystem
的初始化必须先运行doModInitFunctions
中加载了所有Cxx
文件,这里需要注意的一点是libSystem
的初始化必须先运行
通过doImageInit
和doModInitFunctions
方法知道libSystem
初始化必须先运行,这里也和堆栈信息相互验证一致性libSystem
库中的初始化函数libSystem_initializer
中调用了libdispatch_init
函数libdispatch_init
方法中调用了_os_object_init
函数_os_object_init
方法中调用了_objc_init
函数
结合上面的分析,从初始化_objc_init
注册的_dyld_objc_notify_register
的参数2,即load_images
,到sNotifySingle --> sNotifyObjCInie=参数2
到sNotifyObjcInit()
调用,形成了一个闭环
8 寻找主程序入口dyld
汇编源码实现
dyld
加载流程
链接:https://juejin.cn/post/7030253046210773029