注册
iOS

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负责链接,加载程序。

整体流程如下


image.png


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方法。


image.png


dyldbootstrap::start


dyldbootstrap::start其实C++的语法,其中dyldbootstrap代表命名空间,start则是这个命名空间中的方法。


image.png


image.png
可以看到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)获取程序架构

image.png
image.png
image.png


Xcode设置了这两个环境变量参数,在App启动时就会打印相关参数、环境变量信息
如下


image.png


image.png


2 共享缓存



  • checkSharedRegionDisable检查是否开启共享缓存(在iOS中必须开启)
  • mapSharedCache加载共享缓存库,其中调用loadDyldCache函数有这么几种情况:

    • 仅加载到当前进程mapCachePrivate(模拟器仅支持加载到当前进程)
    • 共享缓存是第一次被加载,就去做加载操作mapCacheSystemWide
    • 共享缓存不是第一次被加载,那么就不做任何处理



image.png


image.png


3 主程序的初始化


调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象
image.png
通过instantiateMainExecutable方法创建ImageLoader实例对象
image.png
这里主要是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序.其中sniffLoadCommands函数会获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验
image.png


4 插入动态库


遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载,通过该环境变量我们可以注入自定义的一些动态库代码从而完成安全攻防,loadInsertedDylib内部会从DYLD_ROOT_PATHLD_LIBRARY_PATHDYLD_FRAMEWORK_PATH等路径查找dylib并且检查代码签名,无效则直接抛出异常
image.png
image.png


5 link主程序
image.png


5 link动态库
image.png


6 弱符号绑定
image.png


7 执行初始化方法


image.png
initializeMainExecutable源码主要是循环遍历,都会执行runInitializers方法
image.png
runInitializers(cons其核心代码是processInitializers函数的调用
image.png
processInitializers函数对镜像列表调用recursiveInitialization函数进行递归实例化
image.png
recursiveInitialization函数其作用获取到镜像的初始化,核心方法有两个notifySingledoInitialization
image.png
notifySingle函数,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());这句
image.png
sNotifyObjCInit只有赋值操作
image.png
registerObjCNotifiers发现在_dyld_objc_notify_register进行了调用,这个函数只在运行时提供给objc使用
image.png
objc4源码中查找_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images所以综上所述,notifySingle是一个回调函数
image.png
load_images中可以看到call_load_methods方法调用
image.png
call_load_methods方法其核心是通过do-while循环调用call_class_loads方法
image.png
call_class_loads这里调用的load方法就是类的load方法
image.png
至此也证实了load_images调用了所有的load函数。


doInitialization中调用了两个核心方法doImageInitdoModInitFunctions
image.png
doImageInit其核心主要是for循环加载方法的调用,这里需要注意的一点是libSystem的初始化必须先运行
image.png
doModInitFunctions中加载了所有Cxx文件,这里需要注意的一点是libSystem的初始化必须先运行
image.png
通过doImageInitdoModInitFunctions方法知道libSystem初始化必须先运行,这里也和堆栈信息相互验证一致性
image.png
libSystem库中的初始化函数libSystem_initializer中调用了libdispatch_init函数
image.png
libdispatch_init方法中调用了_os_object_init函数
image.png
_os_object_init方法中调用了_objc_init函数
image.png
结合上面的分析,从初始化_objc_init注册的_dyld_objc_notify_register的参数2,即load_images,到sNotifySingle --> sNotifyObjCInie=参数2sNotifyObjcInit()调用,形成了一个闭环


8 寻找主程序入口
image.png
dyld汇编源码实现
image.png


dyld加载流程


image.png




链接:https://juejin.cn/post/7030253046210773029

0 个评论

要回复文章请先登录注册