iOS进阶 - App启动 & dyld详解(二)

876 阅读28分钟

3. dyld::_main()

dyld::_main() 是整个App启动的关键函数,此函数里面做了很多事情

整个加载过程可细分为九步:

  • 第一步:设置运行环境
  • 第二步:加载共享缓存
  • 第三步:实例化主程序
  • 第四步:加载插入的动态库
  • 第五步:链接主程序
  • 第六步:链接插入的动态库
  • 第七步:执行弱符号绑定
  • 第八步:执行初始化方法
  • 第九步:查找入口点并返回

第一步 设置运行环境

这一步主要是设置运行参数、环境变量等。代码在开始的时候,将入参 mainExecutableMH 赋值给了sMainExecutableMachHeader,这是一个 macho_header 结构体,表示的是当前主程序的 Mach-O 头部信息,加载器依据 Mach-O 头部信息就可以解析整个 Mach-O 文件信息。接着调用 setContext() 设置上下文信息,包括一些回调函数、参数、标志信息等。设置的回调函数都是 dyld 模块自身实现的,如 loadLibrary() 函数实际调用的是 libraryLocator(),负责加载动态库。代码片断如下:

static void setContext(const macho_header* mainExecutableMH, int argc, const char* argv[], const char* envp[], const char* apple[])
{
   gLinkContext.loadLibrary         = &libraryLocator;
   gLinkContext.terminationRecorder = &terminationRecorder;
   ...
}

gLinkContextLinkContext 类型的 Struct,而 LinkContextImageLoader 内部定义的结构体。当然这个 image 不是图片的意思,它大概表示一个二进制文件(可执行文件或so文件),里面是被编译过的符号、代码等。ImageLoader 的作用就是将这些文件加载进内存。gLinkContext 所属的 ImageLoader 加载的就是当前App的可执行文件。

configureProcessRestrictions() 用来配置进程是否受限,根据当前进程是否受限,再次配置链接上下文以及其他环境参数。

checkEnvironmentVariables() 检测环境变量,如果 gLinkContext.allowEnvVarsPath 为 false 并且 gLinkContext.allowEnvVarsPrint 也为 false 就直接返回,否则调用 processDyldEnvironmentVariable() 处理并设置环境变量,源码如下:

void processDyldEnvironmentVariable(const char* key, const char* value, const char* mainExecutableDir)
{
	if ( strcmp(key, "DYLD_FRAMEWORK_PATH") == 0 ) {
		appendParsedColonList(value, mainExecutableDir, &sEnv.DYLD_FRAMEWORK_PATH);
	}
	else if ( strcmp(key, "DYLD_FALLBACK_FRAMEWORK_PATH") == 0 ) {
		appendParsedColonList(value, mainExecutableDir, &sEnv.DYLD_FALLBACK_FRAMEWORK_PATH);
	}
	else if ( strcmp(key, "DYLD_LIBRARY_PATH") == 0 ) {
		appendParsedColonList(value, mainExecutableDir, &sEnv.DYLD_LIBRARY_PATH);
	}
	else if ( strcmp(key, "DYLD_IMAGE_SUFFIX") == 0 ) {
		gLinkContext.imageSuffix = parseColonList(value, NULL);
	}
	else if ( strcmp(key, "DYLD_INSERT_LIBRARIES") == 0 ) {
		sEnv.DYLD_INSERT_LIBRARIES = parseColonList(value, NULL);
#if SUPPORT_ACCELERATE_TABLES
		sDisableAcceleratorTables = true;
#endif
	}
	else if ( strcmp(key, "DYLD_PRINT_OPTS") == 0 ) {
		sEnv.DYLD_PRINT_OPTS = true;
	}
	else if ( strcmp(key, "DYLD_PRINT_ENV") == 0 ) {
		sEnv.DYLD_PRINT_ENV = true;
	}
	else if ( strcmp(key, "DYLD_DISABLE_DOFS") == 0 ) {
		sEnv.DYLD_DISABLE_DOFS = true;
	}
	... // 省略
}	

可以看到有很多 DYLD_ 开头的环境变量,我们可以在 Xcode 的 Edit SchemaArgument 中配置这些环境变量,然后在 Xcode 的控制台中就会输出响应的信息,这些配置的环境变量最终会被保存到 EnvironmentVariables 类型的结构体实例中。

最后是调用 getHostInfo() 获取当前程序架构,至此,第一步的准备工作就完成了。

第二步 加载共享缓存

首先调用 checkSharedRegionDisable() 检查是否开启共享缓存,从方法内部可以看到iOS不能没有共享缓存。

接下来调用 mapSharedCache() 加载共享缓存,而 mapSharedCache() 里面实则是调用了 loadDyldCache(),从代码可以看出,共享缓存加载又分为三种情况:

  • 仅加载到当前进程,调用 mapCachePrivate()
  • 共享缓存已加载,不做任何处理。
  • 当前进程首次加载共享缓存,调用 mapCacheSystemWide()

mapSharedCache() 的源码如下:

static void mapSharedCache()
{
	dyld3::SharedCacheOptions opts;
	opts.cacheDirOverride	= sSharedCacheOverrideDir;
	opts.forcePrivate		= (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);

#if __x86_64__ && !TARGET_IPHONE_SIMULATOR
	opts.useHaswell			= sHaswell;
#else
	opts.useHaswell			= false;
#endif
	opts.verbose			= gLinkContext.verboseMapping;
	loadDyldCache(opts, &sSharedCacheLoadInfo);

	// update global state
	if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
		gLinkContext.dyldCache 								= sSharedCacheLoadInfo.loadAddress;
		dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
		dyld::gProcessInfo->sharedCacheSlide                = sSharedCacheLoadInfo.slide;
		dyld::gProcessInfo->sharedCacheBaseAddress          = (unsigned long)sSharedCacheLoadInfo.loadAddress;
		sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
		dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
	}
}

loadDyldCache() 的源码如下:

bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
    results->loadAddress        = 0;
    results->slide              = 0;
    results->errorMessage       = nullptr;

#if TARGET_IPHONE_SIMULATOR
    // simulator only supports mmap()ing cache privately into process
    return mapCachePrivate(options, results);
#else
    if ( options.forcePrivate ) {
        // mmap cache into this process only
        // 仅加载到当前进程
        return mapCachePrivate(options, results);
    }
    else {
        // fast path: when cache is already mapped into shared region
        bool hasError = false;
        if ( reuseExistingCache(options, results) ) {
        		// 共享缓存已加载,不做任何处理
            hasError = (results->errorMessage != nullptr);
        } else {
            // slow path: this is first process to load cache
            // 当前进程首次加载共享缓存
            hasError = mapCacheSystemWide(options, results);
        }
        return hasError;
    }
#endif
}
共享缓存机制

在iOS系统中,每个程序依赖的动态库都需要通过 dyld 一个一个加载到内存,然而,很多系统库几乎是每个程序都会用到的,如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,OS X 的缓存文件存放在 /private/var/db/dyld/ 目录下,iOS 的存放在 /System/Library/Caches/com.apple.dyld/ 目录下,按不同的架构保存分别保存着。笔者的iPhone6s里面就有 dyld_shared_cache_armv7sdyld_shared_cache_armv64 两个文件。

当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,dyld 首先会检查共享缓存看看该文件是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了 OS X 和 iOS 上程序的启动时间。

第三步 实例化主程序

这一步系统会对已经映射到进程空间的主程序(在内核解析 MachO 阶段就完成了映射操作)创建一个 ImageLoader,再将其加入到 sAllImages 中。instantiateFromLoadedImage() 首先调用 isCompatibleMachO() 检测 Mach-O 头部的 magic、cputype、cpusubtype 等相关属性,判断 Mach-O 文件的兼容性,如果兼容性满足,则调用 ImageLoaderMachO::instantiateMainExecutable() 方法完成主程序 ImageLoader 的创建和添加操作,源码如下:

// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
// 在dyld获得控制权之前,内核已经将App的可执行文件映射到进程空间了,我们需要为已经映射到进程空间的主可执行文件创建一个ImageLoader。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
	// try mach-o loader
  // 验证mach-o类型是否支持
	if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
    // 创建主程序Mach-O的ImageLoader
		ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
    // 添加主二进制文件到sAllImages数组中
		addImage(image);
		return (ImageLoaderMachO*)image;
	}
	
	throw "main executable not a known format";
}

ImageLoaderMachO::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
  // 基于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
}

这里首先会调用 sniffLoadCommands() 函数来获取一些数据

  • compressed,是否是压缩过的 Mach-O 文件。若 Mach-O 存在 LC_DYLD_INFOLC_DYLD_INFO_ONLY 加载命令,则说明是压缩类型的 Mach-O:

    switch (cmd->cmd) {
    case LC_DYLD_INFO:
    case LC_DYLD_INFO_ONLY:
        if ( cmd->cmdsize != sizeof(dyld_info_command) )
            throw "malformed mach-o image: LC_DYLD_INFO size wrong";
        dyldInfoCmd = (struct dyld_info_command*)cmd;
        // 存在LC_DYLD_INFO或者LC_DYLD_INFO_ONLY则表示是压缩类型的Mach-O
        *compressed = true;
        break;
        ...
    }
    
  • segCount,段数量。根据 LC_SEGMENT_COMMAND 加载命令来统计段数量,由源码可以知道段的数量不能超过255个:

    case LC_SEGMENT_COMMAND:
        segCmd = (struct macho_segment_command*)cmd;
    ...
        if ( segCmd->vmsize != 0 )
            *segCount += 1;
    if ( *segCount > 255 )
        dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
    
  • libCount,动态库数量。根据 LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIBLC_REEXPORT_DYLIBLC_LOAD_UPWARD_DYLIB 这几个加载命令来统计库的数量,库的数量不能超过4095个。

    case LC_LOAD_DYLIB:
    case LC_LOAD_WEAK_DYLIB:
    case LC_REEXPORT_DYLIB:
    case LC_LOAD_UPWARD_DYLIB:
    *libCount += 1;
    if ( *libCount > 4095 )
        dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
    
  • codeSigCmd,通过解析 LC_CODE_SIGNATURE 来获取代码签名加载命令:

    case LC_CODE_SIGNATURE:
    *codeSigCmd = (struct linkedit_data_command*)cmd;
    break;
    
  • encryptCmd:通过 LC_ENCRYPTION_INFOLC_ENCRYPTION_INFO_64 来获取段的加密信息:

    case LC_ENCRYPTION_INFO:
    ...
    *encryptCmd = (encryption_info_command*)cmd;
    break;
    case LC_ENCRYPTION_INFO_64:
    ...
    *encryptCmd = (encryption_info_command*)cmd;
    break;
    

ImageLoader 是抽象类,其子类负责把 Mach-O 文件实例化为 image,当 sniffLoadCommands() 解析完以后,根据 compressed 的值来决定调用哪个子类进行实例化。

实例化完成之后,调用 addImage(),将 image 加入到 sAllImages 全局镜像列表中。

static void addImage(ImageLoader* image)
{
	// add to master list
    allImagesLock();
        sAllImages.push_back(image);
    allImagesUnlock();
	
	// update mapped ranges
	uintptr_t lastSegStart = 0;
	uintptr_t lastSegEnd = 0;
	for(unsigned int i=0, e=image->segmentCount(); i < e; ++i) {
		if ( image->segUnaccessible(i) ) 
			continue;
		uintptr_t start = image->segActualLoadAddress(i);
		uintptr_t end = image->segActualEndAddress(i);
		if ( start == lastSegEnd ) {
			// two segments are contiguous, just record combined segments
			lastSegEnd = end;
		}
		else {
			// non-contiguous segments, record last (if any)
			if ( lastSegEnd != 0 )
				addMappedRange(image, lastSegStart, lastSegEnd);
			lastSegStart = start;
			lastSegEnd = end;
		}		
	}
	if ( lastSegEnd != 0 )
		addMappedRange(image, lastSegStart, lastSegEnd);

	if ( gLinkContext.verboseLoading || (sEnv.DYLD_PRINT_LIBRARIES_POST_LAUNCH && (sMainExecutable!=NULL) && sMainExecutable->isLinked()) ) {
		dyld::log("dyld: loaded: %s\n", image->getPath());
	}
}

我们在lldb调试时输入image list 查看所有image,第一个出现的是主程序模块,就是因为这里是第一个把主程序添加到 sAllImages 数组内。

第四步 加载插入的动态库

先判断环境变量 DYLD_INSERT_LIBRARIES 中是否存在要加载的动态库,如果存在则调用 loadInsertedDylib() 依次加载:

if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
	for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
	loadInsertedDylib(*lib);
}

loadInsertedDylib() 内部设置了一个 LoadContext 参数后,调用了 load() 函数:

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));
	}
}

load() 方法不仅被 loadInsertedDylib() 调用,也会被 dlopen等运行时加载动态库的方法使用。

/**
做路径展开,搜索查找,排重,以及缓存查找工作。其中路径的展开和搜索分几个阶段(phase)
*/
ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
{
	CRSetCrashLogMessage2(path);
	const char* orgPath = path;
	cacheIndex = UINT32_MAX;
	
	//dyld::log("%s(%s)\n", __func__ , path);
	char realPath[PATH_MAX];
	// when DYLD_IMAGE_SUFFIX is in used, do a realpath(), otherwise a load of "Foo.framework/Foo" will not match
	if ( context.useSearchPaths && ( gLinkContext.imageSuffix != NULL && *gLinkContext.imageSuffix != NULL) ) {
		if ( realpath(path, realPath) != NULL )
			path = realPath;
	}
	
	// try all path permutations and check against existing loaded images
	ImageLoader* image = loadPhase0(path, orgPath, context, cacheIndex, NULL);
	if ( image != NULL ) {
		CRSetCrashLogMessage2(NULL);
		return image;
	}

	// try all path permutations and try open() until first success
	std::vector<const char*> exceptions;
	image = loadPhase0(path, orgPath, context, cacheIndex, &exceptions);
#if !TARGET_IPHONE_SIMULATOR
	// <rdar://problem/16704628> support symlinks on disk to a path in dyld shared cache
	if ( image == NULL)
		image = loadPhase2cache(path, orgPath, context, cacheIndex, &exceptions);
#endif
	... // 省略
}

load() 函数的实现为一系列的 loadPhase*() 函数,loadPhase1~6 这些 phase 的搜索路径对应各个环境变量如下:DYLD_ROOT_PATH -> LD_LIBRARY_PATH -> DYLD_FRAMEWORK_PATH -> 原始路径 -> DYLD_FALLBACK_LIBRARY_PATH

如果 loadPhase0 返回的是空地址,则走 loadPhase2cache 方法去缓存中查找,找到以后从 ImageLoaderMachO::instantiateFromCache 方法去实例化,否则抛异常。

当内部调用到 loadPhase5load() 函数的时候,会先在共享缓存中搜寻,如果存在则使用 ImageLoaderMachO::instantiateFromCache() 来实例化 ImageLoader,否则通过 loadPhase5open() 打开文件并读取数据到内存后,再调用 loadPhase6(),通过 ImageLoaderMachO::instantiateFromFile() 实例化 ImageLoader,最后调用 checkandAddImage() 验证镜像并将其加入到全局镜像列表 sAllImages 中。

根据上面的分析,主程序 ImageLoader 在全局 sAllImages 表的首位,后面的是插入的动态库的 ImageLoader,每个动态库对应一个 ImageLoader。

第五步 链接主程序

这一步调用 link() 函数将实例化后的主程序进行动态修正,让二进制变为可正常执行的状态。link() 函数内部调用了 ImageLoader::link() 函数:

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
	//dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
	
	// clear error strings
	(*context.setErrorStrings)(0, NULL, NULL, NULL);

	uint64_t t0 = mach_absolute_time();
  // 递归加载主程序所需依赖库
	this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
	context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);

	// we only do the loading step for preflights
	if ( preflightOnly )
		return;

	uint64_t t1 = mach_absolute_time();
	context.clearAllDepths();
  // 递归刷新依赖库的层级
	this->recursiveUpdateDepth(context.imageCount());

	__block uint64_t t2, t3, t4, t5;
	{
		dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
		t2 = mach_absolute_time();
    // 递归进行rebase
		this->recursiveRebase(context);
		context.notifyBatch(dyld_image_state_rebased, false);

		t3 = mach_absolute_time();
		if ( !context.linkingMainExecutable )
      // 递归绑定符号表
			this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);

		t4 = mach_absolute_time();
		if ( !context.linkingMainExecutable )
      // 弱符号绑定
			this->weakBind(context);
		t5 = mach_absolute_time();
	}

    if ( !context.linkingMainExecutable )
        context.notifyBatch(dyld_image_state_bound, false);
	uint64_t t6 = mach_absolute_time();	

	std::vector<DOFInfo> dofs;
  // 注册DOF节
	this->recursiveGetDOFSections(context, dofs);
	context.registerDOFs(dofs);
	uint64_t t7 = mach_absolute_time();	

	// interpose any dynamically loaded images
	if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
		dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
		this->recursiveApplyInterposing(context);
	}

	// clear error strings
	(*context.setErrorStrings)(0, NULL, NULL, NULL);

	fgTotalLoadLibrariesTime += t1 - t0;
	fgTotalRebaseTime += t3 - t2;
	fgTotalBindTime += t4 - t3;
	fgTotalWeakBindTime += t5 - t4;
	fgTotalDOF += t7 - t6;
	
	// done with initial dylib loads
	fgNextPIEDylibAddress = 0;
}

从源代码可以看到,这一步主要做了以下几个事情:

  • recursiveLoadLibraries() 根据 LC_LOAD_DYLIB 加载命令递归地把主程序所需的依赖库加载进内存。
  • recursiveUpdateDepth() 递归刷新依赖库的层级。
  • recursiveRebase() 由于ASLR的存在,必须递归对主程序以及依赖库进行重定位操作。
  • recursiveBind() 把主程序二进制和依赖进来的动态库全部执行符号表绑定。对于 nolazy 的符号进行递归绑定,lazy 的符号会在运行时动态绑定(首次被调用才去绑定)。
  • weakBind() 如果链接的不是主程序二进制的话,会在此时执行弱符号绑定,主程序二进制则在 link() 完后再执行弱符号绑定,后面会进行分析。
  • recursiveGetDOFSections()context.registerDOFs() 注册DOF(DTrace Object Format)节。

第六步 链接插入的动态库

这一步与链接主程序一样,将前面调用 addImage() 函数保存在 sAllImages 中的动态库列表循环取出并调用 link() 进行链接,需要注意的是,sAllImages 中保存的第一项是主程序的镜像,所以要从第二个开始,取到的才是动态库的 ImageLoader:

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);
			}
		}

接下来循环调用每个镜像的 registerInterposing() 函数来注册插入操作,该函数会遍历 Mach-O 的 LC_SEGMENT_COMMAND 加载命令,读取 __DATA,__interpose 节区,找到需要注册插入操作(也可以叫作符号地址替换)的数据,然后做一些检查后,将要替换的符号与被替换的符号信息存入 fgInterposingTuples 列表中,供以后具体符号替换时查询。

void ImageLoaderMachO::registerInterposing(const LinkContext& context)
{
	// mach-o files advertise interposing by having a __DATA __interpose section
	struct InterposeData { uintptr_t replacement; uintptr_t replacee; };
	const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
	const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
	const struct load_command* cmd = cmds;
	for (uint32_t i = 0; i < cmd_count; ++i) {
		switch (cmd->cmd) {
			case LC_SEGMENT_COMMAND:
				{
					const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
					const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
					const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
					for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
						if ( ((sect->flags & SECTION_TYPE) == S_INTERPOSING) || ((strcmp(sect->sectname, "__interpose") == 0) && (strcmp(seg->segname, "__DATA") == 0)) ) {
							// <rdar://problem/23929217> Ensure section is within segment
							if ( (sect->addr < seg->vmaddr) || (sect->addr+sect->size > seg->vmaddr+seg->vmsize) || (sect->addr+sect->size < sect->addr) )
								dyld::throwf("interpose section has malformed address range for %s\n", this->getPath());
							const InterposeData* interposeArray = (InterposeData*)(sect->addr + fSlide);
							const size_t count = sect->size / sizeof(InterposeData);
							for (size_t j=0; j < count; ++j) {
								ImageLoader::InterposeTuple tuple;
								tuple.replacement		= interposeArray[j].replacement;
								tuple.neverImage		= this;
								tuple.onlyImage		    = NULL;
								tuple.replacee			= interposeArray[j].replacee;
								// <rdar://problem/25686570> ignore interposing on a weak function that does not exist
								if ( tuple.replacee == 0 )
									continue;
								// <rdar://problem/7937695> verify that replacement is in this image
								if ( this->containsAddress((void*)tuple.replacement) ) {
									// chain to any existing interpositions
									for (std::vector<InterposeTuple>::iterator it=fgInterposingTuples.begin(); it != fgInterposingTuples.end(); it++) {
										if ( it->replacee == tuple.replacee ) {
											tuple.replacee = it->replacement;
										}
									}
									ImageLoader::fgInterposingTuples.push_back(tuple);
								}
							}
						}
					}
				}
				break;
		}
		cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
	}
}

接着调用 applyInterposing() 函数,内部经由 doInterpose() 虚函数进行符号替换操作,以ImageLoaderMachOCompressed::doInterpose() 函数的实现为例,该函数内部调用了 eachBind()eachLazyBind() 分别对常规的符号与延迟加载的符号进行应用插入操作,具体处理函数是 interposeAt(),该函数调用 interposedAddress()fgInterposingTuples 中查找需要替换的符号地址,找到后进行最终的符号地址替换,代码如下:

// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
	sImageRoots[i]->applyInterposing(gLinkContext);
}
--------------------------------------------------------------------
void ImageLoader::applyInterposing(const LinkContext& context)
{
	dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
	if ( fgInterposingTuples.size() != 0 )
		this->recursiveApplyInterposing(context);
}
--------------------------------------------------------------------
void ImageLoader::recursiveApplyInterposing(const LinkContext& context)
{ 
	if ( ! fInterposed ) {
		// break cycles
		fInterposed = true;
		
		try {
			// interpose lower level libraries first
			for(unsigned int i=0; i < libraryCount(); ++i) {
				ImageLoader* dependentImage = libImage(i);
				if ( dependentImage != NULL )
					dependentImage->recursiveApplyInterposing(context);
			}
				
			// interpose this image
			doInterpose(context);
		}
		catch (const char* msg) {
			// this image is not interposed
			fInterposed = false;
			throw;
		}
	}
}
--------------------------------------------------------------------
void ImageLoaderMachOCompressed::doInterpose(const LinkContext& context)
{
	if ( context.verboseInterposing )
		dyld::log("dyld: interposing %lu tuples onto image: %s\n", fgInterposingTuples.size(), this->getPath());

	// update prebound symbols
	eachBind(context, ^(const LinkContext& ctx, ImageLoaderMachOCompressed* image,
						uintptr_t addr, uint8_t type, const char* symbolName,
						uint8_t symbolFlags, intptr_t addend, long libraryOrdinal,
						ExtraBindData *extraBindData,
						const char* msg, LastLookup* last, bool runResolver) {
		return ImageLoaderMachOCompressed::interposeAt(ctx, image, addr, type, symbolName, symbolFlags,
													   addend, libraryOrdinal, extraBindData,
													   msg, last, runResolver);
	});
	eachLazyBind(context, ^(const LinkContext& ctx, ImageLoaderMachOCompressed* image,
							uintptr_t addr, uint8_t type, const char* symbolName,
							uint8_t symbolFlags, intptr_t addend, long libraryOrdinal,
							ExtraBindData *extraBindData,
							const char* msg, LastLookup* last, bool runResolver) {
		return ImageLoaderMachOCompressed::interposeAt(ctx, image, addr, type, symbolName, symbolFlags,
													   addend, libraryOrdinal, extraBindData,
													   msg, last, runResolver);
	});
}
--------------------------------------------------------------------
uintptr_t ImageLoaderMachOCompressed::interposeAt(const LinkContext& context, ImageLoaderMachOCompressed* image,
												  uintptr_t addr, uint8_t type, const char*,
                                                  uint8_t, intptr_t, long,
                                                  ExtraBindData *extraBindData,
                                                  const char*, LastLookup*, bool runResolver)
{
	if ( type == BIND_TYPE_POINTER ) {
		uintptr_t* fixupLocation = (uintptr_t*)addr;
		uintptr_t curValue = *fixupLocation;
		uintptr_t newValue = interposedAddress(context, curValue, image);
		if ( newValue != curValue) {
			*fixupLocation = newValue;
		}
	}
	return 0;
}
--------------------------------------------------------------------
uintptr_t ImageLoader::interposedAddress(const LinkContext& context, uintptr_t address, const ImageLoader* inImage, const ImageLoader* onlyInImage)
{
	//dyld::log("interposedAddress(0x%08llX), tupleCount=%lu\n", (uint64_t)address, fgInterposingTuples.size());
	for (std::vector<InterposeTuple>::iterator it=fgInterposingTuples.begin(); it != fgInterposingTuples.end(); it++) {
		//dyld::log("    interposedAddress: replacee=0x%08llX, replacement=0x%08llX, neverImage=%p, onlyImage=%p, inImage=%p\n", 
		//				(uint64_t)it->replacee, (uint64_t)it->replacement,  it->neverImage, it->onlyImage, inImage);
		// replace all references to 'replacee' with 'replacement'
		if ( (address == it->replacee) && (inImage != it->neverImage) && ((it->onlyImage == NULL) || (inImage == it->onlyImage)) ) {
			if ( context.verboseInterposing ) {
				dyld::log("dyld interposing: replace 0x%lX with 0x%lX\n", it->replacee, it->replacement);
			}
			return it->replacement;
		}
	}
	return address;
}

再接着调用 recursiveBind() 函数,把之前做过符号替换的符号重新绑定:

// Bind and notify for the inserted images now interposing has been registered
// 绑定和通知插入的image现在已经注册
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);
	}
}

第七步 执行弱符号绑定

weakBind() 首先通过 getCoalescedImages() 合并所有动态库的弱符号到一个列表里,然后调用 initializeCoalIterator() 对需要绑定的弱符号进行排序,接着调用 incrementCoalIterator() 读取 dyld_info_command 结构的 weak_bind_offweak_bind_size 字段,确定弱符号的数据偏移与大小,最终进行弱符号绑定,代码如下:

void ImageLoader::weakBind(const LinkContext& context)
{
	if ( context.verboseWeakBind )
		dyld::log("dyld: weak bind start:\n");
	uint64_t t1 = mach_absolute_time();
	// get set of ImageLoaders that participate in coalecsing
	ImageLoader* imagesNeedingCoalescing[fgImagesRequiringCoalescing];
	unsigned imageIndexes[fgImagesRequiringCoalescing];
	int count = context.getCoalescedImages(imagesNeedingCoalescing, imageIndexes);
	
	... // 省略

#if __arm64e__
		for (int i=0; i < count; ++i) {
			if ( imagesNeedingCoalescing[i]->usesChainedFixups() ) {
				// during binding of references to weak-def symbols, the dyld cache was patched
				// but if main executable has non-weak override of operator new or delete it needs is handled here
				if ( !imagesNeedingCoalescing[i]->weakSymbolsBound(imageIndexes[i]) ) {
					for (const char* weakSymbolName : sTreatAsWeak) {
						const ImageLoader* dummy;
						imagesNeedingCoalescing[i]->resolveWeak(context, weakSymbolName, true, false, &dummy);
					}
				}
			}
			else {
				// look for weak def symbols in this image which may override the cache
				ImageLoader::CoalIterator coaler;
				imagesNeedingCoalescing[i]->initializeCoalIterator(coaler, i, 0);
				imagesNeedingCoalescing[i]->incrementCoalIterator(coaler);
				while ( !coaler.done ) {
					imagesNeedingCoalescing[i]->incrementCoalIterator(coaler);
					const ImageLoader* dummy;
					// a side effect of resolveWeak() is to patch cache
					imagesNeedingCoalescing[i]->resolveWeak(context, coaler.symbolName, true, false, &dummy);
				}
			}
		}
#endif

		// mark all as having all weak symbols bound
		for(int i=0; i < count; ++i) {
			imagesNeedingCoalescing[i]->setWeakSymbolsBound(imageIndexes[i]);
		}
	}

	uint64_t t2 = mach_absolute_time();
	fgTotalWeakBindTime += t2  - t1;
	
	if ( context.verboseWeakBind )
		dyld::log("dyld: weak bind end\n");
}

第八步 执行初始化方法

Objc & Runtime

initializeMainExecutable 开始执行初始化操作,可以看到方法内有针对 DYLD_PRINT_STATISTICSDYLD_PRINT_STATISTICS_DETAILS 这两个环境变量的打印操作,我们在打印 pre-main 启动时间的时候会用到这个环境变量。

void initializeMainExecutable()
{
	// record that we've reached this step
	// 已经记录到此步骤
	gLinkContext.startedInitializingMainExecutable = true;

	// run initialzers for any inserted dylibs
	// 为已插入的动态库执行初始化
	ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
	initializerTimes[0].count = 0;
	const size_t rootCount = sImageRoots.size();
	if ( rootCount > 1 ) {
		for(size_t i=1; i < rootCount; ++i) {
			sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
		}
	}
	
	// run initializers for main executable and everything it brings up
	// 为主可执行文件及其引发的所有内容运行初始化程序
	sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
	
	// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
	if ( gLibSystemHelpers != NULL ) 
		(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);

	// dump info if requested
	// 如果设置了打印参数,打印具体的启动时间
	if ( sEnv.DYLD_PRINT_STATISTICS )
		ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
	if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
		ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

内部调用了 runInitializers() ,接着调用了 processInitializers() ,再接着调用了recursiveInitialization() ,这样从下往上开始运行初始化器,这样可以保证很安全的调用所依赖的内容。

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
										  InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
	recursive_lock lock_info(this_thread);
	recursiveSpinLock(lock_info);

	if ( fState < dyld_image_state_dependents_initialized-1 ) {
		uint8_t oldState = fState;
		// break cycles
		fState = dyld_image_state_dependents_initialized-1;
		try {
			// initialize lower level libraries first
			for(unsigned int i=0; i < libraryCount(); ++i) {
				ImageLoader* dependentImage = libImage(i);
				if ( dependentImage != NULL ) {
					// don't try to initialize stuff "above" me yet
					if ( libIsUpward(i) ) {
						uninitUps.images[uninitUps.count] = dependentImage;
						uninitUps.count++;
					}
					else if ( dependentImage->fDepth >= fDepth ) {
						dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
					}
                }
			}
			
			// record termination order
			if ( this->needsTermination() )
				context.terminationRecorder(this);
			
			// let objc know we are about to initialize this image
			// 通知objc即将初始化此image
			uint64_t t1 = mach_absolute_time();
			fState = dyld_image_state_dependents_initialized;
			oldState = fState;
			context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
			
			// initialize this image
			// 初始化此image
			bool hasInitializers = this->doInitialization(context);

			// let anyone know we finished initializing this image
			// 通知所有人已经完成初始化此image
			fState = dyld_image_state_initialized;
			oldState = fState;
			context.notifySingle(dyld_image_state_initialized, this, NULL);
			
			if ( hasInitializers ) {
				uint64_t t2 = mach_absolute_time();
				timingInfo.addTime(this->getShortName(), t2-t1);
			}
		}
		catch (const char* msg) {
			// this image is not initialized
			fState = oldState;
			recursiveSpinUnLock();
			throw;
		}
	}
	
	recursiveSpinUnLock();
}

我们在这里看到了 notifySingle() 函数,接着跟进 notifySingle() 函数,看到下面处理代码:

if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
    uint64_t t0 = mach_absolute_time();
    (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
    uint64_t t1 = mach_absolute_time();
    uint64_t t2 = mach_absolute_time();
    uint64_t timeInObjC = t1-t0;
    uint64_t emptyTime = (t2-t1)*100;
    if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
        timingInfo->addTime(image->getShortName(), timeInObjC);
    }
}

我们关心的只有 sNotifyObjCInit 这个回调,搜索 sNotifyObjCInit,找到是在 registerObjCNotifiers() 函数内赋值:

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
	for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
		ImageLoader* image = *it;
		if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
			dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
			(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		}
	}
}

从字面意思可以明白,传进来的分别是 map,init,unmap 事件的回调。接着全局搜索什么地方调用了 registerObjCNotifiers()

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}

那么到底谁调用了 _dyld_objc_notify_register() 呢?静态分析已经无法得知,只能对 _dyld_objc_notify_register() 下个符号断点观察一下了。

_dyld_objc_notify_register

从调用栈看到是 libobjc.A.dylib 的 _objc_init 函数调用 _dyld_objc_notify_register() 注册了这个通知,接下来我们打开 runtime的开源库,在 objc-os.mm 文件中找到 _objc_init 方法:

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_objc_init 是在 libsystem 中的一个initialize方法 libsystem_initializer 中初始化了 libdispatch,然后 libdispatch 调用了 _os_object_int,最终调用了 _objc_init

map_images

由上面的代码可以看出,在初始化 libsystem 动态库的时候,Runtime 向 dyld 中注册了回调函数,在这个时候,对已经加载如内存中的所有 image,递归调用了 map_images 回调,而之后再加载进来的 image,dyld 会在 bind 操作结束之后,发出 dyld_image_state_bound 通知,然后调用 map_images 回调。

map_images 主要做以下几件事来完成 Objc Setup

  1. 读取二进制文件的 DATA 段内容,找到与 objc 相关的信息
  2. 注册 Objc 类
  3. 确保 selector 的唯一性
  4. 读取 protocol 以及 category 的信息

map_images 内部最主要的就是 _read_images 函数:

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unopt imizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t i;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0; 
    static bool doneOnce;
    TimeLogger ts(PrintImageTimes);
#define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++
    if (!doneOnce) { 
         doneOnce = YES;
         //实例化存储类的哈希表,并根据当前类数量做动态扩容
         int namedClassesSize = (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
         gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
     }
//由编辑器读取类列表,并将所有的类添加到哈希表中,并标记懒加载的类并初始化空间
    for (EACH_HEADER) {
         if (! mustReadClasses(hi)) { 
            continue;
         }
         bool headerIsBundle = hi->isBundle();
         bool headerIsPreoptimized = hi->isPreoptimized();
         /** 将新类添加到哈希表中 */
         // 从编译后的类列表中取出所有类,获取到一个classref_t类型指针
         classref_t *classlist = _getObjc2ClassList(hi, &count);
         for (i = 0; i < count; i++) {
             // 数组中取出OS_dispatch_queue_concurrent、OS_xpc_object、NSRunLoop等系统类<CF、Fundation、liddispatch等>和自己创建的类
             Class cls = (Class)classlist[i];
             // 通过readClass函数获取处理过后的新类,内部主要操作ro和rw结构体
             Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
             // 初始化所有懒加载的类需要的内存空间
             if (newCls != cls && newCls) {
                 // 将懒加载的类添加到数组中 
                 resolvedFutureClasses = (Class *)realloc(resolvedFutureClasses, (resolvedFutureClassCount+1) * sizeof(Class));
                 resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
             }
         }
     }

//将为映射的Class和superClass重映射,被remap的类都是非懒加载的类
     if (!noClassesRemapped()) {
         for (EACH_HEADER) {
         // 重映射Class,从_getObjc2ClassRefs函数中取出类的引用
             Class *classrefs = _getObjc2ClassRefs(hi, &count);
             for (i = 0; i < count; i++) {
                 remapClassRef(&classrefs[i]);
             }
         } 
         // 重映射父类
         classrefs = _getObjc2SuperRefs(hi, &count); 
         for (i = 0; i < count; i++) {
             remapClassRef(&classrefs[i]);
         }
     }

     // 对SEL做重映射
     static size_t UnfixedSelectors; sel_lock();
     for (EACH_HEADER) {
         if (hi->isPreoptimized()) continue;
         bool isBundle = hi->isBundle();
         SEL *sels = _getObjc2SelectorRefs(hi, &count); UnfixedSelectors += count;
         for (i = 0; i < count; i++) {
             const char *name = sel_cname(sels[i]);
             //注册SEL
             sels[i] = sel_registerNameNoLock(name, isBundle);
         } 
     }
     // 修复旧的函数指针遗留
     for (EACH_HEADER) {
         message_ref_t *refs = _getObjc2MessageRefs(hi, &count); 
         if (count == 0) continue;
         for (i = 0; i < count; i++) {
             // 在内部将常用的alloc、objc_msgSend等函数指针进行注册,并fix为新的函数指针
             fixupMessageRef(refs+i);
         }
     }
     // 遍历所有协议列表,并且将协议列表加载到Protocol的哈希表中
     for (EACH_HEADER) {
         extern objc_class OBJC_CLASS_$_Protocol;
         // cls = Protocol类,所有协议和对象的结构体都类似,isa对应Protocol类
         Class cls = (Class)&OBJC_CLASS_$_Protocol;
         assert(cls);
         // 获取protocol哈希表
         NXMapTable *protocol_map = protocols();
         bool isPreoptimized = hi->isPreoptimized();
         bool isBundle = hi->isBundle();
         // 从编译器中读取并初始化Protocol
         protocol_t **protolist = _getObjc2ProtocolList(hi, &count); 
         for (i = 0; i < count; i++) {
            readProtocol(protolist[i], cls, protocol_map,
                         isPreoptimized, isBundle);
         }
     }
     // 修复协议列表应用,优化后的images不确定正确与否
     for (EACH_HEADER) {
         //下面的_getObjc2ProtocolRefs函数与上面的_getObjc2ProtocolRefs不同
         protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
         for (i = 0; i < count; i++) {
            remapProtocolRef(&protolist[i]);
         }
     }
     
     // 实现非懒加载的类
     for (EACH_HEADER) {
         classref_t *classlist =
            _getObjc2NonlazyClassList(hi, &count);
         for (i = 0; i < count; i++) {
             Class cls = remapClass(classlist[i]);
             if (!cls) continue;
             //实现非来加载的类(实例化一些信息,如rw)
             realizeClass(cls);
         }    
     }
     
     // 遍历resolvedFutureClasses数组,实现所有懒加载的类
     if (resolvedFutureClasses) {
         for (i = 0; i < resolvedFutureClassCount; i++) {
             // 实现懒加载的类
             realizeClass(resolvedFutureClasses[i]); resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
         }
        free(resolvedFutureClasses);
     }
     
// 发现和处理所有Category
     for (EACH_HEADER) {
         // 外部循环遍历找到当前类,查找类对应的Category数组
         category_t **catlist = _getObjc2CategoryList(hi, &count);
         bool hasClassProperties = hi->info()->hasCategoryClassProperties();

         // 内部循环遍历当前类的所有的Category 
         for (i = 0; i < count; i++) {
             category_t *cat = catlist[i];
             Class cls = remapClass(cat->cls);
             // 首先,通过所属的类注册Category。如果这个类已经被实现,则重新构造类的方法列表 
             bool classExists = NO;
             if (cat->instanceMethods || cat->protocols || cat->instanceProperties) {
                 // 将Category添加到对应的value中,value是Class对应的所有的Category数组
                 addUnattachedCategoryForClass(cat, cls, hi);
                 // 将Category的method,protocol,property添加到Class中
                 if (cls->isRealized()) {
                     remethodizeClass(cls);
                     classExists = YES;
                 }
             }
             // 与下面逻辑相同,不过是在Meta Class中进行操作
             if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) {
                 addUnattachedCategoryForClass(cat, cls->ISA(), hi); 
                 if (cls->ISA()->isRealized()) {
                     remethodizeClass(cls->ISA());
                 }

             }
         }
     }
     
     // 初始化所有的类,Category必须是最后执行
     // DebugNonFragileIvars = -1,不执行
     if (DebugNonFragileIvars) {
         realizeAllClasses();
     }
#undef EACH_HEADER
}

read_images主要逻辑:

  1. 加载所有类到 gbd_objc_readlized_classes 表中
  2. 对所有类做重映射
  3. 将所有SEL都注册到 namedSelectors 表中
  4. 修复函数指针遗留
  5. 将所有的 Protocol 都添加到 protocol_map 表中
  6. 对所有的 Protocol 做重映射
  7. 初始化所有非懒加载的类,进行 rw、ro 操作
  8. 遍历已标记的懒加载的类,并做初始化操作
  9. 处理所有 Category,包括 Class 和 Meta Class
  10. 初始化所有未初始化的类
load_images

在收到 dyld_image_state_dependents_initialized 的通知以后,就调用 load_images 回调,回调里面调用了 call_load_methods() 来执行所有的 + load 方法。

void load_images(const char *path __unused, const struct mach_header *mh) {
  // Return without taking locks if there are no +load methods here.
  if (!hasLoadMethods((const headerType *)mh)) return;
  recursive_mutex_locker_t lock(loadMethodLock);
  // Discover load methods
  {
    mutex_locker_t lock2(runtimeLock);
    //主要查找Class、Category的方法
    prepare_load_methods((const headerType *)mh);
  }
  // Call +load methods (without runtimeLock - re-entrant)
  // 调用Class、Category的方法列表
  call_load_methods();
}
void prepare_load_methods(const headerType *mhdr) {
  size_t count, i;
  runtimeLock.assertLocked();
  // 获取到非懒加载类的列表
  classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);
  for (i = 0; i < count; i++) {
    // 设置Class的调用列表
    schedule_class_load(remapClass(classlist[i]));
  }
  // 获取Category的列表
  category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
  for (i = 0; i < count; i++) {
    category_t *cat = categorylist[i];
    Class cls = remapClass(cat->cls);
    // category for ignored weak-linked class 忽略弱链接
    if (!cls) continue;  
    // 实例化所属的类
    realizeClass(cls);
    assert(cls->ISA()->isRealized());
    // 设置Categoty的调用列表
    add_category_to_loadable_list(cat);
  }
}
// 这个方法会对入参的父类进行递归调用,以确保父类优先的顺序
static void schedule_class_load(Class cls) {

  if (!cls) return;
  // _read_images should realize,_read_images是否完成
  assert(cls->isRealized());  
  // 已经添加完成,则return
  if (cls->data()->flags & RW_LOADED) return;

  // Ensure superclass-first ordering,确保superClass添加
  schedule_class_load(cls->superclass);
  // 添加imp、class到调用中
  add_class_to_loadable_list(cls);  
  // 设置class的标识符,表示已添加到list中
  cls->setInfo(RW_LOADED); 
}
    
// 在obj-loadmethod.mm中
void add_category_to_loadable_list(Category cat) {
  IMP method;
  // 获取Category中Load方法的imp
  loadMethodLock.assertLocked();
  method = _category_getLoadMethod(cat);
  // Don't bother if cat has no +load method,没有则return
  if (!method) return;

  if (PrintLoading) {
    _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                 _category_getClassName(cat), _category_getName(cat));
  }
  // 如果已使用大小等于数组大小,对数组进行动态扩容
    if (loadable_categories_used == loadable_categories_allocated) {
      loadable_categories_allocated = loadable_categories_allocated*2 + 16;
      loadable_categories = (struct loadable_category *)
        realloc(loadable_categories,loadable_categories_allocated * sizeof(struct loadable_category));
    }

  loadable_categories[loadable_categories_used].cat = cat;
  loadable_categories[loadable_categories_used].method = method;
  loadable_categories_used++;
}
     
// 准备好了loadable_calss、loadable_categories数组,load_images会通过call_load_methods函数执行这些load方法
     
void call_load_methods(void) {
  static bool loading = NO;
  bool more_categories;

  loadMethodLock.assertLocked();

  // Re-entrant calls do nothing; the outermost call will finish the job.防止重新进入
  if (loading) return;
  loading = YES;

  void *pool = objc_autoreleasePoolPush();

  do {
    // 1. Repeatedly call class +loads until there aren't any more
    while (loadable_classes_used > 0) {
      call_class_loads();
    }

    // 2. Call category +loads ONCE
    more_categories = call_category_loads();

    // 3. Run more +loads if there are classes OR more untried categories
  } while (loadable_classes_used > 0  ||  more_categories);
  objc_autoreleasePoolPop(pool);
  loading = NO;
}
     
// 通过遍历获得loadable_class结构体
static void call_class_loads(void) {
  int i;

  // Detach current loadable list.
  struct loadable_class *classes = loadable_classes;
  int used = loadable_classes_used;
  loadable_classes = nil;
  loadable_classes_allocated = 0;
  loadable_classes_used = 0;

  // Call all +loads for the detached list.
  for (i = 0; i < used; i++) {
    Class cls = classes[i].cls;
    load_method_t load_method = (load_method_t)classes[i].method;
    if (!cls) continue; 

    if (PrintLoading) {
      _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
    }
    (*load_method)(cls, SEL_load);
  }
  // Destroy the detached list.
  if (classes) free(classes);
}
     
     
struct loadable_class {
  Class cls; // may be nil 
  IMP method;
};

prepare_load_methods 函数执行完之后,所有满足 +load 方法调用条件的类和分类就被分别保存在全局变量 loadable_classesloadable_categories 中了。

准备好类和分类之后,接下来就是对他们的 +load 方法进行调用了,找到 call_load_methods 方法:

void call_load_methods(void)
{
		......
    // 加载所有类和分类的+load
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);
		......
}

在这个方法里,会以类优先于分类的顺序调用 +load 方法

这里有两个关键的函数 call_class_loads()call_category_loads

这两个函数会遍历上一步中准备好的 loadable_classesloadable_categories+load 方法,需要注意的是他们都是以函数内存地址的方式 ((*load_method)(cls, SEL_load))+load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。

通过上述代码,发现load方法的调用顺序为 父类 -> 子类 -> Category。

这样的调用方式就使得 +load 方法拥有了一个特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用,多个分类实现了 +load 方法时,每个分类的 +load 方法都会被调用。

@interface Father : NSObject
@end

@implementation Father
+ (void)load {
    NSLog(@"father");
}
@end
---------------------------------------------------
@interface Son : Father
@end

@implementation Son
+ (void)load {
    NSLog(@"son");
}
@end
---------------------------------------------------
@interface Son (load)
@end

@implementation Son (load)
+ (void)load {
    NSLog(@"son_category");
}
@end

运行以后,控制台输出:

father
son
son_category
doInitialization

最终迭代执行了 ImageLoaderMachOdoInitialization() 方法,doInitialization() 内部首先调用 doImageInit 来执行镜像的初始化函数,也就是 LC_ROUTINES_COMMAND 中记录的函数,然后再执行 doModInitFunctions() 方法来解析并执行 _DATA_,__mod_init_func 这个section中保存的函数,_mod_init_funcs 中保存的是全局C++对象的构造函数以及所有带 __attribute__((constructor) 的C函数。

bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
	CRSetCrashLogMessage2(this->getPath());

	// mach-o has -init and static initializers
	doImageInit(context);
	doModInitFunctions(context);
	
	CRSetCrashLogMessage2(NULL);
	
	return (fHasDashInit || fHasInitializers);
}

第九步 查找入口点并返回

这一步调用主程序镜像的 getThreadPC(),从加载命令读取 LC_MAIN 入口,如果没有 LC_MAIN 就调用 getMain() 读取 LC_UNIXTHREAD,查找到 main() 函数之后返回给 __dyld_start 调用。 至此,启动流程结束,进入主程序的 main() 函数

dyld3 优化

WWDC2017推出了dyld3,应用在系统的app中。在iOS13开放给三方的app,极大优化了启动速度。

dyld3

dyld2是纯碎的 in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。

dyld2主要工作流程

  • dyld的初始化,主要代码在 dyldbootstrap::start ,接着执行 dyld::_maindyld::_main 代码较多,是dyld加载的核心部分;
  • 检查并准备环境,比如获取二进制路径,检查环境变量,解析主二进制的 image header 等信息;
  • 实例化主二进制的 image loader,校验主二进制和dyld的版本是否匹配;
  • 检查 shared cache 是否已经map,没有的话则先执行 map shared cache 操作;
  • 检查 DYLD_INSERT_LIBRARIES,有的话则加载插入的动态库(实例化 image loader);
  • 执行link操作。这个过程比较复杂,会先递归加载依赖的所有动态库(会对依赖库进行排序,被依赖的总是在前面),同时在这阶段将执行符号绑定,以及 rebase,binding 操作;
  • 执行初始化方法。OC的 +load 以及C的 constructor 方法都会在这个阶段执行;
  • 读取Mach-O的LC_MAIN段获取程序的入口地址,调用 main() 方法。

dyld3 优化思路

针对于dyld2的流程,dyld3优化有以下两点思路:

  • 识别安全性敏感的组件:解析 mach-o 文件并寻找依赖是安全性敏感的,因为恶意篡改的 mach-o 头部可以进行某些攻击,如果一个 app 使用了 @rpath,那么恶意修改路径或者将一些库插入到特定的地方,攻击者就可以毁坏 app。所以这部分工作需要被搬到进程外来完成,比如搬到一个 daemon 进程中。
  • 识别可以被缓存的部分:符号查找就是其中一个,因为在一个特定的库中,除非软件更新或者这个库被改变,不然每个符号都应该有固定的偏移量。

在 dyld 3.0 中,mach-o 头部解析和符号查找工作完成后,这些执行结果会被作为启动闭包(launch closure)写入硬盘。

启动闭包(launch closure):这是一个新引入的概念,指的是 app 在启动期间所需要的所有信息。比如这个 app 使用了哪些动态链接库,其中各个符号的偏移量,代码签名在哪里等等。启动闭包比mach-o更简单。它们是内存映射文件,不需要用复杂的方法进行分析。

dyld3是部分 out-of-process,部分 in-process。上图中,虚线之上的部分是 out-of-process 的,在App下载安装和版本更新的时候会去执行。

dyld 3包含三个组件

1. 进程外的Mach-O分析器/编译器;

在dyld 2的加载流程中,Parse mach-o headers 和 Find Dependencies 存在安全风险(可以通过修改 mach-o header 及添加非法 @rpath 进行攻击),而 Perform symbol lookups 会耗费较多的CPU时间,因为一个库文件不变时,符号将始终位于库中相同的偏移位置,这两部分在dyld 3中将采用提前写入把结果数据缓存成文件的方式构成一个 lauch closure(可以理解为缓存文件)。

  • 处理了所有可能影响启动速度的 search path,@rpaths 和环境变量
  • 解析 mach-o 二进制文件,分析其依赖的动态库
  • 执行所有符号查找的工作
  • 最后它将这些工作的结果创建成了启动闭包,写入缓存,这样,在应用启动的时候,就可以直接从缓存中读取数据,提高启动速度。
  • 这是一个普通的 daemon 进程,可以使用通常的测试架构。

out-of-process 是一个普通的后台守护程序,因为从各个APP进程抽离出来了,可以提高dyld3的可测试性。

2. 进程内的执行 launch closures 的解析引擎

进程内验证 launch closure 是否正确,映射所有的动态库,执行初始化并跳转到 main() 函数。

不再需要解析 mach-o 头文件和查找符号,这是耗时的大头,优化了这一部分,所以带来了更快的启动。

3. launch closure 的缓存服务

系统的app的launch closures直接构建到共享的缓存内,可以运行和分析系统中每一个mach-o文件

对于第三方的app,将在程序安装期间构建 launch closures,在系统更新期间重新构建。因为那时候的系统库已经发生更改。这样就能保证 lauch closure 总是在APP打开之前准备好。启动闭包会被写到到一个文件里,下次启动则直接读取和验证这个文件。

在 iOS,tvOS,watchOS 中,一切(生成启动闭包)都是在 app 启动之前做完的。在 macOS 上,由于有 sideload app,进程内引擎会在首次启动时启动一个 daemon,之后就可以使用启动闭包了。总之大部分情景下,这些工作都在 app 启动之前完成了。

大部分的启动场景都不需要调用这个进程外的 mach-o 解析器。而启动闭包又比 Mach-O 简单很多,因为它是一个内存映射文件,解析和验证都非常简单,并且经过了良好的性能优化。所以 dyld 3.0 的引入,能让 app 的启动速度得到明显提升。