iOS底层原理12:应用程序加载(上)| 8月更文挑战

788 阅读9分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

应用程序的加载

前边文章中我们介绍了很多底层的源码,那么在应用程序加载的过程中,这些代码是怎么写到内存中的呢?我们工程中的动态库和静态库是如何加载到内存中的?这一篇文章我们来分析一下应用程序的加载原理!

动态库和静态库

我们的应用程序在加载的过程用会依赖很多底层的基础库,比如UIKit,CoreFoundation等一系列库,这些库其实是一些可执行的二进制文件,能够被操作系统加载到内存里面;库分类两种:动态库静态库;

动态库静态库的区别就在于库的链接;

在理解链接之前,我们先要了解一下编译的过程:

编译的流程

  • 源文件会经过一个预编译过程,进行词法树语法树的分析;
  • 之后交给编译器进行编译处理;
  • 编译之后会形成一些汇编文件;
  • 汇编文件会进行链接装载;
  • 最终变成可执行文件;

动态链接和静态链接

既然动态库静态库的区别在于链接,那么他们的链接过程有什么不同呢?我们来看下边的图片:

我们可以发现:静态库在装载的时候会一个一个进行装载,最终会有多个库重复装载的问题存在,会浪费性能和空间;而动态库在装载的时候会被多个需要装载的地方共享,对内存和空间都有很大优化;

那么这些库是如何加载到内存中的呢?这就需要用到链接器,也就是我们常说的dyld

应用程序的加载流程

dyld的引出

那么什么是dyld呢?如何找到dyld呢?我们新建一个工程,然后在main函数中打上断点运行:

发现,在main函数执行前,还有一个start被执行了,那么这个start是什么呢?

start来自于libdyld.dylib,这个libdyld.dylib就是dyld;它是一个动态链接器,用来链接

那么我们如何研究dyld呢,start符号断点无法被拦截,但是ViewControllerload方法在main函数之前调用了(load方法在main函数之前调用),我们以此为切入点,在load方法中断点执行:

最开始执行的是dyld_dyld_start,接下来就需要借助dyld的源码进行分析了dyld源码下载地址

dyld的流程

我们现在所用的是dyld3

dyld_start

dyld的源码中搜索_dyld_start,找到_dyld_start被调用的地方

dyldbootstrap::start

dyldStartup.sdyld的起始文件,是一个汇编文件,我们以arm架构来分析,虽然汇编看不太懂,但是根据注释,可以清楚_dyld_start中调用了dyldbootstrap::start,也就是调用了命名空间dyldbootstrap下的start方法:

dyld::_main

start方法的最后,调用了dyld::_main方法

由于_main方法太过庞大,其中包含了配置环境平台版本路径主机等一系列信息来为后边的操作做准备条件,我们主要看一下其中的几个核心的地方:

getHostInfo(mainExecutableMH, mainExecutableSlide); // 对主机信息的处理 架构等
setContext(mainExecutableMH, argc, argv, envp, apple); // 设置dyld上下文信息
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide); // 确定当前是否有共享缓存
mapSharedCache(mainExecutableSlide); // 加载共享缓存
reloadAllImages: // 加载镜像文件

_main方法最终返回了一个result,而根据源码分析发现resultsMainExecutable息息相关,那么sMainExecutable是在哪里创建的呢?

很明显reloadAllImages这个方法体是我们需要研究的核心方法;接下来我们分析一下,系统在reloadAllImage方法中都做了什么操作?

reloadAllImage

instantiateFromLoadedImage 初始化主程序

镜像文件加载器:从MachO文件中,按照格式加载镜像文件,初始化主程序

loadInsertedDylib 加载动态库

加载插入的动态库

link 主程序和动态库

链接主程序以及链接插入的动态库

weakBind 弱引用绑定

所有的镜像文件链接完毕后进行弱引用绑定

initializeMainExecutable 运行主程序

运行所有初始化主程序

notifyMonitoringDyldMain 通知进入main

主程序运行之后,通知dyld可以进入main函数

initializeMainExecutable

前面我们大致分析了一下dyld的大致流程,接下来,我们分析一下主程序运行过程中的一些细节问题

initializeMainExecutable方法中只有一个核心方法runInitializers,它就是我们主要研究的内容:

runInitializers 镜像文件初始化

主程序运行一开始,就根据镜像文件的个数allImagesCount()初始化每个镜像文件

在每一个镜像文件初始化的过程中,都会执行processInitializersnotifyBatch方法;

processInitializers 准备工作

在递归调用的过程中,recursiveInitialization进行初始化工作

recursiveInitialization 初始化

uninitUps.imagesAndPaths拿到镜像文件路径;dependentImage->recursiveInitialization递归初始化依赖文件

在初始化镜像文件之前,我们需要先通知注入一些依赖文件(比如C++文件),然后才能开始初始化镜像文件,镜像文件初始化完成之后,通知程序初始化完成;

这也就是objc中的C++的构造函数比我们自己写的构造函数更早执行的原因

notifySingle

搜索发现,此方法在这里被赋值

最终调用的是:

此方法核心内容是给sNotifyObjCInit进行赋值,那么sNotifyObjCInit是在什么时候调用的呢?

registerObjCNotifiers方法被赋值,那么registerObjCNotifiers方法什么时候被调用了呢?

registerObjCNotifiers是被_dyld_objc_notify_register调用的,我们继续向上查找发现无法找到其被调用的地方,那么接下来我们通过符号断点的方式看一下

我们发现是_objc_init调起的_dyld_objc_notify_register,那么_objc_init在哪里呢?

结果发现,_objc_initobjc的源码中被调用,那么我们在源码中搜索查看:

发现,在_objc_init中确实调用了_dyld_objc_notify_registerobjc的源码和dyld的源码是有关联的;

_objc_init

那么,在这个地方就会引出一个问题,是谁调用了_objc_init呢?我们断点运行objc的源码查看堆栈信息:

根据堆栈信息显示_objc_init是由_os_object_init来调起的,那么_os_object_init来自什么地方呢?

_os_object_init来源于libdispatch.dylib,接下来下载libdispatch的源码分析libdispatch源码下载地址

libdispatch源码中的_os_object_init方法里确实调用了_objc_init方法,而且搜索发现,此方法确实来源于objc的源码

那么_os_object_init又是谁调起的呢?根据堆栈信息:

_os_object_init是由libdispatch.dyliblibdispatch_init这个方法调用了,我们在源码中验证一下:

根据堆栈信息libdispatch_init是由libSystem.B.dyliblibSystem_initializer方法调用的

接下来,下载libSystem的源码验证一下:

libSystem.B.dyliblibSystem_initializer方法中调用了libdispatch_init方法;

那么继续分析,libSystem_initializer的调用者是谁?

调用者为dylddoModInitFunctions方法,重新又回到了dyld的源码中;

doModInitFunctions

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
	if ( fHasInitializers ) {
		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) {
			if ( cmd->cmd == 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) {
					const uint8_t type = sect->flags & SECTION_TYPE;
					if ( type == S_MOD_INIT_FUNC_POINTERS ) {
						Initializer* inits = (Initializer*)(sect->addr + fSlide);
						const size_t count = sect->size / sizeof(uintptr_t);
						// <rdar://problem/23929217> Ensure __mod_init_func 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("__mod_init_funcs section has malformed address range for %s\n", this->getPath());
						for (size_t j=0; j < count; ++j) {
							Initializer func = inits[j];
							// <rdar://problem/8543820&9228031> verify initializers are in image
							if ( ! this->containsAddress(stripPointer((void*)func)) ) {
								dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
							}
							if ( ! dyld::gProcessInfo->libSystemInitialized ) {
								// <rdar://problem/17973316> libSystem initializer must run first
								const char* installPath = getInstallPath();
								if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
									dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
							}
							if ( context.verboseInit )
								dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
							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;
							}
						}
					}
					else if ( type == S_INIT_FUNC_OFFSETS ) {
						const uint32_t* inits = (uint32_t*)(sect->addr + fSlide);
						const size_t count = sect->size / sizeof(uint32_t);
						// 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("__init_offsets section has malformed address range for %s\n", this->getPath());
						if ( seg->initprot & VM_PROT_WRITE )
							dyld::throwf("__init_offsets section is not in read-only segment %s\n", this->getPath());
						for (size_t j=0; j < count; ++j) {
							uint32_t funcOffset = inits[j];
							// verify initializers are in image
							if ( ! this->containsAddress((uint8_t*)this->machHeader() + funcOffset) ) {
								dyld::throwf("initializer function offset 0x%08X not in mapped image for %s\n", funcOffset, this->getPath());
							}
							if ( ! dyld::gProcessInfo->libSystemInitialized ) {
								// <rdar://problem/17973316> libSystem initializer must run first
								const char* installPath = getInstallPath();
								if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
									dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
							}
                            Initializer func = (Initializer)((uint8_t*)this->machHeader() + funcOffset);
							if ( context.verboseInit )
								dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
#if __has_feature(ptrauth_calls)
							func = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)func, ptrauth_key_asia, 0);
#endif
							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;
							}
						}
					}
				}
			}
			cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
		}
	}
}

在这个方法中,确保libSystem必须被优先初始化

doInitialization 初始化镜像文件

而根据源码分析得出结论:doModInitFunctions是被doInitialization方法调起的;

doInitialization我们在之前分析过,它是在recursiveInitialization方法中调用的:

map_images和load_images

在源码中断点执行:

发现,map_images会先于load_images方法执行;map_imagesload_images是沟通了dyldobjc的桥梁

map_images执行流程:

_dyld_objc_notify_register-->registerObjCNotifiers-->notifyBatchPartial-->map_images

_dyld_objc_notify_register调用的时候,我们注意到map_images是第一个参数,那么根据调用流程最后判断在registerObjCNotifiers方法中的sNotifyObjCMapped就是map_images:

那么,在这里赋值给sNotifyObjCMapped之后,又是在什么时候调用的呢?全局搜索sNotifyObjCMapped:

sNotifyObjCMapped是在notifyBatchPartial方法中被调用的,那么notifyBatchPartial方法又是什么时候被调用的呢?

最终发现又回到了registerObjCNotifiers方法中,再次方法中将map_images赋值给sNotifyObjCMapped之后,紧接着直接在notifyBatchPartial方法中调用了sNotifyObjCMapped,也就是map_images;

load_images执行流程:

_dyld_objc_notify_register-->registerObjCNotifiers-->load_images

load_images里边是所有的load方法的集合,他作为_dyld_objc_notify_register的第二个参数,赋值给了sNotifyObjCInit,那么sNotifyObjCInit是在什么时候调用的呢?

sNotifyObjCInit实在notifySingle方法中被调用执行的,搜索notifySingle找到它被调用的地方:

那么,这里就有一个疑问了?按照流程,明明是在doInitialization的执行过程中执行到registerObjCNotifiers方法中,才将load_images赋值给sNotifyObjCInit,那为什么在doInitialization执行之前就去调用了notifySingle了呢?

因为doInitialization是对当前镜像文件的初始化,并非对整个程序的初始化,所以当前镜像文件的load方法应该是在doInitialization之后的notifySingle方法中调用,在doInitialization之前的notifySingle的方法调用的是依赖文件的方法,recursiveInitialization是一个递归初始化方法;

我们通过objc的源码来验证一下:

objc-os.mm文件中添加以下代码:

__attribute__((constructor)) void objcFunc() {
    printf("这是objc的Func--->%s \n", __func__);
}

在类Person中添加load方法:

@implementation Person
+ (void)load {
    NSLog(@"%s", __func__);
}
@end

main文件中添加以下代码:

__attribute__((constructor)) void mineFunc() {
    printf("这是我自己的Func--->%s \n", __func__);
}

运行打印结果如下:

这是objc的Func--->objcFunc 
+[Person load]
这是我自己的Func--->mineFunc 
Hello, World!

结论objc源码中的C++构造方法先执行,然后执行我们自己程序中的类的load方法,最后执行我们自己程序中的C++构造方法与上述分析结论一致;

dyld流程分析