App启动流程之 dyld 探析

1,537 阅读16分钟

1、App 启动指的是什么?

从用户点击 App 开始到用户看到第一个界面,这称为一次 App 启动。

一般情况下,App 的启动分为冷启动和热启动。

  • 冷启动是指, App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。

  • 热启动是指 ,App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少。

今天这篇文章,我们就探讨 App 的冷启动。

2、App 的拆包

在没有接触到 dyld 的时候,认为 App 启动就是把 Xcode 最后打包的 .ipa 包交给系统执行,系统自己调用代码启动 App。之后为了研究 App 是如何启动的,把 .ipa 拆包了(随意找了微信的包),如下图:

拆包之后,发现就是一些资源(包括:图片,json文件,html本地文件等)和一个显示 exec 的黑色文件,这个黑色文件就是代码打包后的东西,也就是 Mach-O 文件。

3、Mach-O

什么是Mach - O

维基百科的解释:Mach-OMach Object 文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。Mach-O 文件里面的内容,主要就是代码和数据:代码是函数的定义;数据是全局变量的定义,包括全局变量的初始值。

其实 .ipa 包里显示 exec 的黑色文件就是 Xcode 打包代码后形成的可执行的二进制文件。

Mach - O 是怎么被生成的

因为本文主要探索 App 的启动流程,Mach - O 的生成过程简单提一下。

开发者使用 Xcode 进行打包代码后形成 Mach - O 文件,其实打包的过程是使用 LLVM 编译器编译的。编译器会对每个工程文件进行编译,生成多个 Mach-O(可执行文件),之后由链接器会将项目中的多个 Mach-O 文件合并成一个。

戴铭大佬的这篇文章 中解释了编译的几个主要过程:

  • 首先,你写好代码后,LLVM 会预处理你的代码,比如把宏嵌入到对应的位置。

  • 预处理完后,LLVM 会对代码进行词法分析和语法分析,生成 AST 。AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查,同时还能更快地生成 IR(中间表示)。

  • 最后 AST 会生成 IR,IR 是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。

下图展示了编译的主要过程。

Mach - O 文件是什么样的

找到一个 CoreFoundationMach - O ,使用 MachOView 打开如下:

能看到使用 Xcode 写的代码经过 LLVM 编译后变成了一个个数据段,分类别存储代码中的 类、方法、字符串 等,上图展示的是被引用类的指针地址和名称。

PS:扩展一下,有指针地址说明,类是在编译时生成的。类的实际地址为:ASLR + Offset。

4、App 启动流程

了解了 Mach - O 文件的概念,现在我们对 App 启动流程进行探索。

从调用栈中查看 dyld 的加载流程

既然是 App 启动,那么程序到 main.m 文件的 main 函数,肯定是启动完成了。

尝试1:把断点打在 main.mmain 函数

可惜什么也看不到,只是知道了在 libdyld.dylib 调用了 start 函数。

尝试2:把断点打在 objc-runtime-new.m 的随意一个函数里

打开上帝视角,其实应该断点到 +(void)load {} 方法里面去,分析完 dyld 就明白了

看见这个调用栈信息后,发现似乎是找对了,因为方法前缀都有 dyld 的前缀(这个是 C++ 的命名空间),有了眉目,那就从 __dyld_start 开始吧。

__dyld_start(dyld的入口)

点击这个调用栈,发现是一段汇编:

汇编中 call xxx 就会调用一个方法,能看到汇编后面的方法说明,其实是调用了 dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) 方法。

dyldbootstrap:: 为命名空间,commend + shift + o 搜索 dyldbootstrap ,看到上方注释 Code to bootstrap dyld into a runnable state.(引导dyld进入可运行状态的代码),随后在 dyldbootstrap 命名空间下方找到了 start 方法。

到此 App 启动第一步开始。

dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*)

start 源码如下:

//
//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  这是引导dyld的代码。这项工作通常由dyld和crt为程序完成。
//  In dyld we have to do this manually.
//  在dyld中,我们必须手动执行此操作。
//
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
				const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

    // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    // 释放kdebug跟踪点,指示dyld引导已经启动
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    
    // 如果内核必须滑动dyld,我们需要修复加载敏感的位置,我们必须在使用任何全局变量之前完成此操作
    rebaseDyld(dyldsMachHeader);

	// kernel sets up env pointer to be just past end of agv array
    // 内核将env指针设置为agv数组的刚刚结束
	const char** envp = &argv[argc+1];
	
	// kernel sets up apple pointer to be just past end of envp array
    // 内核将苹果指针设置为刚好超过envp数组的末尾
	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(argc, argv, envp, apple);
#endif

	_subsystem_init(apple);

	// now that we are done bootstrapping dyld, call dyld's main
    // 现在我们已经完成了对dyld的引导,调用dyld的main
	uintptr_t appsSlide = appsMachHeader->getSlide();
	return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

上方代码是对 dyld 的引导操作, 完成引导后调用了 dyld::_main 函数返回结果,并且也能从调用栈看到 dyld::_main 方法的调用。

start 函数中调用了 rebaseDyld(dyldsMachHeader); 这个函数就是在引导 dyld 时候确定 ASLR

ASLR :内核产生的随意性偏移地址,用来做地址定向攻击的防护。

dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*)

dyldbootstrap::start 函数的结尾能看到,调用了 _main 函数后就返回了,所以 dyld 的加载必定是在 dyld::_main 中。

_main 函数上方的注释,苹果也对 start 方法进行了注释, Returns address of main() in target program which __dyld_start jumps to.,意思是返回 __dyld_start 跳转到的目标程序中的 main() 地址。

有了这个前提,再看 _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
// 返回 __dyld_start 跳转到的目标程序中的 main() 地址
uintptr_t
_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;
	}

	// 获取主程序的架构
	getHostInfo(mainExecutableMH, mainExecutableSlide);
    
	// 保存主程序 Mach-O 的头
	sMainExecutableMachHeader = mainExecutableMH;
    
   	// 保存主程序的 ASLR
	sMainExecutableSlide = mainExecutableSlide;
    
   	// 设置上下文信息
	setContext(mainExecutableMH, argc, argv, envp, apple);
    
   	// 根据环境变量判断当前进程是否受限
   	configureProcessRestrictions(mainExecutableMH, envp);
    
   	// 检测进程环境变量
	checkEnvironmentVariables(envp);
	defaultUninitializedFallbackPaths(envp);
    
   	// 配置环境变量后就可以打印
	if ( sEnv.DYLD_PRINT_OPTS )
    	//配置 DYLD_PRINT_OPTS 后可以打印 Mach-O 文件的地址
		printOptions(argv);
	if ( sEnv.DYLD_PRINT_ENV ) 
		printEnvironmentVariables(envp);
    
   	// 开始加载共享缓存
	
	// 检查共享缓存库是否禁用,iOS不能禁用
	checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    
   	// 加载共享缓存(UIKIT,Fundation)
	mapSharedCache(mainExecutableSlide);
    
	// instantiate ImageLoader for main executable
	// 为主要可执行文件实例化ImageLoader
		
	// 实例化主程序,从 Mach - O 头部 读取CPU架构等,判断兼容性
	sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
	gLinkContext.mainExecutable = sMainExecutable;
	gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);、
    
   	// load any inserted libraries
	// 加载插入的动态库
		
	// 根据 DYLD_INSERT_LIBRARIES 的值是否允许加载插入动态库 
   	// (越狱的环境都是用这个,下载插件影响别人的程序)
	if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
		for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
			loadInsertedDylib(*lib); // 循环加载
	}
    
	// 开始链接主程序
	// link main executable
	gLinkContext.linkingMainExecutable = true;
    
	// 链接动态库和第三方库,进行符号绑定
	link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
    
   	// 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();
		}
	}
    
	// do weak binding only after all inserted images linked
		
	// weak符号绑定,weakbind是比其他后绑定的意思,其他的绑定是在上方 link 完成时就绑定好了
	sMainExecutable->weakBind(gLinkContext);
    
	// 链接操作结束
	gLinkContext.linkingMainExecutable = false;
    
	// run all initializers
	// 开始运行主程序了(load_images ,class_load)类的load方法就是这里调用的
	initializeMainExecutable(); 
    
   	// notify any montoring proccesses that this process is about to enter main()
   	// 通知任何监视进程该进程即将进入main()
	notifyMonitoringDyldMain();
    
	// find entry point for main executable
   	// 查找主程序的入口点
	result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
    
   	// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
   	// 调用 main()
	result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
    
	//...
    
	return result;
}

看到了上方主要代码流程后,这里需要提几个重要参数 :

  • macho_header* mainExecutableMH:mach-O 文件的 header ,每一个 mach-O 文件都会有,表示了 CPU 架构等一些运行环境的信息;

  • uintptr_t mainExecutableSlide:滑块地址,其实就是 ASLR 的起始地址;

  • sEnv.DYLD_PRINT_OPTS:这个参数配置后,控制台就能打印 Mach-O 的文件地址;

  • sEnv.DYLD_INSERT_LIBRARIES:这个参数正向开发基本上用不到,但是如果要做逆向防护的时候,这个就是重要的参数,根据 DYLD_INSERT_LIBRARIES 的值, dyld 判断是否允许加载插入动态库,玩过越狱的朋友就知道,下载了一个插件所有的 App 都能使用,其实不是改变了 App 的东西,而是系统加载了插入库。

对于主线流程有了概念,接下来分析主线流程中一些重要代码。

checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)

checkSharedRegionDisable :判断共享缓存是否能禁用。

这个方法不多说,本文主要探索 iOS 系统下 App 的启动流程,所以这里就只看一句话。

static void checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)
{
#if TARGET_OS_OSX
	// ...
#endif
	// iOS cannot run without shared region
}

去掉 OSX方法,就只剩一个注释,这里就说明了,为什么 iOS 不能禁止共享缓存库了。

instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)

// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.

// 在dyld获得控制之前,内核在main可执行文件中映射。我们需要为主可执行文件中已经映射的对象创建一个ImageLoader*。

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);
		//添加到 AllImages 环境变量中去
		addImage(image);
		return (ImageLoaderMachO*)image;
//	}
	
//	throw "main executable not a known format";
}

isCompatibleMachO 以前判断了 CPU 架构是否兼容,现在去掉了。

创建是由 ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext); 这行代码去创建的,这里需要往下看一下,有一些有趣的参数需要了解一下。

instantiateMainExecutable

instantiateMainExecutable 源码:

// create image for main executable
// 为主要可执行文件创建映像
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
	// ...
    
	//通过这个方法去实例化
	sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
    
	// ...
}
sniffLoadCommands

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)
{
	//dlyd_info_only
	*compressed = false;
	
	//有最大限度
	
	//最大 255
	*segCount = 0;
	
	//LC_LOAD_DYLD_LIB 最大4095个
	*libCount = 0;
	
	//代码签名
	*codeSigCmd = NULL;
	
	//加密信息,应用上传加壳
	*encryptCmd = 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;

	// dylibs that use LC_DYLD_CHAINED_FIXUPS have that load command removed when put in the dyld cache
	if ( !*compressed && (mh->flags & MH_DYLIB_IN_CACHE) )
		*compressed = true; 
}
  • *compressed:从这里的代码能看到 *compressed 是标记 LC_DYLD_INFO/LC_DYLD_INFO_ONLY 中有没有取到 cmdsize ,确定 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;
			*compressed = true;
			break;
            
	// ...
}

MachOView 的图如下:

  • *segCountLC_SEGMENT (或 LC_SEGMENT_64)命令是最主要的加载命令,这条命令知道内核如何设置新运行的进程的内存空间,这些命令条数最大 255 个。

  • *libCount :动态库+三方库的总和,这些库最大允许加载 4095 个。因为是一个空工程,只有系统动态库,没有第三方库。

  • codeSigCmd :代码签名;

  • encryptCmd :应用的加密信息,用于应用加壳。

到这里主程序 初始化 完成。

ImageLoader::link 链接器

主程序初始化完成之后,现在就开始 链接库符号绑定 了。

戴铭大佬对于动态库链接的说明如下:

Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

使用 dyld 加载动态库,有两种方式:有程序启动加载时绑定和符号第一次被用到时绑定。为了减少启动时间,大部分动态库使用的都是符号第一次被用到时再绑定的方式。

加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 Clang Attribute 的 constructor 修饰函数。

每个函数、全局变量和类都是通过符号的形式定义和使用的,当把目标文件链接成一个 Mach-O 文件时,链接器在目标文件和动态库之间对符号做解析处理。

源码如下:

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{

	// ...
	uint64_t t0 = mach_absolute_time();

	// ...
    
	//根据LC_LOAD_DYLIB 告诉主程序动态库的里面的方法的实际地址
	this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
	context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
    
	// 进行符号绑定,
	this->recursiveRebaseWithAccounting(context);
	
	// ...
}
  • recursiveLoadLibraries(undefined) external _NSLog (from Foundation) 上方解释说明到,动态库编译的时候并没有符号地址,只有链接库的目录,所以,dyld 在 link 的时候需要把被 LLVM 标识为 undefined 的这种未实现、非私有的符号通过动态库解析成真正的指针地址。

  • recursiveRebaseWithAccounting : 把解析后的指针地址和 undefined 逐个对应。

initializeMainExecutable

主程序实例化好了,动态库链接完成了,符号地址绑定了,现在就开始运行主程序。

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]);
	
	// ...
}
ImageLoader::runInitializers

initializeMainExecutable() 的核心函数调用了 processInitializers(),基本上没有处理其他东西,接着往下看。

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
	// ...
    
	processInitializers(context, thisThread, timingInfo, up);
	
	// ..
}
ImageLoader::processInitializers

images 列表中的所有图像调用递归 init ,构建一个新的未初始化的向上依赖项列表。

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
									 InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
	// Calling recursive init on all images in images list, building a new list of
	// uninitialized upward dependencies.
	 // 对 images 列表中的所有图像调用递归 init ,构建一个新的未初始化的向上依赖项列表。
	for (uintptr_t i=0; i < images.count; ++i) {
		images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
	}

}
ImageLoader::recursiveInitialization

这里就开始实例化 images 了, 通过 notifySingle 回调。

这里主要处理以下 2 个事情:

  • notifySingle : 调用 runtime 中注册的通知
  • doInitialization :实例化这些 image
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.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
						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 知道我们将要初始化这个图
			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);

			//...
            
		}
		catch (const char* msg) {
			// this image is not initialized
			// ...
		}
	}
	
	recursiveSpinUnLock();
}
notifySingle

这个方法就是 C++ 方法回调,通过调用栈知道是由 _objc_init() 函数下 _dyld_objc_notify_register 注册通知后,(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());回调的。

看到这里后,就明白了 dyldruntime 是怎么完美的链接起来的。

  • sNotifyObjCMapped :调用 runtime 方法 map_images,处理由 dyld 映射的 image,计算class数量,根据总数调整各种表的大小;
  • sNotifyObjCInit :调用 runtime 方法 load_images ,对类的初始化。

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
	
	// ...
    
	if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
		// ...
        
		// 调用 runtime 方法
		(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		
		// ...
	}

	// ...
}
doInitialization

doInitialization 做一些初始化。

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

	// mach-o has -init and static initializers
	doImageInit(context);
	
	// 调用 全局 C++ 方法
	doModInitFunctions(context);
	
	CRSetCrashLogMessage2(NULL);
	
	return (fHasDashInit || fHasInitializers);
}

两个主要方法如下:

  • doImageInit :逐个初始化 image

  • doModInitFunctions : 调用全局 C++ 方法。这个方法目前正向开发不怎么用,但是到逆向上,一些代码的注入,都会在这里做。因为这个方法是在调用 main() 之前,95% 配置结束之后执行的。

doModInitFunctions

调用了 doModInitFunctions 后,发现到 _objc_init 时,调用栈中还有 libSystem_initializerlibdispatch_init_os_object_init 没有看到在哪调用的,

继续看 doModInitFunctions 函数,发现在当前函数的最后调用了 libSystemInitialized

bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
{
	dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
	func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}

bool haveLibSystemHelpersAfter = (dyld::gLibSystemHelpers != NULL);
if ( !haveLibSystemHelpersBefore && haveLibSystemHelpersAfter ) {
	// now safe to use malloc() and other calls in libSystem.dylib
	dyld::gProcessInfo->libSystemInitialized = true;
}
  • libSystem_initializer :在 libSystem 库中;
  • libdispatch_init :在 libdispatch 库中;
  • _os_object_init :在 libdispatch 库中。
libSystem_initializer

libSystem 库中找到了 libSystem_initializer ,发现 libdispatch_init() 的调用。

static void
libSystem_initializer(int argc,
		      const char* argv[],
		      const char* envp[],
		      const char* apple[],
		      const struct ProgramVars* vars)
{
	// ...

	_dyld_initializer();
	_libSystem_ktrace_init_func(DYLD);

	libdispatch_init();
	
	// ...
	errno = 0;
}
libdispatch_init()

看到 libdispatch_init() 调用了 _os_object_init(),继续追!!

void
libdispatch_init(void)
{
	// ...
    
	_os_object_init();
    
	// ...
}
_os_object_init()

这里,终于找到了 _objc_init 的调用方法,完美的和调用栈匹配。

void
_os_object_init(void)
{
	// ...
    
	_objc_init();
	
	// ...
}

_objc_init()

这个方法会在下一篇 类的加载 说,这里就不细说了,在这里就查找一下 + (void)load {} 方法的调用。

load_images 的源码:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // ...

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

从上述 runtime 源码中,看到调用了 call_load_methods(),源码如下:

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, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

看完这段代码, (*load_method)(cls, @selector(load)) 就是对各个类 load 方法的调用。

如果配置了 PrintLoading 环境变量,那么就能打印所有使用 load 方法的类了。优化启动时间就会很方便。

(uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD()

接下来就调用 main() 函数了。

5、总结

上述就是 App 启动的时候 dyld 工作的流程,也能看到 dyld 和 Object-C runtime 链接的方式。

下图是 dyld 的加载流程总结图:

到这里,App启动流程之 dyld 探析就结束了,感谢阅读,如果对你有帮助,希望给个赞,如果有错误的地方,希望大佬指出,共同学习,一起进步,谢谢。