iOS-底层原理10 App启动到main方法前都做了什么

2,218 阅读9分钟

启动之前(编译阶段)

编译阶段主要是把代码编译成可执行文件。主要流程如下:

  • 加载源文件:载入.h、.m、.cpp等文件
  • 预编译:替换宏,删除注释,展开头文件,产生.i文件
  • 编译:将.i文件转换为汇编语言,产生.s文件
  • 汇编:将汇编文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件 以下是编译的流程

编译流程.png

编译阶段静态库动态库的处理

静态库

在编译阶段会直接拷贝一份到目标程序中,所以,程序运行后,程序对静态库文件没有依赖性。但是会造成目标程序的体积增大和运行速度减慢等问题

动态库

在编译阶段目标程序只会存储指向动态库的引用,在程序运行时才被载入。

  • 优点:减少目标程序的大小。一份文件可以多处使用,而且不需要复制到目标程序中。
  • 缺点:动态载入消耗部分性能。程序对动态库文件有很强的依赖性。

2251862-c74ad3d9bfccfcdc.png

探索启动之前

在探索程序启动时执行的方法之前。我们去找程序的调用的入口。首先在main函数中增加断点,如下图所示,可以看出在执行main之前调用了dyld中的start方法。

截屏2021-07-19 下午5.31.29.png

  • main方法执行之前调用的方法加上断点进行查看。先查看方法调用的顺序。如下所示:load方法->C++方法->main方法

截屏2021-07-19 下午5.40.58.png

  • load方法的断点(C++方法一样)。 B1521A0E-5903-449D-B316-622C2C3826E0.png
  • 所以我们可以到dyld源码中查看是否是程序的入口。并且在启动时做了什么事情。

启动过程

在load函数中打上断点,可以看到XCode左边的堆栈中的方法列表。其中 _dyld_start是程序的入口。而且调用的方法大多数是dyld源码中的方法,下载dyld源码查看内部实现。

B1521A0E-5903-449D-B316-622C2C3826E0.png

在源码中查看到dyld主要做了如下事情

  • App运行环境的配置。
    • 环境变量配置
    • 共享缓存设置
  • 主程序的初始化
  • 对需要使用的库进行插入和绑定
    • 插入动态库
    • 链接主程序
    • 链接动态库
    • 弱符号绑定
  • 初始化方法
  • 寻找程序入口(main方法),并且返回 app启动流程.png

启动时dyld具体代码实现

  • dyld源码中搜索_dyld_start,可以看到汇编方法如下所示。_dyld_start的入口同时会调用一个C++方法dyldbootstrap::start

WeChatf3882f2b228dff44752ace4f3b5ce593.png

  • 源码中搜索dyldbootstrap,再在这个文件中查找start方法,其核心是返回值的调用了dyld的main函数,其中macho_header是Mach-O的头部,而dyld加载的文件就是Mach-O类型的,即Mach-O类型是可执行文件类型,由四部分组成:Mach-O头部、Load Command、section、Other Data,可以通过MachOView查看可执行文件信息。

WeChatf182096543ed2188c63e1490882d0c95.png

App运行环境的配置

  • 环境变量配置 根据环境变量设置相应的值以及获取当前运行架构
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue)
{
...
...
// 创建主程序cdHash的空间
uint8_t mainExecutableCDHashBuffer[20];
// 从环境中获取可执行主要文件CDHash
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
	unsigned bufferLenUsed;
	if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
	mainExecutableCDHash = mainExecutableCDHashBuffer;
}
        ...
        ...
// 获取当前运行环境架构的信息
getHostInfo(mainExecutableMH, mainExecutableSlide);
{
	//【第一步--------环境变量配置】
	checkEnvironmentVariables(envp);
	defaultUninitializedFallbackPaths(envp);
}
     ... 
     ...
}
  • 共享缓存设置 检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKitCoreFoundation
...
...
////【第二步--------共享缓存】
// 检查共享缓存是否开启 在iOS中必须开启
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
        if ( sSharedCacheOverrideDir)
	//检查共享缓存是否映射到共享区
	mapSharedCache(mainExecutableSlide);
#else
	//检查共享缓存是否映射到共享区
	mapSharedCache(mainExecutableSlide);
#endif
...
...
// 如果还没有关闭模式 请检查环境和共享缓存的类型
// If we haven't got a closure mode yet, then check the environment and cache type
if ( sClosureMode == ClosureMode::Unset ) {...}
...
...

主程序的初始化

调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象

...
...
// instantiate ImageLoader for main executable
//【第三步--------主程序的初始化】
// 加载可执行文件,并且生产一个LoadedImage实例对象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
...
...

对需要使用的库进行插入和绑定

  • 插入动态库 遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载
...
...
// load any inserted libraries
//【第四步--------插入动态库】
// 加载所有DYLD_INSERT_LIBRARIES指定的库
if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
	for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
		loadInsertedDylib(*lib);
}
// 记录插入库的数量以便进行统一搜索
// record count of inserted libraries so that a flat search will look at
// 插入库,然后main和其他
// inserted libraries, then main, then others.
sInsertedDylibCount = sAllImages.size()-1;
...
...
  • 链接主程序
...
...
//【第五步--------link主程序】
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
	gLinkContext.bindFlat = true;
	gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
...
...
  • 链接动态库
...
...
//【第六步--------link动态库】
// link any inserted libraries
// 链接插入的动态库
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
// 在链接主程序后执行链接动态库,保证所有的dylibs在使用前(如:libSystem)都被插入
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);
        }
    }
}
...
...
  • 弱符号绑定
...
...
// 绑定并且通知主要可执行文件,现在插入已经被注册
// Bind and notify for the main executable now that interposing has been registered
uint64_t bindMainExecutableStartTime = mach_absolute_time();
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);

// 绑定并通知已插入镜像已经被注册
// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
    for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
	ImageLoader* image = sAllImages[i+1];
	image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
	}
}
		
// 弱符号绑定
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
...
...

初始化方法

// run all initializers
//【第八步--------执行初始化程序】
initializeMainExecutable(); 

寻找程序入口(main方法),并且返回

从Load Command读取LC_MAIN入口,如果没有,就读取LC_UNIXTHREAD,这样就来到了日常开发中熟悉的main函数了

...
...
{
//【第九步--------寻找主程序入口】
// find entry point for main executable
// 寻找入口并且执行
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
if ( result != 0 ) {
// main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
    // 主要可执行文件使用LC_MAIN,我们需要在libdyld中使用helper来调用main()
    if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
        *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
    else
        halt("libdyld.dylib support not present for LC_MAIN");
}
else {
    // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
    // 主要可执行文件使用LC_UNIXTHREAD
    result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
    *startGlue = 0;
    }
}
...
...

启动时中的细节

初始化方法initializeMainExecutable

  • 点进该方法,主要是执行所有插入的dylibs的初始化
void initializeMainExecutable()
{
	// record that we've reached this step
	gLinkContext.startedInitializingMainExecutable = true;

	// run initialzers for any inserted dylibs
	// 执行所有插入的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]);
	//当程序退出时注册cxa_atexit处理程序
	// 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]);
}
  • 查看dylib的初始化方法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.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);
}
  • 在这里,对镜像表中的所有镜像执行recursiveInitialization ,创建一个未初始化的向上依赖新表。如果依赖中未初始化完毕,则继续执行processInitializers,直到全部初始化完毕。
// <rdar://problem/14412057> upward dylib initializers can be run too soon
// To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
// have their initialization postponed until after the recursion through downward dylibs
// has completed.
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);
}
  • 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) {...}
			
			// record termination order
			if ( this->needsTermination() )
				context.terminationRecorder(this);
			//让objc知道将要初始化这个镜像
			// 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);
			//初始化镜像 会执行_objc_init方法
			// initialize this image
			bool hasInitializers = this->doInitialization(context);
			//让anyone知道将要初始化这个镜像
			// 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 ) {...}
		}
		catch (const char* msg) {...}
	}
	
	recursiveSpinUnLock();
}
  • 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镜像被映射,取消映射,和初始化时 被调用的注册处理器。我们可以在 objc源码中的_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
}
  • 其中load_images是一个调用所有类的+load的方法。

总结

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

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

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

借鉴:

iOS 应用程序加载
dyld加载流程