iOS进阶之路 (八)dyld加载流程

1,784 阅读9分钟

一.动态库和静态库

1. 编译过程

从一个C程序源文件到可执行程序到底经历了哪几步,每一步都做了什么?

  1. 预编译(Prepressing):处理代码中的# 开头的预编译指令,比如删除#define并展开宏定义,将#include包含的文件插入到该指令位置等
  2. 编译(Compilation):对预编译处理过的文件进行词法分析、语法分析和语义分析,并进行源代码优化,然后生成汇编代码;
  3. 汇编(Assembly):通过汇编器将汇编代码转换为机器码,并生成目标文件.o文件
  4. 链接(Linking):将源文件中用到的库函数与汇编生成的目标文件.o合并生成可执行文件。链接器将不同的目标文件链接起来,因为不同的目标文件之间可能有相互引用的变量或调用的函数,如我们经常调用Foundation框架和UIKit 框架中的方法和变量,但是这些框架跟我们的代码并不在一个目标文件中,这就需要链接器将它们与我们自己的代码链接起来。
  • 静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。

  • 动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。

Foundation和UIKit这种可以共享代码、实现代码的复用统称为库。其实库就是可执行的代码的二进制,可以被操作系统写入内存中的,库可以分为静态库和动态库

2. 静态库

静态库链接阶段会将汇编生产的目标与引用的库一起链接打包到可执行文件当中。多次使用多次拷贝,造成冗余,使包变的更大。

  • 类型: .a .lib
  • 优势:开发者可以自己开发定义

3. 动态库

动态库是指链接时不复制,程序运行时由系统加在到内存中,供系统调用,系统只需加载一次,多次使用,共用节省内存。

  • 类型:framework .so .dll
  • 优势:共享内容,节省资源;可以通过更新动态库,起到更新程序的目的;减少ipa包体积。

二. dyld

1. dyld简介

dyld(The dynamic link editor)是苹果的动态链接器,负责程序的链接及加载工作,是苹果操作系统的重要组成部分,存在于MacOS系统的(/usr/lib/dyld)目录下。在应用被编译打包成可执行文件格式的Mach-O文件之后 ,交由dyld负责链接,加载程序。

2. dyld_shared_cache

由于不止一个程序需要使用UIKit系统动态库,所以不可能在每个程序加载时都去加载所有的系统动态库。为了优化程序启动速度和利用动态库缓存,苹果从iOS3.1之后,将所有系统库(私有与公有)编译成一个大的缓存文件,这就是dyld_shared_cache,该缓存文件存在iOS系统下的/System/Library/Caches/com.apple.dyld/目录下。

三. dyld加载流程

我们重写一个load方法(目的就是想找到app启动时,我们能跟踪到的最早的方法)并打断点,查看堆栈信息,发现入口处就是libdyld的start函数。

进入_dyld_start的汇编,我们找到一个dyldbootstrap::start函数。切入点已经找到,接下来进入dyld的源码

1. _dyld_start

源码中全局搜索__dyld_start。在arm64中,找到__dyld_start的第一个bl方法(bl在汇编中是跳转):dyldbootstrap::start。其实在load的堆栈调用里有这个方法的影子。

2. dyldbootstrap::start

全局搜索dyldbootstrap::start,在汇编中找不到。

猜测start是C函数或者是C++函数,如果是C函数的话,函数格式一般: 返回值类型+空格+函数名(参数)。我们搜一下空格+start(。

//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
				intptr_t slide, const struct macho_header* dyldsMachHeader,
				uintptr_t* startGlue)
{
	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    slide = slideOfMainExecutable(dyldsMachHeader);
    bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
    shouldRebase = true;
#endif
    if ( shouldRebase ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

	// allow dyld to use mach messaging
	mach_init();

	// kernel sets up env pointer to be just past end of agv array
	const char** envp = &argv[argc+1];
	
	// kernel sets up apple pointer to be just past end of envp array
	const char** apple = envp;
	while(*apple != NULL) { ++apple; }
	++apple;

	// set up random value for stack canary
	__guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
	// run all C++ initializers inside dyld
	runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

	// now that we are done bootstrapping dyld, call dyld`s main
	uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
	return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

其实dyldbootstrap::start是指dyldbootstrap这个命名空间作用域里的 start函数.

最后进入到了dyld::_main函数

3. dyld::_main

// Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue)
{
    if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
		launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
	}
    ...代码省略
    if (sSkipMain) {
        if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
            dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
        }
        result = (uintptr_t)&fake_main;
        *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
    }
    
    return result;
}

代码太长,不上源码了,分析下主要流程。

3.1 配置环境变量,获取当前运行架构

checkEnvironmentVariables(envp);

3.2 加载共享缓存

	// load shared cache
	// 检测是否开启共享缓存
	checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
#if TARGET_IPHONE_SIMULATOR
	// <HACK> until <rdar://30773711> is fixed
	gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
	// </HACK>
#endif
	if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
	    // 加载共享缓存
		mapSharedCache();
	}

3.3 将dyld本身添加到UUID列表里

addDyldImageToUUIDList将dyld本身添加到UUID列表

3.4 reloadAllImags 加载镜像文件

reloadAllImags也是我们探索dyld流程的目的。这个image不是图片的意思,是镜像文件的意思。

  1. instantiateFromLoadedImage:加载主程序入口
// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
	// try mach-o loader
	if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
		ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
		addImage(image); // 加载image
		return (ImageLoaderMachO*)image;
	}
	
	throw "main executable not a known format";
}
  • 通过instantiateMainExecutable实例化主程序ImageLoader
  • 加载主程序ImageLoader
  1. instantiateMainExecutable:实例化主程序入口
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
	//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
	//	sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
	bool compressed;
	unsigned int segCount; // 数据段
	unsigned int libCount; // 静态库
	const linkedit_data_command* codeSigCmd;
	const encryption_info_command* encryptCmd;
	sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
	// instantiate concrete class based on content of load commands
	if ( compressed ) 
		return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
	else
#if SUPPORT_CLASSIC_MACHO
		return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
		throw "missing LC_DYLD_INFO load command";
#endif
}
  1. sniffLoadCommands 实例话主程序方法
// determine if this mach-o file has classic or compressed LINKEDIT and number of segments it has
void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
											unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
											const linkedit_data_command** codeSigCmd,
											const encryption_info_command** encryptCmd)
{
    *compressed = false; // 根据LC_DYLD_INFO_ONYL来决定
    *segCount = 0;       // MachO文件中segment数量,最多不超过255个
    *libCount = 0;       // MachO文件中依赖的动态库的数量
    *codeSigCmd = NULL;  // codeSigCmd: 签名信息
    *encryptCmd = NULL;  // 加密信息,如cryptid等
    
    const uint32_t cmd_count = mh->ncmds;
	const uint32_t sizeofcmds = mh->sizeofcmds;
	if ( sizeofcmds > (MAX_MACH_O_HEADER_AND_LOAD_COMMANDS_SIZE-sizeof(macho_header)) )
		dyld::throwf("malformed mach-o: load commands size (%u) > %u", sizeofcmds, MAX_MACH_O_HEADER_AND_LOAD_COMMANDS_SIZE);
	// 加载函数库的集合
	if ( cmd_count > (sizeofcmds/sizeof(load_command)) )
		dyld::throwf("malformed mach-o: ncmds (%u) too large to fit in sizeofcmds (%u)", cmd_count, sizeofcmds);
	const struct load_command* const startCmds = (struct load_command*)(((uint8_t*)mh) + sizeof(macho_header));
	const struct load_command* const endCmds = (struct load_command*)(((uint8_t*)mh) + sizeof(macho_header) + sizeofcmds);
	const struct load_command* cmd = startCmds;
	bool foundLoadCommandSegment = false;
	const macho_segment_command* linkeditSegCmd = NULL;
	const macho_segment_command* startOfFileSegCmd = NULL;
	const dyld_info_command* dyldInfoCmd = NULL;
	const symtab_command* symTabCmd = NULL;
	const dysymtab_command*	dynSymbTabCmd = NULL;
    
    ...代码省略
    
    // fSegmentsArrayCount is only 8-bits
    if ( *segCount > 255 )
    	dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
    
    // fSegmentsArrayCount is only 8-bits
    if ( *libCount > 4095 )
    	dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
    
    if ( needsAddedLibSystemDepency(*libCount, mh) )
    	*libCount = 1;
    ...
}

  • 这才是真正实例化主程序的函数
  • load_command:加载函数库的集合。我们通过mach-o分析应用的可执行文件可以发现在load_command里面是存放所有的库和镜像文件

3.5 loadInsertedDylib 插入动态库

遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载,通过该环境变量我们可以注入自定义的一些动态库代码从而完成安全攻防。

// load any inserted libraries
		if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
			for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
				loadInsertedDylib(*lib);
		}
static void loadInsertedDylib(const char* path)
{
	ImageLoader* image = NULL;
	unsigned cacheIndex;
	try {
		LoadContext context;
		context.useSearchPaths		= false;
		context.useFallbackPaths	= false;
		context.useLdLibraryPath	= false;
		context.implicitRPath		= false;
		context.matchByInstallName	= false;
		context.dontLoad			= false;
		context.mustBeBundle		= false;
		context.mustBeDylib			= true;
		context.canBePIE			= false;
		context.enforceIOSMac		= true;
		context.origin				= NULL;	// can`t use @loader_path with DYLD_INSERT_LIBRARIES
		context.rpath				= NULL;
		image = load(path, context, cacheIndex);
	}
	catch (const char* msg) {
		if ( gLinkContext.allowInsertFailures )
			dyld::log("dyld: warning: could not load inserted library '%s' into hardened process because %s\n", path, msg);
		else
			halt(dyld::mkstringf("could not load inserted library '%s' because %s\n", path, msg));
	}
	catch (...) {
		halt(dyld::mkstringf("could not load inserted library '%s'\n", path));
	}
}

3.6 ImageLoader::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();
			}
			// 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);
			}
		}

4. initializeMainExecutable 加载主函数启动程序

runInitializers -> processInitializers中,遍历recursiveInitialization

  1. 第一次执行时,进行libsystem初始化——doInitialization -> doImageInit -> libSystemInitialized
  2. libsystem的初始化 -> libdispatch_init初始化- > _os_object_init -> _objc_init
  3. _objc_init中注册并保存了map_images、load_images、unmap_image函数地址
  4. 注册完毕继续回到recursiveInitialization递归下一次调用

5. getEntryFromLC_MAIN 寻找主程序入口

_main的末尾,调用getEntryFromLC_MAIN读取Mach-O的LC_MAIN段获取程序的入口地址,也就是我们的main函数入口地址。

四.总结

  1. 动态库的加载是从libdyld.dylib`start:开始,经历了一些汇编然后进入到dyldbootstrap::start函数,然后从dyld::_main开始加载
  2. 第一步是通过checkEnvironmentVariables和defaultUninitializedFallbackPaths进行环境变量处理
  3. 再通过checkShareRegionDisable和mapSharedCache加载共享缓存
  4. 缓存加载完成后会将dyld本身添加到UUID列表里
  5. 然后开始加载镜像文件image
  • 先通过instantiateFromLoadedImag创建一个ImageLoader,用来加载image
  • 再通过loadInsertedDylib函数将动态库读取成镜像文件image
  • 遍历所有的镜像文件,将镜像文件链接到二进制文件中
  • 开始通过initializeMainExecutable初始化程序了
  • 遍历ImageLoader(其实一般只有一个),然后每个ImageLoader都进行runInitializers初始化操作
  • 然后在processInitializers函数里遍历ImageLoader里的每个image进行recursiveInitialization初始化
  • image初始化中最终会走到_object_init函数里面将相应的镜像文件的函数指针注册到对应dyld的的通知中去。

附上dyld流程图和思维导图

五.写在后面

写到最后,我已经懵了,dyld加载过程真是非常复杂,我只能简单捋一捋dyld的函数调用过程,写的不好大家见谅。