APP加载过程

422 阅读10分钟

对象原理 --- 调试方式和alloc流程分析

对象原理---isa详解

OC类原理

iOS实例方法cache_t的缓存逻辑

objc_msgSend流程分析

iOS消息发送流程

iOS消息转发流程

一、底层库分类

我们都知道APP依赖很多底层库,底层库到底是什么呢 其实库就是可执行的代码的二进制,可以被操作系统写入内存中的,库可以分为静态库和动态库

那动态库和静态库的区别呢, 先放一个编译过程的图:

image.png

  • 静态库 :
    • (1)类型:.a .lib
    • (2)加载时机:在链接阶段会将汇编生产的目标与引用的库一起链接打包到可执行文件当中
    • (3)优势:开发者可以自己开发定义
  • 动态库:
    • (1)类型: framework .so .dll
    • (2)加载时机:在需要的时候链接到到程序里,是不在可执行文件中,手机里面所以应用程序都是共享一份动态库
    • (3)优势:
      • a、共享内容,节省资源
      • b、减少打包之后的APP的大小
      • c、 通过更新动态库,打到更新程序的目的
      • d、常见的:UIKit、libdispatch、libobjc.dyld

推荐一本书:《程序员的自我修养》

二、库的加载过程

APP启动的流程图:

image.png

我们在main函数出打个断点,然后看下堆栈信息,发现入口处就是libdyld的start函数

image.png

并且我们在一个类里面重写一下load方法,然后加一个断点,发现load方法是在load方法之后执行的

image.png

说明在main函数之前系统做了很多操作,根据堆栈信息我们知道程序入口是从libdyld.dylib`start:开始的 下面从汇编出分析: 首先在main函数处加断点,然后点击左边的start,进入到start的汇编:

image.png

发现啥都没有,考虑到缓存,我们也知道load在main之前,那我们在load 方法处加一个断点,再看看start的汇编:

image.png

我们看到一个start函数dyldbootstrap::start,但是在该函数之前系统依然做了很多操作,从最上面,我们看到是_dyld_start,所以我们下载下来dyld的源码,然后在里面搜索_dyld_start,然后我们看到入口:

image.png

在找到_dyld_start的在arm64下的 实现:

image.png

然后往下找,我们发现

image.png

bl在汇编中是跳转的意思,并且通过注释我们发现,就是跳转到dyldbootstrap::start函数,这个函数跟我们在load断点处看的的star汇编里面一模一样,我们搜索一下该函数里面看看:

image.png

发现在汇编中找不到,那么我们猜测start是C函数或者是C++函数,如果是C函数的话,在C函数中,函数格式一般: 返回值类型+空格+函数名(参数)

考虑到start有好几个参数,所以我们搜一下空格+start(,然后我们就搜索到了start的实现:

uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
				intptr_t slide, const struct macho_header* dyldsMachHeader,
				uintptr_t* startGlue)
{
	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    slide = slideOfMainExecutable(dyldsMachHeader);
    bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
    shouldRebase = true;
#endif
    if ( shouldRebase ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

	// allow dyld to use mach messaging
	mach_init();

	// 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; }
	++;

	// set up random value for stack canary
	__guard_setup();

#if DYLD_INITIALIZER_SUPPORT
	// run all C++ initializers inside dyld
	runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, );
#endif

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

在实现中我们发现最后进入到了dyld::_main函数里面,我们再看一下dyld::_main函数的实现,其实在这个函数里面做了很多操作。例如加载各种库,初始化之类的:

image.png

发现这个函数太长了,就不上源码了,一点点分析

环境变量相关处理

image.png

加载共享缓存

image.png

将dyld本身添加到UUID列表里

image.png

加载镜像文件--reloadAllImags

其实我们想看的是image是怎么加载的,这个image不是图片的意思,是镜像文件的意思

image.png

发现这个地方reloadAllImags instantiateFromLoadedImage在这个函数里内核回映射到主要可执行文件中,我们需要为已经映射到主可执行文件中的文件创建一个ImageLoader * 我们进入到instantiateFromLoadedImage这个函数看看

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

此处是将image加载进来,并且image是从instantiateMainExecutable返回的,然后我们进入到instantiateMainExecutable函数

// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
	//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
	//	sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
	bool compressed;
	unsigned int segCount;
//加载的库的数量
	unsigned int libCount;
	const linkedit_data_command* codeSigCmd;
	const encryption_info_command* encryptCmd;
	sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
	// instantiate concrete class based on content of load commands
	if ( compressed ) 
		return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
	else
#if SUPPORT_CLASSIC_MACHO
		return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
		throw "missing LC_DYLD_INFO load command";
#endif
}

然后我们进入到sniffLoadCommands函数,方法太长也不上源码了

image.png

根据注释,我们知道这个函数是用来确定mach-o文件的

在里面我们可以看到load_command,我们通过mach-o分析应用的可执行文件可以发现在load_command里面是存放所有的库和镜像文件

image.png

image.png

从下面这段这段我们分析得到其实我们的代码文件以及库都编译到了load_command里面,并且在load_command里面是按照分区存储的

image.png

插入库

然后往下看,我们会看到loadInsertedDylib,根据函数名我们就知道这个是插入动态库的意思,并且此处是病历动态库,讲动态库插入到应用中

image.png

//根据上下文将动态库读取成镜像文件,然后在合适地方map到代码中

static void loadInsertedDylib(const char* path)
{

	ImageLoader* image = NULL;
	unsigned cacheIndex;
	try {
		LoadContext context;
		context.useSearchPaths		= false;
		context.useFallbackPaths	= false;
		context.useLdLibraryPath	= false;
		context.implicitRPath		= false;
		context.matchByInstallName	= false;
		context.dontLoad			= false;
		context.mustBeBundle		= false;
		context.mustBeDylib			= true;
		context.canBePIE			= false;
		context.enforceIOSMac		= true;
		context.origin				= NULL;	// can't use @loader_path with DYLD_INSERT_LIBRARIES
		context.rpath				= NULL;
		//根据上下文将动态库读取成镜像文件,然后在合适地方map到代码中
		image = load(path, context, cacheIndex);
	}
	catch (const char* msg) {
		if ( gLinkContext.allowInsertFailures )
			dyld::log("dyld: warning: could not load inserted library '%s' into hardened process because %s\n", path, msg);
		else
			halt(dyld::mkstringf("could not load inserted library '%s' because %s\n", path, msg));
	}
	catch (...) {
		halt(dyld::mkstringf("could not load inserted library '%s'\n", path));
	}
}

到此镜像文件已经处理好了,下一步就是链接了

链接---link

往下走我们会发现link这个函数,从这段代码,我们会发现是变量镜像文件然后拿到ImageLoader,再link到二进制文件中

image.png

link完的下一步就是加载了,往下走,我们会发现调用了initializeMainExecutable函数,就是加载主函数启动程序了

image.png

进入到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]);
}

上面源码我们发现实在便利镜像文件,然后每个镜像文件都会执行一下runInitializers函数,我们进入到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.images[0] = this;
	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函数,比较特别,我们看看你processInitializers实现,在这里面会遍历加载进来的镜像文件image,然后将每个image初始化

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.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
	}
	// If any upward dependencies remain, init them.
	if ( ups.count > 0 )
		processInitializers(context, thisThread, timingInfo, ups);
}

然后进入到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.images[uninitUps.count] = dependentImage;
						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
			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();
}

发现在这个函数里面,doInitialization 再找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,再找doModInitFunctions

image.png

然后在里面找到了libSystemInitialized,并且根据注释可以知道libSystem库必须是第一个初始化的,这样,在其他库调用它的时候它才存在

image.png

然后我们再到systerm库里面找一下initializer函数,发现在initializer函数里面调用了_dyld_initializer函数,并且还调用了libdispatch_init

image.png

然后我们到libdispatch源码里面去找libdispatch_init, 然后在里面找到了_os_object_init函数

image.png

再找一下_os_object_init函数

void
_os_object_init(void)
{
	_objc_init();
	Block_callbacks_RR callbacks = {
		sizeof(Block_callbacks_RR),
		(void (*)(const void *))&objc_retain,
		(void (*)(const void *))&objc_release,
		(void (*)(const void *))&_os_objc_destructInstance
	};
	_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
	const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
	if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
	v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
	if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
	v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
	if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}

在这里面调用了_objc_init函数 我们在_objc_init发现,镜像文件最终是走到_dyld_objc_notify_register函数里的

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();
/*
注册通知,C++函数中如何做到通知
 C++中 通知是按照指针进行回调
 *函数()---函数指针
保存 - libobjc,函数在dyld
怎么回调呢,所以需要在函数参数中加上libobjc的地址
例如:*函数(libobjc地址)
_dyld_objc_notify_register是dyld库的函数
所以这一步其实就是objc通过dyld库的_dyld_objc_notify_register函数将应用的镜像文件链接到相应的库
			 */
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

这样就完成了每个image链接到相应的库里,完成了动态库的加载

总结:

  • 1、动态库的加载是从libdyld.dylib`start:开始,经历了一些汇编然后进入到dyldbootstrap::start函数,然后从dyld::_main开始加载
  • 2、第一步是通过checkEnvironmentVariables和defaultUninitializedFallbackPaths进行环境变量处理
  • 3、再通过checkShareRegionDisable和mapSharedCache加载共享缓存
  • 4、缓存加载完成后会将dyld本身添加到UUID列表里
  • 5、然后开始加载镜像文件image
    • (1)先通过instantiateFromLoadedImag创建一个ImageLoader,用来加载image
    • (2)再通过loadInsertedDylib函数将动态库读取成镜像文件image
    • (3)遍历所有的镜像文件,将镜像文件链接到二进制文件中
    • (4)开始通过initializeMainExecutable初始化程序了
    • (5)遍历ImageLoader(其实一般只有一个),然后每个ImageLoader都进行runInitializers初始化操作
    • (6)然后在processInitializers函数里遍历ImageLoader里的每个image进行recursiveInitialization初始化
    • (7)image初始化中最终会走到_object_init函数里面将相应的镜像文件的函数指针注册到对应dyld的的通知中去
    • (8)关于如何加载类、分类、方法、协议等具体内容下回分解

我们在main.m文件中写上这个函数:

attribute((constructor))void func1(){ printf("sdfsdf"); }

我们会发现系统会自动调用该函数,并且是在main函数之前,这个函数叫做析构函数,析构函数属于C++的函数,会在加载完成之后执行