Ios应用程序启动全过程解析

在iOS应用启动的时候,我们都知道入口函数是main.m的main方法:

int main(int argc, char * argv[]) {...}

但是这只是从应用层面上的入口,在这之前,系统加载的过程是非常值得我们了解和运用的

当应用程序被打开的时候,kernel就会将应用程序加载到内存中,然后kernel会加载另一个程序,就是我们的dyld(动态连接器),不论是我们的应用还是dyld都是一个可执行的程序,在Mac OS和iOS上称作Mach-O

Mach-O

简单介绍下Mach-O文件内容,就拿我们的app来说,找到编译后的xxx.app文件,这里是Test.app,包内有个Test执行文件,就是我们要找的Mach-O文件,这里使用MachOView这个软件来打开它,看看内部结构

从图中可以看到,大致分成了下面三部分(segment),其他的部分这里先不说

|------------|
|   __TEXT   |  只读
|------------|		  
|			 |
|   __DATA   |	读写
|------------|
|			 |
| __LINKEDIT |	只读
|			 |
|------------|

其中每个区域都有多个页结构,因为iOS中的内存使用虚拟内存,使用了页式存储,在加载的时候也是需要按照页来加载,每个页的大小在arm64上是16K,其他的是4K大小

虚拟内存

提到虚拟内存,虽然作者没深入过,但是参考文档来的内容需要说下关于应用启动有几个点需要提一下:

基本原理: 由于物理内存有限,将物理内存映射成更大的虚拟内存,每个进程有独立的虚拟内存,需要进行文件操作的时候才真正计算出物理内存地址进行读写.

下面是虚拟内存加载我们的应用的内存模型:

|------------|	0x000000			
|   PAGEZERO |				--> 防止NULL指针, 不可读写和执行
|------------|	0x???000	  
| 	 App	 |				--> app
|------------|  0x???000	  
|	 A.dylib |				--> app依赖的所有动态库
|------------|  0x???000	  
|	 dyld    |				--> 负责加载所有动态库
|------------|

一个dylib加载到内存的过程大致可以分成:

-> 加载Mach-O header :里面包含应用程序的说明信息

-> 加载LINKEDIT的内容 :包含如何加载数据内容及方法名和地址

-> 加载DATA数据 :加载数据到__DATA段,进行读写

-> 卸载LINKEDIT内容 :因为加载完数据后,这部分内容不需要再使用了,可以卸载掉

说了这么多,还是没进入主题??…

下面就涉及到了我们的主角: dyld程序,万幸这是个开源程序,在这里看源码

dyld的工作

应用程序在main()之前所做的工作大致可以分成几个步骤:

  1. kernel准备环境,将应用程序装载到内存中
  2. kernel装载dyld程序,并调用dyld进行应用初始化操作
  3. load dylibs 根据Mach-O的Load Commands段加载所有依赖的动态库
  4. rebase 将经过ASLR随机偏移过的访问首地址创建偏移指针并存储到__DATA段,方便后续访问使用
  5. bind 将应用程序内调用外部的指令绑定起来
  6. Objc 调用Objc的runtime环境并初始化,处理category和调用+load()方法
  7. initializers 调用所有动态库的initializer方法,初始化动态库
  8. call main() 调用app入口函数main

我们先在工程中断点打在_objc_init方法,来看一下dyld做的事情

dyld启动

dyld的执行从dyldStartup.s汇编文件开始,

	...
	
__dyld_start:
	popq	%rdi		# param1 = mh of app
	pushq	$0		# push a zero for debugger end of frames marker
	movq	%rsp,%rbp	# pointer to base of kernel frame
	andq    $-16,%rsp       # force SSE alignment
	subq	$16,%rsp	# room for local variables
	
	# call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
	movl	8(%rbp),%esi	# param2 = argc into %esi
	leaq	16(%rbp),%rdx	# param3 = &argv[0] into %rdx
	movq	__dyld_start_static(%rip), %r8
	leaq	__dyld_start(%rip), %rcx
	subq	 %r8, %rcx	# param4 = slide into %rcx
	leaq	___dso_handle(%rip),%r8 # param5 = dyldsMachHeader
	leaq	-8(%rbp),%r9
	call	__ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
	movq	-8(%rbp),%rdi
	
	...

汇编实在看不懂,但是从代码片段中的注释也能看得出来,这里调用了dyldbootstrap::start方法

dyldbootstrap::start方法进行了dyld自身的初始化操作(rebase, initialize),然后调用dyld::_main方法开始对app的工作

dyld::_main方法就进行了加载dylibs,rebase,bind等操作,这个方法内容太多,详细的可以去读源码,这里简要标注出它做了哪些事情,省略大部分代码:

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue)
{
	// 1.检查如果在模拟器中运行,就是用模拟器的dyld程序
	result = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue);
	
	// 2.设置ImageLoader的context
	setContext(mainExecutableMH, argc, argv, envp, apple);
	
	// 3.初始化各种存储变量
	// make initial allocations large enough that it is unlikely to need to be re-alloced
	sAllImages.reserve(INITIAL_IMAGE_COUNT);
	sImageRoots.reserve(16);
	sAddImageCallbacks.reserve(4);
	sRemoveImageCallbacks.reserve(4);
	sImageFilesNeedingTermination.reserve(16);
	sImageFilesNeedingDOFUnregistration.reserve(8);
    
	// 4.加载Image(代表镜像)
	sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
	
	// 5.如果有插入的库,加载
	loadInsertedDylib(*lib);
	
	// 6.link链接
	link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
	
	// 7.初始化调用image的初始化方法
	initializeMainExecutable();

	// 8.找到`main()`函数地址返回
	result = (uintptr_t)sMainExecutable->getThreadPC();
	return result;
}

ImageLoader

ImageLoader负责将每一个image加载到内存中,经过了一系列的调用过程,进行了

runInitializers -> processInitializers -> recursiveInitialization -> doInitialization -> doImageInit -> doModInitFunctions

之后,完成了

其中调用image的初始化方法其实是调用了C++的构造方法,也就是被static __attribute__((constructor))修饰的方法,这一点可以从加载libSystem.dylib包中的libSystem_initializer方法看到,libSystem.dylib的源码在[这里] (https://opensource.apple.com/source/Libsystem/Libsystem-169.3/)

libSystem.dylibinit方法中初始化了多了dylib库,比如:liblaunch.dylib,libc.a,libdispatch.a

load 方法何时被调用?

从上面的堆栈图可以看到,进入_objc_init方法是在doModInitFunctions之后,这个时候ImageLoader会调用所有image的init方法,其中就包含了libObjc.dylib包,libdispatch_init方法被调用

void libdispatch_init(void)
{
	...
	// libDispatch.dylib初始化方法
	_os_object_init();
	...
}

void _os_object_init(void)
{
	// 调用了libObjc.dylib初始化方法
	_objc_init();
	...
}

这里转到了libObjc.dylib库中,这里就包含了runtime的和load方法调用等初始化等操作,我们继续进入objc源代码看一下_objc_init方法实现

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_2_images, load_images, unmap_image);
}

可以看到最后_dyld_objc_notify_register(&map_2_images, load_images, unmap_image);想法dyld注册了回调函数,当imagemap到内存中,当初始化完成image时和卸载image的时候都会回调注册者,load_images函数:

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
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

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

prepare_load_methods方法中对load方法进行了前期准备:

  1. 遍历所有的load方法,确保父类先于子类的顺序加载到一个类的load方法list
  2. 遍历所有的category类,将load方法添加一个类目的load方法list

load方法加载顺序: 父类 > 子类 > 类目(category)

call_load_methods方法中只做了一件事情: 先调用类的load方法list中所有方法, 然后调用类目的load方法list中的所有方法

总结:load方法在libObjc.dyld库初始化的时候调用,在main函数之前调用

category的加载

从上面的_objc_init方法中注册的dyld回调可以发现,当dyld将image拷贝到内存中(mmap)之后会调用map_2_images方法,然后进行了部分初始化后调用_read_images方法

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
	// 1. 初始化全局存储所有类的list
	gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
	
	// 2. 从Mach-O的__DATA区 __objc_classlist 获取所有类,并加入
	//    gdb_objc_realized_classes list中
	classref_t *classlist = _getObjc2ClassList(hi, &count);
	
	// 3. 注册Sel,并存储到全局变量namedSelectors的list中
	sel_registerNameNoLock(name, isBundle);
	
	// 4. 找到所有Protocol并处理引用
	protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
	
	// 5. 初始化类结构,插入子类
	realizeClass(cls);
	
	// 6. 开始处理category
	// 6.1 找到所有category,
	category_t **catlist = _getObjc2CategoryList(hi, &count);
	// 6.2 处理未绑定的category,存储到静态变量category_map的list中
	addUnattachedCategoryForClass(cat, cls, hi);
	// 6.3 如果元类已经创建了,就开始将category的方法,属性,Protocol绑定到元类上
	attachCategories(cls, cats, true /*flush caches*/);
	

}

6.3中可以看到将category的方法,属性和Protocol信息添加到元类的方列表,属性列表和Protocol列表中了,但是添加顺序是如何的呢?

例如元类是NSObject, category是NSObject+Cat

元类中有方法列表: [A, B, C] 三个方法
category类有方法列表: [A+, B+] 两个方法

在6.1中找到的所有category,在这个时候是按照load顺序加载进来的,比如`cat3, cat2, cat1`

到6.3的时候会按照加载顺序倒序或取出来所有category的方法,属性和Protocol列表,这里保证了最后加载的category会优先绑定到元类中

在6.3中继续进行category绑定到元类中,使用了`memmov`和`memcpy`来保证元类的方法,属性和Protocol会整体放在category的相关信息之后,例如上面方法列表合并之后变成了:

> [A+, B+, A, B, C]

值得注意的点:

继续回到dyld的_main函数中来,继续加载根目录Framework目录下的其他动态库. 加载完所有的dylibs之后,每个dylib之间还是没有关联,不知道怎么调用,这时候就该进行link操作了,link操作分成rebase和binding辆部分.

rebase: 由于ASLR访问地址被随机化,所以rebase在动态库内部进行修正访问地址,并创建访问地址存储在__DATA段,这个期间可能会产生缺页并进行IO操作

binding: 主要负责动态库之间的调用地址的修正和创建

initialize

这里就比较简单了, 这个时候各个库都已经load完成,访问地址指针也已经修正过,就可以初始化所有dylib了, 会调用C++的初始化构造方法,也就是用__attribute__((constructor))修饰的方法,被修饰的方法都会在main()方法之前调用

调用main()

当执行完dyld::_main方法之后,返回了main()函数地址,这个时候所有初始化工作都已经完成了,正式进入Objc声明周期

总结

在main()函数之前,kernel和dyld已经做了非常多的底层工作来保证程序正常运行,大致可以总结成以下步骤:

  1. 解析image
  2. 映射image到虚拟内存
  3. rebase image
  4. bind image
  5. 初始化所有dylib
  6. 调用main()

其中在Objc这一层面上还有category的处理,类的结构处理和runtime环境的初始化等等工作,值得我们细致研究

参考

对于debug:在Scheme中添加环境变量

DYLD_PRINT_STATISTICS: 分析main方法之前的加载时间

DYLD_PRINT_INITIALIZERS : 打印初始化的dylib路径

DYLD_PRINT_ENV: 打印环境

dyld源码: https://github.com/opensource-apple/dyld

wwdc: https://developer.apple.com/videos/play/wwdc2016/406

Mach-O: https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/MachOTopics