iOS-应用程序的加载

1,037 阅读7分钟

资料准备:

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.pngobjc4源码中查找_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images所以综上所述,notifySingle是一个回调函数 image.pngload_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