iOS 应用程序加载

3,968 阅读25分钟

1. 理论基础速成

1.1 静态库与动态库

库是已写好的、供使用的 可复用代码,每个程序都要依赖很多基础的底层库。

从本质上,库是一种可执行代码的二进制形式。可以被操作系统载入内存执行。库分为两种:静态库(.a .lib)和 动态库 (framework .so .dll)。

所谓的静态、动态指的是 链接的过程

将一个程序编译成可执行程序的步骤如下:

1.1.1 静态库

之所以称之为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o 与 引用的库一起链接到可执行文件中。对应的链接方式称为 静态链接

如果多个进程需要引用到【静态库】,在内存中就会存在多份拷贝,如上图中进程1 用到了静态库1、5,进程2也用到了静态库1、5,那么静态库1、5在编译期就分别被链接到了进程1和进程2中,假设静态库1占用2M内存,如果有20个这样的进程需要用到静态库1,将占用40M的空间。

【静态库】的特点如下:

  • 静态库对函数库的链接是在编译期完成的。执行期间代码装载速度快。
  • 使可执行文件变大,浪费空间和资源(占空间)。
  • 对程序的更新、部署与发布不方便,需要全量更新。如果 某一个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。

1.1.2 动态库

【动态库】在程序编译时并不会链接到目标代码中,而是在运行时才被载入。不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。

【动态库】在内存中只存在一份拷贝,如果某一进程需要用到动态库,只需在运行时动态载入即可。

【动态库】的特点:

  • 动态库把对一些库函数的链接载入推迟到程序运行时期(占时间)。
  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)
  • 将一些程序升级变得简单,不需要重新编译,属于增量更新。  

1.2 Mach-O

程序想要运行起来,它的可执行文件格式就要被操作系统所理解,比如 ELF(Executable and Linking Format) 是 Linux 下可执行文件的格式,PE32/PE32+(Portable Executable) 是 windows 的可执行文件的格式,那么对于 OS XiOS 来说 Mach-O 是其可执行文件的格式。

Mach-O】 为 Mach Object 文件格式的缩写,是 iOS 系统不同运行时期 可执行文件 的文件类型统称。它是一种用于 可执行文件、目标代码、动态库、内核转储的文件格式。

Mach-O】 的三种文件类型:Executable、Dylib、Bundle

  • Executable

Executableapp 的二进制主文件,我们可以在 Xcode 项目中的 products 文件中找到它:

  • Dylib

Dylib 是动态库,动态库分为 动态链接库动态加载库

动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。【随着程序启动而启动】
动态加载库:当需要的时候再使用 dlopen 等通过代码或者命令的方式加载。【程序启动之后】

  • Bundle

Bundle 是一种特殊类型的Dylib,你无法对其进行链接。所能做的是在Runtime运行时通过dlopen来加载它,它可以在macOS 上用于插件。

  • Image 和 Framework

Image (镜像文件)包含了上述的三种类型;
Framework 可以理解为动态库。

1.2.1 Mach-O的结构

【Mach-O】是一个以数据块分组的二进制字节流,每个【Mach-O】文件包括一个Mach-O头,然后是一系列的载入命令,再是一个或多个段,每个段包括0到255个块。

  • Header结构

保存【Mach-O】的一些基本信息,包括运行平台、文件类型、LoadCommands指令的个数、指令总大小,dyld标记Flags等等。

  • Load Commands

紧跟Header,这些加载指令清晰地告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的。加载【Mach-O】文件时会使用这部分数据确定内存分布以及相关的加载命令,对系统内核加载器和动态连接器起指导作用。比如我们的main()函数的加载地址、程序所需的dyld的文件路径、以及相关依赖库的文件路径。

  • Data

每个segment的具体数据保存在这里,包含具体的代码、数据等等。

1.2.1.1 segment段:

【Mach-O】 镜像文件 是由 segments 段组成的。

  • 段的名称为大写格式

所有的段都是 page size 的倍数。

  • 在arm64上为 16kB
  • 其它架构为 4KB

这里在普及一下 虚拟内存内存页 的知识:

具有 VM 机制的操作系统,会对每个运行的进程创建一个逻辑地址空间 logical address space 或者叫 虚拟地址空间 virtual address space;该空间的大小由操作系统位数决定。

虚拟地址空间 会被分为相同大小的块,这些块被称为内存页(page)。计算机处理器和它的内存管理单元(MMU - memory management unit)维护着一张将程序的 虚拟地址空间 映射到 物理地址 上的分页表 page table

macOS 和早版本的 iOS 中,分页大小为 4kb。在之后的基于A7A8 的系统中,虚拟内存(64位的地址空间)地址空间的分页大小变为了16kb,而物理RAM上的内存分页大小仍然维持在 4kb;基于 A9 及以后的系统,虚拟内存和物理内存的分页都是16kb

1.2.1.2 section

segment 段内部还有许多的 section 区。section 名称为小写格式。 section 节 实际上只是一个 segment 段的子范围,它们没有页面大小的任何限制,但是它们是不重叠的。

1.2.1.3 常见的segments
  • __TEXT:代码段,包含头文件、代码和只读常量只读不可修改

  • __DATA:数据段,包含全局变量,静态变量等。可读可写

  • __LINKEDIT:如何加载程序,包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。
1.2.2 Mach-O Universal Files

【Mach-O】 通用文件,将多种架构的 Mach-O 文件合并而成。它通过 header 来记录不同架构在文件中的偏移量,segment 占多个分页,header 占一页的空间。header 单独占一页 有利于 虚拟内存 的实现。

1.3 虚拟内存

虚拟内存是一层 间接寻址

【虚拟内存】是在物理内存上建立的一个逻辑地址空间。建立在进程物理内存之间的中间层,它向上(应用)提供了一个连续的逻辑地址空间,向下隐藏了物理内存的细节。

虚拟内存被划分为一个个大小相同的Page(64位系统上是16KB),提高管理和读写的效率。 Page又分为只读读写的Page。

虚拟内存解决的是管理所有进程使用 物理RAM 的问题。通过添加间接层来让每个进程使用 逻辑地址空间,它可以映射到RAM 上的某个物理页上。这种映射 不是一对一 的,逻辑地址可能映射不到 RAM 上,也有可能有多个逻辑地址映射到同一个物理RAM 上。

虚拟内存使得逻辑地址可以没有实际的物理地址,也可以让多个逻辑地址对应到一个物理地址。

  • 针对第一种情况(逻辑地址可能映射不到 RAM ):在应用执行的时候,它被分配的逻辑地址空间都是可以访问的,当应用访问一个逻辑Page,而在对应的物理内存中并不存在的时候,这时候就发生了一次Page fault。当Page fault发生的时候,会中断当前的程序,在物理内存中寻找一个可用的Page,然后从磁盘中读取数据到物理内存,接着继续执行当前程序。
  • 而第二种情况(多个逻辑地址映射到同一个物理RAM 上)就是多进程共享内存

对于文件可以不用一次性读入整个文件,可以使用分页映射 mmap()的方式获取。也就是把文件 某个片段 映射到进程逻辑内存的 某个页 上。当某个想要读取的页没有在内存中,就会触发 page fault,内核只会读入那一页,实现文件的 懒加载。也就是说 【Mach-O】 文件中的 __TEXT 段可以映射到多个进程,并可以懒加载,且进程之间 共享内存

__DATA 段是可读写的。这里使用到了Copy-On-Write 技术,简称【COW】。 也就是多个进程共享一页内存空间时,一旦有进程要做写操作,它会先将这页内存内容复制一份出来,然后重新映射逻辑地址到新的RAM 页上。也就是这个进程自己拥有了那页内存的拷贝。这就涉及到了 clean/dirty page 的概念。dirty page 含有进程自己的信息,而clean page 可以被内核重新生成(重新读磁盘)。多以 dirty page 的代价大于 clean page

1.4 多进程加载Mach-O 镜像

  • 所以在多个进程加载【Mach-O】镜像时,__TEXT__LINKEDIT 因为是只读的,都是可以共享内存的,读取速度就会很快。
  • __DATA 因为是可读写的,就有可能产生dirty page,如果检测有 clean page 就可以直接使用,反之就需要重新读取 DATA page。一旦产生了 dirty page,当dyld执行结束后,__LINKEDIT 需要通知内核当前页面不再需要了,当别人需要使用的时候就可以重新 clean 这些页面。

1.5 ASLR

有两种主要的技术来保证应用的安全:ASLRCode Sign

【ASLR】的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”App被启动的时候,程序会被映射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而【ASLR】技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。

1.6 Code Signing

【Code Sign】相信大多数开发者都知晓,这里要提一点的是,为了在运行时 验证【Mach-O】 文件的签名,在进行【Code Sign】的时候,加密哈希不是针对于整个文件,而是针对于每一个Page的。并存储在 __LINKEDIT 中。这就保证了在dyld进行加载的时候,可以对每一个page进行独立的验证

1.7 exec()

exec() 是一个系统调用。系统内核把应用程序映射到新的地址空间,且每次起始位置都是随机的(因为ASLR)。并将起始位置到0x000000 这段范围的进程权限都标记为不可读写不可执行。如果是32 位进程,这个范围至少是4kb ;如果是64位进程则至少是4GBNULL指针引用和指针截断误差都是会被它捕获,这个范围也叫做 PAGEZERO

1.8 dyld

内核完成映射进程的工作后,会将名字为 dyldMach-O 文件映射到进程中的随机地址,它将PC 寄存器设为 dyld 的地址并运行。dyld 在应用进程中运行的工作是加载应用依赖的所有动态链接库,准备好运行所需的一切,它拥有的权限跟应用程序一样。

dyld(the dynamic link editor),【动态链接器】是苹果操作系统一个重要部分,在 iOS / macOS 系统中,仅有很少的进程只需内核就可以完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容填充,这个填充的工作就是由 dyld 来完成的。

【动态链接加载器】在系统中以一个用户态的可执行文件形式存在,一般应用程序会在Mach-O文件部分指定一个 LC_LOAD_DYLINKER 的加载命令,此加载命令指定了dyld的路径,通常它的默认值是“/usr/lib/dyld”。系统内核在加载Mach-O文件时,会使用该路径指定的程序作为动态库的加载器来加载dylib

1.9 共享缓存

dyld加载时,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被dyld映射到内存中,之后,当任何Mach-O镜像加载时,dyld首先会检查该Mach-O镜像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的情况下,这种做法对程序启动性能是有明显提升的。

1.10 dyld 流程

  • Load dylibs

从主执行文件header获取到需要加载的所依赖的动态库列表,而header早就被内核映射过。然后它需要找到每个dylib,然后打开文件,读取文件起始位置,确保它是Mach-O文件。接着会找到代码签名并将其注册到内核。然后在dylib文件的每个segment 上调用 mmap()。应用所依赖的dylib文件可能会再依赖其他dylib,所以dyld所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载100到400 个dylib文件,但大部分都是系统的dylib,它们会被预先计算和缓存起来,加载速度很快。

  • Fix-ups

在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个dylib 调用另一个 dylib,这是就需要很多间接层。

Mach-O中有很多符号,有指向当前 Mach-O 的,也有指向其他 dylib 的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?

Mach-O中采用了PIC技术,全称是Position Independ code。意味着代码可以被加载到间接的地址上。当你的程序要调用printf的时候,会先在 __DATA 段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分:rebasingbinding

  • Rebasing 和 Binding

Rebasing:在镜像内部调整指针的指向。 Binding: 将指针指向镜像外部的内容。

之所以需要Rebase,是因为刚刚提到的 ASLR 使得地址随机化,导致起始地址不固定,另外由于 Code Sign,导致不能直接修改 ImageRebase的时候只需要增加对应的偏移量即可。(待Rebase的数据都存放在 __LINKEDIT中,可以通过MachOView查看:Dynamic Loader Info -> Rebase Info)

Binding就是将这个二进制调用的外部符号进行绑定的过程。 比如我们objc代码中需要使用到NSObject, 即符号_OBJC_CLASS_$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要Binding这个操作将对应关系绑定到一起。

Rebase解决了内部的符号引用问题,而外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的。

1.11 dyld 2 和 dyld 3

iOS 13之前,所有的第三方App都是通过dyld 2来启动 App 的,主要过程如下:

  • 解析 Mach-OHeaderLoad Commands,找到其依赖的库,并递归找到所有依赖的库
  • 加载Mach-O文件
  • 进行符号查找
  • 绑定和变基
  • 运行初始化程序

dyld 3被分为了三个组件:

  • 一个进程外的Mach-O 解析器 预先处理了所有可能影响启动速度的search path、@rpaths 和环境变量 然后分析Mach-OHeader和依赖,并完成了所有符号查找的工作 最后将这些结果创建成一个启动闭包 这是一个普通的daemon进程,可以使用通常的测试架构

  • 一个进程内的引擎,用来运行启动闭包 这部分在进程中处理 验证启动闭包的安全性,然后映射到dylib之中,再跳转到main函数 不需要解析Mach-OHeader 和依赖,也不需要符号查找。

  • 一个启动闭包缓存服务 系统App的启动闭包被构建在一个Shared Cache 中,我们甚至不需要打开一个单独的文件 对于第三方的App,我们会在App安装或者升级的时候构建这个启动闭包。 在iOS、tvOS、watchOS中,这一切都是App启动之前完成的。在macOS上,由于有Side Load App,进程内引擎会在首次启动的时候启动一个daemon进程,之后就可以使用启动闭包启动了。

dyld 3 把很多耗时的查找、计算和I/O 的事件都预先处理好,这使得启动速度有了很大的提升。

2、App 加载流程

有了前面的知识储备,接下来将探索app的加载流程。

在应用程序的入口 main()函数之前断点,查看堆栈信息 main( )函数之前

可以看到,先于main函数调用的是 start,同时,这一流程是由libdyld.dylib库执行的。dyld 是开源库,可以下载源码探索。点击下载dyld 源码

为了看到更详细的调用过程,我们在项目中的 ViewController+ (void) load 方法打断点。详细堆栈信息如下

2.1 _dyld_start

可见,调用流程是从 _dyld_start 开始的,我们在下载好的源码中搜索 _dyld_start 。在 dyldStartup.s 文件中找到了入口,这里是用汇编实现的,尽管在不同架构下有所区别,但都是会调用 dyldbootstrap 命名空间下的start 方法,这和上面的堆栈顺序也是相同的。

call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)

2.2 dyldbootstrap::start

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>
    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
    rebaseDyld(dyldsMachHeader);

	// 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(argc, argv, envp, apple);
#endif

	// now that we are done bootstrapping dyld, call dyld's main
	uintptr_t appsSlide = appsMachHeader->getSlide();
	return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyldbootstrap::start中,主要过程为:

①使用全局变量之前,对dyld进行rebase操作,以修复为 real pointer 来运行;

②设置参数和环境变量;

③读取 app二进制文件 Mach-Oheader 得到偏移量 appSlide,然后调用dyld 命名空间下的_main 方法。

2.3 dyld::_main

这里是dyld的入口。内核加载了dyld然后跳转到 _dyld_start 来设置一些寄存器的值之后 进入这个方法。返回 _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)
{
	......

    // 设置运行环境,可执行文件准备工作
	......

	// load shared cache   加载共享缓存
	mapSharedCache();
    ......

reloadAllImages:

    ......
	// instantiate ImageLoader for main executable 加载可执行文件并生成一个ImageLoader实例对象
	sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

	......

	// 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 main executable  链接主程序
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

	......
	// link any inserted libraries   链接所有插入的动态库
	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->weakBind(gLinkContext);
		
	sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);

	......
    // run all initializers   执行初始化方法
	initializeMainExecutable(); 

	// notify any montoring proccesses that this process is about to enter main()
	notifyMonitoringDyldMain();

    return result;
}

主要过程:

①第一步: 设置运行环境,为可执行文件的加载做准备工作;

②第二步: 映射共享缓存到当前进程的逻辑内存空间;

③第三步: 实例化主程序;

④第四步: 加载插入的动态库;

⑤第五步: 链接主程序;

⑥第六步: 链接插入的动态库;

⑦第七步: 执行弱符号绑定(weakBind);

⑧第八步: 执行初始化方法;

⑨第九步: 查找程序入口并返回main( ).

  • 注1: sMainExecutable = instantiateFromLoadedImage(....) 与 loadInsertedDylib(...)

这一步 dyld 将我们可执行文件以及插入的 lib 加载进内存,生成对应的imagesMainExecutable 对应着我们的可执行文件,里面包含了我们项目中所有新建的类。 InsertDylib 一些插入的库,他们配置在全局的环境变量 sEnv 中,我们可以在项目中设置环境变量 DYLD_PRINT_ENV 为1来打印该 sEnv 的值。

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);
		return (ImageLoaderMachO*)image;
	}
	
	throw "main executable not a known format";
}

isCompatibleMachO 是检查Mach-O的subtype是否是当前cpu可以支持; 内核会映射到主可执行文件中,我们需要为映射到主可执行文件的文件,创建ImageLoader。

instantiateMainExecutable 就是实例化可执行文件, 这个期间会解析LoadCommand, 这个之后会发送 dyld_image_state_mapped 通知; 在此方法中,读取image,然后addImage() 到镜像列表。

  • 注2: link(sMainExecutable,...) 和 link(image,....)

对上面生成的 Image 进行链接。这个过程就是将加载进来的二进制变为可用状态的过程。其主要做的事有对image进行 load(加载),rebase(基地址复位),bind(外部符号绑定),我们可以查看源码:

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
	......
	this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);	
	......
	this->recursiveRebaseWithAccounting(context);
	......
    this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
}
  • 注2.1: recursiveLoadLibraries(context, preflightOnly, loaderRPaths) 递归加载所有依赖库进内存。

  • 注2.2:recursiveRebase(context) 递归对自己以及依赖库进行rebase操作。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现 ASLR(Address space layout randomization,地址空间布局随机化),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问。

  • 注2.3:recursiveBindWithAccounting(context, forceLazysBound, neverUnload); 对库中所有nolazy的符号进行bind,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind。

2.4 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]);
	
	// 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]);
}

这一步主要是调用所有imageInitalizer方法进行初始化。先为所有插入并链接完成的动态库执行初始化操作

sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);

再为主程序可执行文件执行初始化操作

sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

具体流程为: ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization

详细代码如下:

2.5 ImageLoader::runInitializers

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
	uint64_t t1 = mach_absolute_time();
	mach_port_t thisThread = mach_thread_self();
	ImageLoader::UninitedUpwards up;
	up.count = 1;
	up.imagesAndPaths[0] = { this, this->getPath() };
      // 重点
	processInitializers(context, thisThread, timingInfo, up);
	context.notifyBatch(dyld_image_state_initialized, false);
	mach_port_deallocate(mach_task_self(), thisThread);
	uint64_t t2 = mach_absolute_time();
	fgTotalInitTime += (t2 - t1);
}

调用 processInitializers

2.6 ImageLoader::processInitializers

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
									 InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
	uint32_t maxImageCount = context.imageCount()+2;
	ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
	ImageLoader::UninitedUpwards& ups = upsBuffer[0];
	ups.count = 0;
	// Calling recursive init on all images in images list, building a new list of
	// uninitialized upward dependencies.
	for (uintptr_t i=0; i < images.count; ++i) {
      // 重点
		images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
	}
	// If any upward dependencies remain, init them.
	if ( ups.count > 0 )
		processInitializers(context, thisThread, timingInfo, ups);
}

在这里,对镜像表中的所有镜像执行recursiveInitialization ,创建一个未初始化的向上依赖新表。如果依赖中未初始化完毕,则继续执行processInitializers,直到全部初始化完毕。

2.7 ImageLoader::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.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);

			// 重点 1: let objc know we are about to initialize this image
			uint64_t t1 = mach_absolute_time();
			fState = dyld_image_state_dependents_initialized;
			oldState = fState;
			context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
			
			// 重点 2: initialize this image   
			bool hasInitializers = this->doInitialization(context);

			// 重点 3: let anyone know we finished initializing this 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();
}

recursiveInitialization 函数中,我们重点关注

  • context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
  • doInitialization(context)
  • context.notifySingle(dyld_image_state_initialized, this, NULL);
  • context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

通知objc我们要初始化这个镜像,这里 通过 notifySingle 函数对sNotifyObjCInit 进行函数调用。

2.7.1 context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo)

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
	......

    if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
		uint64_t t0 = mach_absolute_time();
		
		(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		
	}
    ......	
}

获取镜像文件的真实地址 【*sNotifyObjCInit)(image->getRealPath(), image->machHeader() 】,而 sNotifyObjCInit 是 通过 registerObjCNotifiers 中传递的参数(_dyld_objc_notify_init)进行赋值的。

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

继而找到,registerObjCNotifiers 的 拉起函数 _dyld_objc_notify_register .

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 函数是供 objc runtime 使用的,当objc镜像被映射,取消映射,和初始化时 被调用的注册处理器。我们可以在 libobjc.A.dylib 库里,_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(); // C++
    runtime_init(); // runtime 初始化
    exception_init(); // 异常初始化
    cache_init(); // 缓存初始化
    _imp_implementationWithBlock_init(); //

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

runtime初始化后,在_objc_init中注册了几个通知,从dyld这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的load方法等。

就拿sMainExcuatable来说,它的initializer方法是最后调用的,当initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,但由于lazy bind机制,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。

当所有的依赖库的lnitializer都调用完后,dyld::main 函数会返回程序的main()函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口。

那么 _objc_init 又是如何被调用的呢?

看调用堆栈,在 ImageLoader::recursiveInitialization 函数中,我们之前关注的重点2: doInitialization

  • this->doInitialization(context);
// 重点 2: initialize this image   
bool hasInitializers = this->doInitialization(context);

2.7.2 ImageLoaderMachO::doInitialization

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

在 doModInitFunctions之后 会 先执行 libSystem_initializer,保证系统库优先初始化完毕,在这里初始化 libdispatch_init,进而在_os_object_init 中 调用 _objc_init

由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理

runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法。

至此,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,在这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)

总结:

APP是由内核引导启动的,kernel内核做好所有准备工作后会得到线程入口及main入口,但是线程不会马上进入main入口,因为还要加载动态链接器(dyld),dyld会将入口点保存下来,等dyld加载完所有动态链接库等工作之后,再开始执行main函数。

系统kernel做好启动程序的初始准备后,交给dyld负责。

dyld接手后,系统先读取 App 的可执行文件(Mach-O文件),从里面获取dyld的路径,然后加载dylddyld去初始化运行环境,开启缓存策略,配合 ImageLoader 将二进制文件按格式加载到内存,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。当所有依赖库初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回main()函数地址,main()函数被调用。

这个过程远比写出来的要复杂,这里只提到了 runtime 这个分支,还有像 GCD、XPC 等重头的系统库初始化分支没有提及(当然,有缓存机制在,它们也不会玩命初始化),总结起来就是 main 函数执行之前,系统做了茫茫多的加载和初始化工作,最终引入那个熟悉的main函数。