手撕iOS底层17 -- 应用程序加载流程(完善更新)

1,370 阅读18分钟

有坑位的小伙伴联系我.微信bgwx7788


这篇文章主要记录dyld的启动加载流程,探索main函数之前,系统底层干了哪些活?

0x00 - main方法和load以及__attribute__((constructor))修饰方法的执行顺序?

新建一个iOSdemo工程,在ViewController.m里重写load方法, 在main.m写一个__attribute__((constructor))修饰的c++函数

// ViewController.m
+ (void)load{
    NSLog(@"%s",__func__);
}
// main.m
__attribute__((constructor)) void kcFunc(){
//    printf("来了 : %s \n",__func__);
    NSLog(@"%s", __func__);
}

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    NSLog(@"main 来了");
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

通过运行结果查看:执行顺序为:loadkcFuncmain,即 先执行load方法,再执行被__attribute((constructor))修饰的c/c++方法,最后执行main函数。

按照正常思维,main函数不是程序的入口嘛?为什么最后执行呢?

那么问题来了,

  • 为什么main函数最后执行?
  • load方法和被修饰的c++方法什么时候执行?
  • main执行前都做了些什么?

围绕这些问题,展开我们本篇文章的探索之路~~~😄


0x01 - 编译过程

分析之前,先普及一下常识,也就是编译过程

在项目编译时,会把项目中编写的源文件编译成一个一个对应的.o的目标文件,最后打包成Mach-O的可执行文件(.o文件的集合)。

编译过程主要分为以下几步:

  1. 源文件.mcpp等文件
  2. 预处理:把源文件的带#的宏替换,比如#include#define
  3. 编译:把预处理之后输出的结果编译object file目标文件
  4. 链接: 把.o文件集合链接Linker以及引用到的其它静态库标准库文件,生成可执行文件mac平台Mach-O格式, windows平台exe格式等。

0x02 - Mach-O

**WWDC - 2016**有关于Mach-O的详细介绍,以及Optimizing start up time的实操。并且详细介绍了app的启动流程

通过编译出来的产物是Mach-O文件,这个格式的文件是可以被OSXiOS平台可以理解的,就像exe可以被Windows平台识别一样,Mach-OMach Object文件的缩写。

0x03 - 静态库与动态库

app运行,会依赖底层的库 ,比如一些动态库UIKitFoundation等。还有一些静态库

App using static libraries App using dynamic libraries

静态库

格式有.a .lib framework

静态链接的时候会完整的复制到可执行文件中, 多个程序使用,就会有多份静态库,因为参与链接,会复制到可执行文件中, 所以使可执行文件体积增大,对内存、速度、性能影响消耗很大

动态库

格式有.dylib .so .framework

动态库在程序链接时不会复制到执行文件中,程序运行时由系统动态加载到内存,供程序使用,系统只加载一次,多个程序共用,节省内存

Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

dlopen 会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen 也可以选择是立刻解析所有引用还是滞后去做。dlopen 打开动态库后返回的是引用的指针,dlsym 的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用。

Dylib是动态库,分为动态链接库动态加载库

动态加载库: 当需要的时候再用dlopen等通过代码或命令的方式去加载,即程序运行中需要库再去加载库到内存中,类似懒加载。

动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着加载到内存中,即随着程序启动而启动。属于程序依赖的启动库

使用 dyld 加载动态库,有两种方式:有程序启动加载时绑定和符号第一次被用到时绑定(1.动态链接库 ,2 动态加载库)。为了减少启动时间,大部分动态库使用的都是符号第一次被用到时再绑定的方式。

加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 Clang Attribute 的 constructor 修饰函数。每个函数、全局变量和类都是通过符号的形式定义和使用的,当把目标文件链接成一个 Mach-O 文件时,链接器在目标文件和动态库之间对符号做解析处理。

⚠️ Xcode6之后支持创建动态库工程,创建的动态库是为App Extension之间所需要使用的。

那么动态库不参与链接, 那么它是怎么启动的?并且在程序需要动态库的时候是怎么识别?所以要做这些工作,需要一个动态连接器,也就是著名的dyld,下面开始分析下dyld的流程。

0x04 - dyld加载流程

dyld(the dynamic link editor),苹果的动态链接器,Apple OS的重要组成部分,WWDC - 2017- 413详细介绍了dyld的历史与特性

0x05 - app启动的起始点

在之前的demo中,知道启动最先执行load方法,所以先从load方法入手。

通过堆栈信息查看到程序的起点是dyld里的_dyld_start,所幸dyld是开源的,可以去Apple Open Source dyld这里下载源码查看

打开新下载的源码,在其中搜索_dyld_start

从左侧的搜索结果可以看到,__dyld_start在汇编中,随便找一个架构可以看其中走向,以arm64为例,尽管不同架构下有所区别,但通过汇编的注释发现都调用dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)方法,这和上边的堆栈顺序也是符合的,在源码里搜索dyldbootstrap这个namespace,在这里找start函数。

dyldbootstrap::start

//
//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//  这是dyld的引导代码。这个工作通常是由dyld和crt完成,在dyld中, 手动执行这个操作
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
				const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

    // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    rebaseDyld(dyldsMachHeader);

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

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

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

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

系统的启动应用入口是dyldbootstrap::start,首先通过汇编调用dylddyldbootstrap::start,其核心主要是调用了dyld::_main函数,其中macho_headerMach-O的头部信息, Mach-O可以通过MachOView查看由四部分组成,Mach-O头部Load CommandsectionOther Data

dyld::_main源码

精简代码如下

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue)
{
	......

    // 设置运行环境,可执行文件准备工作
	......

	// load shared cache   加载共享缓存
	mapSharedCache();
    ......

reloadAllImages:

    ......
	// instantiate ImageLoader for main executable 加载可执行文件并生成一个ImageLoader实例对象
	sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

	......

	// load any inserted libraries   加载插入的动态库
	if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
		for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
			loadInsertedDylib(*lib);
	}
		
	// link main executable  链接主程序
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

	......
	// link any inserted libraries   链接所有插入的动态库
	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);
			}
		}
	}

    ......
    //弱符号绑定
	sMainExecutable->weakBind(gLinkContext);
		
	sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);

	......
    // run all initializers   执行初始化方法
	initializeMainExecutable(); 

	// notify any montoring proccesses that this process is about to enter main()
	notifyMonitoringDyldMain();

    return result;
}

开始进入dyld::_main方法实现,整个方法体大概600多行代码,整体主要做了这么几件事情:

  1. 设置运行环境,配置环境变量,根据环境变量设置相应的值以及获取当前运行架构
  2. 加载共享缓存 -> load share cache
  3. 主程序image的初始化mainExecutable
  4. 插入动态库 loadInsertedDylib
  5. link主程序
  6. link插入的动态库
  7. weakBind
  8. initializeMainExecutable()
  9. 返回 main函数

对其中主要几件事情进行分析:

  • 第三步:从Mach-O格式可执行文件读取数据实例化成image,加入到allImages

sMainExecutable代表主程序的变量,通过instantiateFromLoadedImage初始化image,然后将image加入到allImages中。

// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
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";
}

来到instantiateMainExecutable源码,其中主要的方法sniffLoadCommands读取Mach-O可执行文件的信息,比如CPU Type ,Load Commands个数以及Size of Load Commands.

// 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方法用于确定这个mach-O文件是否具有经典或压缩的LINKEDIT以及具有的段数
	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方法

  • 第五步开始链接sMainExecutableimage,再链接插入的动态库
    		// link main executable
    		// 第五步 link主程序
    		gLinkContext.linkingMainExecutable = true;
    #if SUPPORT_ACCELERATE_TABLES
    		if ( mainExcutableAlreadyRebased ) {
    			// previous link() on main executable has already adjusted its internal pointers for ASLR
    			// work around that by rebasing by inverse amount
    			sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
    		}
    #endif
    		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(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
    		// 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
    		// 第六步 link动态库
    		// 链接插入的动态库,链接主执行文件后执行此操作,这样插入的dylib,(例如libSystem)插入的dylib不会在程序使用的dylib的前面
    		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
    				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);
    				}
    			}
    		}
    
第八步 : initializeMainExecutable,开始初始化之前加入的iamge, 主要遍历各个image,执行runInitializers方法

开始初始化链接加入的images,再initializeMainExecutable()函数中,主要递归调用runInitializers

🤮先贴一张程序启动调用堆栈图

runInitializers方法中,核心方法processInitializers处理

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

进入processInitializers方法,主要是对动态库的依赖调用``recursiveInitialization`递归初始化

// <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.
	// 在image列表中所有image调用递归实例化,以建立未初始化的向上依赖关系新列表
	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);
}

然后就进入了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
			// 让objc知道要初始化这个镜像了
			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(); // 递归解锁
}

会有动态库依赖的递归调用初始化,主要是研究的代码是notifySingledoInitialization

  • 先看notifySingle函数

notifySingle函数中,重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());,在源码中全局搜索sNotifyObjCInit,并没有找到实现,直发发现了对其的赋值操作

然后搜索registerObjCNotifiers方法,看其在哪里调用并对其赋值了。

接着搜索_dyld_objc_notify_register函数,并没有找到它的实现,找到这么一坨东西,

注意第一句only for use by objc runtime,意思这玩意只提供给runtime使用,因为runtimelibObjc中,所以在我们的libObjC中搜索

_objc_init中找到调用赋值_dyld_objc_notify_register(&map_images, load_images, unmap_image);

load_images赋值给了sNotifyObjCInit, load_images会调用所有类的+load方法。即在objc这层注册了回调函数,在dyld调用这些回调函数。

  • load函数加载

load_images通过call_load_methods调用+load方法

进入call_load_methods方法,其核心通过do-while循环调用call_class_loads

来到call_class_loads方法,明显的看到在这里调用了load方法, 携带俩个默认的参数

+ load方法总结:

  1. load_images函数调用了所有的load方法,通过堆栈对应了以上的分析。


既然在_objc_init调用了dyld的注册回调函数_dyld_objc_notify_register,那什么时候调用_objc_init,因为libObjc也是一个image,即libObjc什么时候初始化,也就会调用了_objc_init这个函数,形成闭环。

回到dyldrecursiveInitialization函数,有一个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);
}

主要做了doImageInitdoModInitFunctions

doImageInit中, 核心就是for循环调用image的初始化方法,但是需要注意的是libSystem库需要第一个初始化,

⚠️libSystem initializer must run first

void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
	if ( fHasDashInit ) {
		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) {
			switch (cmd->cmd) {
				case LC_ROUTINES_COMMAND:
					Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
#if __has_feature(ptrauth_calls)
					func = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)func, ptrauth_key_asia, 0);
#endif
					// <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
						dyld::throwf("-init function in image (%s) that does not link with libSystem.dylib\n", this->getPath());
					}
					if ( context.verboseInit )
						dyld::log("dyld: calling -init function %p in %s\n", func, this->getPath());
					{
						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);
					}
					break;
			}
			cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
		}
	}
}

为什么libSystem要第一个初始化,是因为在libObjc库的初始化是在libDispatch库执行的, 而libDispatch库是在libSystem库初始化后执行。

libObjc库的初始化函数中_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();
    runtime_init();
    exception_init();
    cache_init();
    _imp_implementationWithBlock_init();
    //调用dyld的注册回调函数
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

objc库中调用dyld库中的函数_dyld_objc_notify_register,因为我们是在可编译的objc-781的环境, 在这里打个断点,看下堆栈流程

粉色横线标记的就是我们上边梳理的流程, 现在从objc_init开始自顶向下的梳理,流程为

_objc_init <-- _os_object_init <--- libdispatch_init <----libSystem_initializer

所幸这些库都是开源的,在苹果的Apple Open Source 下载libSystemlibdispatch这些库来一探究竟

首先打开系统要求第一个加载的libSystem,搜索libSystem_initializer

// libsyscall_initializer() initializes all of libSystem.dylib
// <rdar://problem/4892197>
__attribute__((constructor))
static void
libSystem_initializer(int argc,
		      const char* argv[],
		      const char* envp[],
		      const char* apple[],
		      const struct ProgramVars* vars)
{
  .....
    
    
	// TODO: Move __malloc_init before __libc_init after breaking malloc's upward link to Libc
	// Note that __malloc_init() will also initialize ASAN when it is present
	__malloc_init(apple);
	_libSystem_ktrace_init_func(MALLOC);

#if TARGET_OS_OSX
	/* <rdar://problem/9664631> */
	__keymgr_initializer();
	_libSystem_ktrace_init_func(KEYMGR);
#endif

	_dyld_initializer();
	_libSystem_ktrace_init_func(DYLD);

	libdispatch_init();
	_libSystem_ktrace_init_func(LIBDISPATCH);

#if !TARGET_OS_DRIVERKIT
	_libxpc_initializer();
	_libSystem_ktrace_init_func(LIBXPC);

#if CURRENT_VARIANT_asan
	setenv("DT_BYPASS_LEAKS_CHECK", "1", 1);
#endif
#endif // !TARGET_OS_DRIVERKIT

	// must be initialized after dispatch
	_libtrace_init();
	_libSystem_ktrace_init_func(LIBTRACE);
  ....
}

// system library initialisers
extern void mach_init(void);			// from libsystem_kernel.dylib
extern void __libplatform_init(void *future_use, const char *envp[], const char *apple[], const struct ProgramVars *vars);
extern void __pthread_init(const struct _libpthread_functions *libpthread_funcs, const char *envp[], const char *apple[], const struct ProgramVars *vars);	// from libsystem_pthread.dylib
extern void __malloc_init(const char *apple[]); // from libsystem_malloc.dylib
extern void __keymgr_initializer(void);		// from libkeymgr.dylib
extern void _dyld_initializer(void);		// from libdyld.dylib
extern void libdispatch_init(void);		// from libdispatch.dylib
extern void _libxpc_initializer(void);		// from libxpc.dylib
extern void _libsecinit_initializer(void);        // from libsecinit.dylib
extern void _libtrace_init(void);		// from libsystem_trace.dylib
extern void _container_init(const char *apple[]); // from libsystem_containermanager.dylib
extern void __libdarwin_init(void);		// from libsystem_darwin.dylib

放了部分的源码, 这里很明显的可以看到libSystem初始化的时候, 会初始化其它的库,比如_dyld_initializer(); 这个是dyld库的初始化调用,因为dyld也是一个动态库,

在启动一个可执行文件的时候,系统内核做完环境的初始化,就把控制权交给dyld去执行加载和链接。

__malloc_init(apple);,这其中就有libdispatch_init();,因为libdispatch属于libdispatch库,所以打开libdispatch工程,搜索它的初始化代码

void
libdispatch_init(void)
{
  ...
  _dispatch_hw_config_init();
	_dispatch_time_init();
	_dispatch_vtable_init();
	_os_object_init();
	_voucher_init();
	_dispatch_introspection_init();
}

在这里就可以看到_os_object_init();调用,因为这个函数也属于libdispatch库,所以查到

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_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable ---> ImageLoader::runInitializers ---> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization ---> doInitialization --->doModInitFunctions----> libSystem_initializer 属于libSystem.B.dylib ---> libdispatch_init 属于libdispatch.dylib ---> _os_object_init 属于 libdispatch.dylib --> _objc_init 属于libobjc.A.dylib

  1. dyld加载到开始链接mainExecutable的时候,递归调用recursiveInitialization函数。
  2. 这个函数第一次运行,会进行libSystem的初始化,会走到doInitialization -> doModInitFunctions -> libSystem_initializer
  3. libSystem的初始化,会调用libdispatch_initlibdispatch_init会调用_os_object_init_os_object_init会调用_objc_init
  4. _objc_init中注册保存了map_imagesload_imagesunmap_images的函数地址
  5. 注册完回到dyldrecursiveInitialization递归下一次调用,例如libObjc,当libObjc来到recursiveInitialization调用时,会触发保存的load_images回调,就调用了load_images函数

  • 还有一个问题是在doModInitFunctions方法是怎么调用libSystem的初始化函数,即libSystem_initializer,因为在doModInitFunctions没有看到显示调用。但是在doModInitFunctions有这么个一个判断
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context){
  .....
    for (uint32_t i = 0; i < cmd_count; ++i) {
      if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
        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);;
            for (size_t j=0; j < count; ++j) {
              Initializer func = inits[j];
              if ( ! dyld::gProcessInfo->libSystemInitialized ) {
                const char* installPath = getInstallPath();
                if ( (installPath == NULL) || (strcmp(installPath, LIBSYSTEM_DYLIB_PATH) != 0) )
                  dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
                }
              bool haveLibSystemHelpersBefore = (dyld::gLibSystemHelpers != NULL);
              {
                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;
              }
            }
          }
        }
      }
    }
}

这个函数,就是找镜像中的flags字段匹配S_MOD_INIT_FUNC_POINTERS的section,这种类型的section只存储了入口地址。

使用otool或者MachOView工具看一下libSystem.dylib

通过烂苹果打开,可以直观的看到libSystem_initializer函数地址以及名称,它们在被镜像加载时调用。

对于 libSystem.dylib 而言,该 section 名为__mod_init_func(一般都是这个名字),存储了一个函数指针,该函数指针恰好对应_libSystem_initializer符号。当libSystem_initializer被调用时,dyld 会对gProcessInfo->libSystemInitialized进行标记,表示 libSystem 已经被初始化。

补充说明

如何确保自定义函数被编译到__mod_init_func中呢?一种常见的做法是对函数标记__attribute__((constructor))

libSystem 的初始化是一个内部行为,dyld 是如何知道它被初始化的呢?libSystem 是一个特殊的 dylib,默认情况下会被所有可执行文件所依赖,dyld 为它单独提供了一个 API:_dyld_initializer,当 libSystem 被初始化时,会调用该函数,进而 dyld 内部就知道了 libSystem 被初始化了。

0x06 - main函数的返回

走到这里,开始进入我们主执行程序的入口点,从dyld怎么走到应用的main函数

dylddyldbootstrap::start函数走完后,之后的处理通过汇编查看, 在LLDB通过读取寄存器,rax的值是我们main函数的地址了,然后查看dyld__dyld_start:汇编实现。

在执行完dyldbootstrap::start后, 会call to main(),说明main函数是底层写定函数,如果我们改了main函数名字,会ld报错

0x07 - 收获总结

  1. 编译阶段有了链接器,所以可以根据模块功能在不同文件里写代码,每个文件都可以独立编译成Mach-O文件,编译器可以根据修改范围来减少编译,提高编译速度
  2. 通过链接机制,也能够明白, 源文件越多,链接器链接Mach-O文件所需要绑定遍历的操作就会越多,拖慢编译速度。

欢迎大佬留言指正😄,码字不易,觉得好给个赞👍 有任何表达或者理解失误请留言交流;共同进步

参考资料

iOS里的动态库和静态库

深入剖析iOS动态链接库

Dynamic Library Programming

Objective-C Runtime 分析

深入剖析 iOS 编译 Clang / LLVM

从 dyld 到 runtime

dyld与ObjC

iOS 底层 - 从头梳理 dyld 加载流程