iOS底层探究—–dyld程序启动加载流程

前言

在程序加载过程中,不是每句代码都是从0基础开始的,都会依赖很多基础库,如UIKitCoreFoundationobjc等等。。。这些库都是可以被操作系统加载到内存里面的可执行二进制文件。库总体分为静态库动态库。还有在运行工程的时候,如果打上断点,也能在Xcode的左边看到一些加载的过程,从start一直到断点的方法处。但是在程序的启动开始,直到main函数加载完成,这个过程中,到底经历了什么?接下来,就一一揭晓。

资源准备

进入正文

刚刚我们说到了静态库动态库,那么程序是怎样把这些静态库动态库给加载到工程里面去的了?需要一个动态链接器dyld。到底是怎么样进行链接的了?下文中将详细分析。

  • 静态库和动态库的区别

未命名文件-2.png
静态库可能被重复添加,这样就会造成内存的浪费,而动态库的话,就能避免这个问题,这也是为什么在苹果系统里面,使用的动态库占大多数的原因了。

  • 编译过程

未命名文件-3.png

案例引入

提出问题:为什么会进入到_objc_init函数?

首先,在objc源码中,不做任何操作,直接运行:
未命名文件.png
会直接进入到_objc_init函数里面。这个是objc的初始化,为什么回来到这里了?

引入案例准备工作

重新建立一个工程,ViewController里面实现+load方法。然后在下图几处设置断点:
未命名文件(1).png
再运行工程,断点先来到+load方法处,然后通过lldb调试,执行bt指令,查看堆栈信息:
未命名文件-8.png
从堆栈信息,可以看出程序是从_dyld_start开始,共执行了13步,最后到+[ViewController load],但是在Xcode的左边,只看到_dyld_start –> load_images –> +[ViewController load]这个三个对外的方法展示,但是很多的内部方法都没有展示出来,通过打印堆栈信息,才能一一查看。

同样的,也可以通过汇编,也可以一步步的查看整个加载流程。

现在对加载流程,有了一个初步的了解,接下来,就通过对dyld库分析,来进行详细了解。文中分析的是用dyld-852库。(因为这个库依赖底层系统库太多,所以运行不起来。嘿嘿)

dyld宏观流程

从刚刚的分析中,知道程序的启动,是从_dyld_start开始的,那么在dyld-852库里面的入手点,也就从这里开始。老规矩,在dyld代码里面全文索引_dyld_start,因为从dyld2版开始,苹果系统为了减少预绑定工作量,拆分了多种架构,如:X86X86_64armarm64等。下面是以arm架构为例,搜索结果如下图:
未命名文件.png
直接看汇编可能比较生涩,但是其中有注释一行C++代码,dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue),看这个C++的方法,再对照汇编代码和注释,就能大致清楚一二,通过计算得到相应的几个参数,然后再传值跳转。为了直观性,我们就直接通过C++方法进行探索。由于C++定义方法的特性,直接索引dyldbootstrap::start是不会得到结果的,先找到dyldbootstrap,再查找dyldbootstrap作用域里面的start函数。

从汇编_dyld_start到C++的dyldbootstrap::start

DC420F8D-414F-4E33-915B-263331E18C92.png
接着就在dyldbootstrap作用域里面找到start函数。(现在我们是探究dyld宏观流程,那么就对函数里面的具体实现,不做过多的解释了,下文中的函数也是一样)

uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
				const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

   。。。。代码省略。。。。

	// 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);
}
复制代码

start函数的返回值可以看到,是dyld::_main()(是dyld作用域的_main()函数)。接着就是进入dyld::_main()里面。

dyldbootstrap::start进入dyld::_main()

同样,我们通过下图可以得到,光main()函数的代码就是接近1000行,这个main()函数不是程序加载完成的main()函数,而是dyld内部的一个main函数。
7C48CECD-022C-4AEA-A0C2-9D93AF5D3E51.png
对于这么庞大的一段代码,该如何去分析了?我们知道,dyld的作用是动态链接镜像文件(image),那么我们在源码中,只要找到这方面的代码,就能得到答案。查看_main函数的返回值,返回了result。那就接着查看在_main函数里面result的赋值情况,如下图:
未命名文件-4.png
_main函数里面的查询结果来看,一个是fake_main函数,另外一个是sMainExecutable函数。然而fake_main函数,显然不是我们要找的。

int
fake_main()
{
	return 0;
}
复制代码

那么只剩下sMainExecutable函数了,接着就得看在_main函数里面,对sMainExecutable函数的是使用情况了。下图是在_main函数里面调用sMainExecutable函数的情况,如弱引用绑定(7229)、数据绑定(7215)、镜像文件绑定(7136)、实例化(7009)等等,这也从侧面反馈着,我们查找sMainExecutable函数是查找对了,和我们查找的目标也相匹配(目标:加载所有的镜像文件以及相应的其他的),这就是反推法:
未命名文件.png
确定了sMainExecutable函数是接下来的步骤后,就直接查看sMainExecutable函数的实例化有关的方法,那就是

sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
复制代码

这个初始化方法,就是镜像文件加载器。那么接下来就直接进入instantiateFromLoadedImage函数里面。

实例化主程序instantiateFromLoadedImage()

instantiateFromLoadedImage函数里面的实现,知道需要传入macho_headerslidepath,再加载image(镜像文件)

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";
}
复制代码

对于这些传入的参数到底是什么了?我们直接可以在finder中,拿到前文中的那个案例的包,然后显示包内容,拿到其machO文件:
8E41A66A-4D5F-499D-96E4-76493D8C7DEF.png
拿到machO文件,再用MachOView这个工具打开,就能查看machO文件里面的内容,里面包含了macho_headerLoad Commands代码段(TEXT)数据段(DATA)符号表(Symbol Table)字符串表(String Table)等等。如下图:
829E20DE-CA34-4490-800B-6F5DA5A05791.png

加载插入的动态库loadInsertedDylib

根据注释load any inserted libraries得知:
C4559C59-2E72-4FF0-9B44-D6B7221B9DC0.png

共享缓存加载mapSharedCache

根据注释load shared cache得知:
3DFAEDF6-E524-4747-AE79-37D0FFE18803.png

link主程序

链接主程序的可执行文件插入的动态库
未命名文件-2.png

弱引用绑定主程序weakBind

在所有的镜像文件都被链接后,才进行弱引用绑定:
未命名文件-3.png

通知dyld可以进入main()函数

通知所有监测进程,此进程将要进入main()
未命名文件-5.png

初始化initializeMainExecutable

run所有的实例化内容:
未命名文件-4.png

进入初始化initializeMainExecutable

  • 初始化镜像文件

未命名文件-6.png

  • 初始化主程序可执行文件

未命名文件-7.png
都是调用了runInitializers函数,那么就直接去看runInitializers函数实现。

runInitializers函数的执行内容

进行初始化的准备工作
未命名文件-8.png

processInitializers初始化的准备

通过for循环加载镜像文件
未命名文件-9.png

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

			// 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);
			
			// initialize this image ------ ②、 调用init方法
			bool hasInitializers = this->doInitialization(context);

			// 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();
}
复制代码

在这里面主要做了三件事:

1、加载依赖文件(①):

  • 单个通知的注入context.notifySingle,为后面的流程做准备,准备完成之后,才能开启后面的流程 ——– 将要开始初始化镜像文件;

2、加载本身(②、③):

  • 调用init方法doInitialization ——– 开始初始化镜像文件;
  • 通知初始化完成context.notifySingle——– 完成镜像文件初始化。

notifySingle

搜索notifySingle赋值的地方,查看其处理的情况,我们最主要的是要查找镜像文件的加载,所以就下面这一截代码符合:

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();
		dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
                //-----重点部分
		(*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);
		}
	}
    。。。。。  代码省略  。。。。。
}
复制代码

要加载镜像文件(image),就是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader()),其类型是sNotifyObjCInit,接着就进入sNotifyObjCInit

sNotifyObjCInit的详情

通过索引,可以知道其类型,static _dyld_objc_notify_init sNotifyObjCInit。再看下sNotifyObjCInit的赋值,直接就是init
未命名文件-2.png
而执行赋值的函数是registerObjCNotifiers。其中有三个赋值对象:

  • sNotifyObjCMapped= mapped;

  • sNotifyObjCInit= init;

  • sNotifyObjCUnmapped = unmapped;

registerObjCNotifiers的调用

未命名文件-3.png
当看到_dyld_objc_notify_register函数时,在对应文章开端的objc源码中的_objc_init函数里面的实现,也有_dyld_objc_notify_register函数的调用,如下图,是objc源码_obcj_init函数
未命名文件-5.png

  • notifySingle –> _dyld_objc_notify_register,而通过objc源码可知,_dyld_objc_notify_registerobjcinit也会被调用,在汇编里面,_dyld_objc_notify_register是在libobjc.A.dylib这个镜像库里面的。

objc源码中的_dyld_objc_notify_register函数传入的值是:

  • map_images的函数地址—沟通前面内容的非常关键的以数据,如:classprotocolpropertymethodlist等等数据;
  • load_images函数实现;
  • unmap_image函数实现。

结合上面dyld里面源码registerObjCNotifiers函数的赋值情况:

  • sNotifyObjCMapped= mapped = &map_images;

  • sNotifyObjCInit= init = load_images;

  • sNotifyObjCUnmapped = unmapped = unmap_image;

可以说,objc_init函数向dyld中注册了三个函数,在dyld加载镜像文件时,如果满足的条件,这三个函数会被调用执行。

根据上述分析,知道map_images函数和load_images函数是沟通objcdyld之间的桥梁,那么map_images函数和load_images函数调用的情形是怎样的了?—– 请看文章末尾的注解

梳理小结1

文章到这里,已经讲了一条比较长的流程了,也许有些童鞋会感到有些迷惑了,那么我们来稍微梳理下,根据上面的分析,再回过头来,看案例里面的堆栈信息,就能感到不那么陌生了:
未命名文件-8.png

  • 梳理流程:_dyld_start –> dyldbootstrap::start –> dyld::_main –> dyld::initializeMainExecutable –> runInitializers –> processInitializers –> recursiveInitialization

这只是在dyld库里面所进行的流程,要到达_objc_init函数,还有其他库的参与,所以,我们还需要接着往下探索。

_objc_init函数反推

现在我们要想弄清楚接下来的流程。目前,我们已经知道在objc源码里面,运行空工程,都能执行_objc_init函数,那么就在这个函数上打上断点,查看他的堆栈信息:
99481245-460D-4E48-9864-268F3A593D43.png
通过这些信息,我们可以知道,在我们推导到的recursiveInitialization函数之后,还需要执行多个函数,才能到_objc_init函数,现在已知的就这两个条件了,那么需要进行反推,从_objc_init函数开始,往前推导。

_os_object_init函数的调用

根据堆栈信息,_objc_init函数是由_os_object_init函数调起的。堆栈信息里面,_os_object_init函数在libdispatch库里面,那么打开这个库的代码,查找该函数。
3822B369-6252-4D6F-9676-2927993D790C.png
通过源码知道,_os_object_init函数是调用了_objc_init函数。接着,是libdispatch_init函数,调用了_os_object_init函数。所以还是要全局索引libdispatch_init函数,

void
libdispatch_init(void)
{
	。。。。。。省略配置代码。。。。。
#endif
	_dispatch_hw_config_init();
	_dispatch_time_init();
	_dispatch_vtable_init();
	_os_object_init();//--------调用了
	_voucher_init();
	_dispatch_introspection_init();
}
复制代码

从源码上,libdispatch_init函数是调用了_os_object_init函数。

libSystem_initializer函数的调用

再根据堆栈信息,是libSystem_initializer函数调用了libdispatch_init函数,而由堆栈信息可以知道libSystem_initializer函数是在libSystem库里面,那么打开这个库的源码,进行查找。看源码:
533F5C28-F021-44F2-A841-F08186003B10.png
libSystem_initializer函数里面,的确调用了libdispatch_init函数。

doModInitFunctions函数的调用

再回到堆栈信息里面,调用libSystem_initializer函数的是doModInitFunctions函数,而doModInitFunctions函数数属于dyld库里面的。看doModInitFunctions函数的实现源码:
1F15CEDA-D2A0-4680-84A7-9914B5DE78FE.png
这里面有一句注释,libSystem initializer must run first(必须最先加载libSystem库)。根据前面的分析,我们知道,dyld是加载所有的镜像文件。而libdispatch库和objc,都是依赖于libSystem库的。所以,libSystem库为第一要加载的库。由此就可以推断出,doModInitFunctions函数必然是对libSystem库进行加载。

也可以说,doModInitFunctions函数加载了所有C++文件。为什么这么说了,一个下案例:
B92C2655-6ED1-4DFC-8263-C4F3F5CA555C.png
从左侧的堆栈信息结果上看,在加载kcFunc()函数时,先执行doModInitFunctions函数。

doInitialization函数的调用

还是回到堆栈信息里面,由doInitialization函数调用了doModInitFunctions函数(也可以在dyld库里面,全文索引doModInitFunctions函数)。看源码:
4E9D9D27-7D26-4E70-8322-5079D9F1A719.png

接这再往前找,也可以在dyld库里面,全文索引doInitialization函数,看源码:
3687CD08-9178-4A05-A4CF-141D1A15A548.png
是在recursiveInitialization函数里面调用的doInitialization函数。

至此,就和前面的推导衔接了起来。

梳理小结2

dyld库,再到libdispatch库和libSystem库,形成一个通顺的流程:

_dyld_start –> dyldbootstrap::start –> dyld::_main –> dyld::initializeMainExecutable –> ImageLoader::runInitializers –> ImageLoader::processInitializers –> ImageLoader::recursiveInitialization –> doInitialization –> doModInitFunctions –>libSystem_initializerlibSystem.B.dylib) –> _os_object_initlibdispatch.dylib) –> _objc_init(libobjc.A.dylib)

main()函数的调用

当执行完_objc_init函数之后,就来到了main()函数。那么他是怎样实现这一步的了?
根据dyld源码,在_dyld_start的汇编中,通过注释,可以知道是如何跳转进入main()里面的:
A952FB0B-98FC-4AD4-A7C5-2A9087385ACB.png
当然,我们也能在我们的案例工程里面,通过打印得知。在汇编中,知道是存在rax寄存器里面,那么在工程里面:
74DB7C15-214D-4310-A2D4-3C972D4A76C0.png

  • main也是作为特定的符号,写在dyld里面,不能随意修改。

注解:map_imagesload_images调用情况

根据前面的分析,我们可以知道map_images函数和load_images函数是沟通objcdyld之间的桥梁。需要我们把这里面的细节理清楚。

传入map_imagesload_images,是在objc_objc_init函数里面调用的_dyld_objc_notify_register函数:
未命名文件-5.png
_objc_init函数是由dyld里面的doModInitFunctions函数对接初始化的。与此同时,在dyld中,是由registerObjCNotifiers进行赋值:
未命名文件-2.png

根据赋值情况,先看map_images,就可以找有sNotifyObjCMapped的使用地方了。在dyld中全文索引:
B149789D-C9AA-432B-B90B-9819DCE9494B.png
notifyBatchPartial函数里面,当sNotifyObjCMapped不为NULL时,就直接调用。也就是map_images的调用。
回到registerObjCNotifiers里面,就能看到,notifyBatchPartial函数就在这个函数里面调用了:
E80291EE-55EF-4EB1-9AD1-7B59C5631278.png
load_images也同样在registerObjCNotifiers里面,且依照代码排列顺序,是map_images先执行,load_images后执行。完成注册情况。

我们也可以在objc源码中做个测试,分别在map_images函数和load_images函数上打上断点,看执行的顺序:
F1CA10A2-F049-46D6-B44C-782C98BEA90D.png
先执行map_images,由左侧的堆栈信息可知:_objc_init –> _dyld_objc_notify_register –> notifyBatchPartial –> map_images.
CC2B31DD-7992-4FE9-A070-D42AEF254CBB.png
再执行load_images,由左侧的堆栈信息可知:_objc_init –> _dyld_objc_notify_register –> registerObjCNotifiers –> load_images.

  • 小结:dyld进行加载镜像文件,初始化主程序的时候,会加载libObjc.dylib库,此时,objc会向dyld注册三个方法:map_imagesload_imagesunmap_image,并且map_images会先执行,而load_images后执行!
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享