iOS底层学习——dyld应用程序加载

本篇文章将探究应用程序是如何加载的。我们平时都认为main是程序的入口,是不是这样呢?

1.案例分析

引入一个案例,在ViewController.m中添加一个load方法,程序main.m中添加一个C++函数
ViewControler代码如下:

@interface ViewController ()
@end

@implementation ViewController
+ (void)load{
    NSLog(@"%s", __func__);
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSLog(@"viewDidLoad --- ");
}
@end
复制代码

main.m代码如下:

__attribute__((constructor)) void kcFunc(){
    printf("C... %s \n",__func__);
}
int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"main ...");
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
复制代码

运行程序,根据运行日志,发现程序首先运行了load方法,然后调用了C++函数,最后才进入到main方法中。

image.png

流程:load -> C++ -> main。为什么?main函数难道不是应用程序的入口吗?在main之前到底做了什么?我们一步步来探究。

在分析启动流程之前,先学习一些概念。

2.应用程序编译过程

1.静态库

在链接阶段,会将汇编生成的目标程序与引用的库一起链接打包到可执行文件当中。此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的。例如:.a.lib

  • 优点:编译完成后,库文件实际上就没有作用了,目标程序没有外部依赖,直接就可以运行
  • 缺点:由于静态库可能会有两份,所以会导致目标程序的体积增大,对内存、性能、速度消耗很大

2.动态库

程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入。例如:.so.framwork.dll

  • 优点:减少打包之后app的大小,共享内存,节约资源,更新动态库,达到更新程序
  • 缺点:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行

动静态库

3.编译过程

  • .h、.m、.cpp等源文件->预编译->编译->汇编->链接(动静态库加持)->可执行文件。
  • 源文件:载入.h、.m、.cpp等文件
  • 预处理:替换宏,删除注释,展开头文件,产生.i文件
  • 编译:将.i文件转换为汇编语言,产生.s文件
  • 汇编:将汇编文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件

编译过程

4.动态链接器dyld

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的重要组成部分,在app被编译打包成可执行文件格式的Mach-O文件后,交由dyld负责连接,并加载程序。

  • dyld的作用:加载各个库,也就是image镜像文件,由dyld从内存中读到表中,加载主程序, link链接各个动静态库,进行主程序初始化。

dyld动态链接器

3.dyld入口探索

如何寻找入口?没错,bt!!!
Viewcontrollerload方法添加断点,运行程序。在控制台输入指令bt,查看运行的函数调用堆栈信息。见下图:

image.png

我们找到了程序的入口dyld_start,从左侧的堆栈信息也看到了入口。其实从这里我们已经可以看到一些端倪。

  • 调用流程_dyld_start -> dyldbootstrap::start -> dyld::main -> dyld::initializeMainExecutable -> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> dyld::notifySingle -> load_images -> [ViewController load]

下载dyld源码:dyld源码下载地址,当前最新版本是dyld-852.7 。这部分源码是不能编译的。
打开dyld源码,全局搜索_dyld_start,开始探索程序加载流程。最终在dyldStartup.s文件中搜索到了伪汇编流程。见下图:

image.png

针对不同的环境提供了不同的实现方式,比如,我们现在看到的就是针对arm64真机环境的实现。能力有限,看不懂,但是看注释发现,不论是何种环境,最终都会走到dyldbootstrap::start流程中。这也和我们前面bt看到的函数调用堆栈信息结果是一致的:_dyld_start -> dyldbootstrap::start
继续!下面全局搜索dyldbootstrap。见下图:

image.png

dyldInitialization.cpp文件中搜索到了dyldbootstrap命名空间。其中start方法是我们所需要关注的,通过注释我们可以了解到这是引导dyld的代码。完成dyld的引导,并执行dyldmain函数。

  • 补充一点macho_header是什么?我们知道程序在编译后形成可执行的Mach-O文件,交由dyld连接并加载。也就说dyld加载的就是可执行文件Mach-Omacho-header就是可执行文件的头文件。我们可以通过工具MachOView查看可执行文件信息。

    Mach-O文件

典型的Mach-O文件包含三个区域:

  1. Header:保存Mach-O的一些基本信息,包括平台、文件类型、指令数、指令总大小,dyld标记Flags等等。
  2. Load Commands:紧跟Header,加载Mach-O文件时会使用这部分数据确定内存分布,对系统内核加载器和动态连接器起指导作用。
  3. Data:每个segment的具体数据保存在这里,包含具体的代码、数据等等。

4.dyld main函数分析

进入main函数,600多行代码,这可如何是好?好吧,从return result;进行反推,确定主程序初始化的过程,总结dyld main主要做了哪些工作呢?

  1. 环境变量的配置

根据环境变量设置相应的值并获取当前运行框架。

配置环境变量

  1. 共享缓存

检查是否开启,以及共享缓存是否映射到共享区域,例如UIKitCoreFoundation等。

共享缓存

  1. 主程序初始化

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

主程序初始化

  1. 加载动态库

遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib

插入动态库

  1. 链接主程序

连接主程序

  1. 链接动态库

链接动态库

  1. 执行初始化方法

执行初始化方法

  1. 寻找主程序入口即main函数

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

寻找main函数

下面详解主程序初始化和主程序执行流程

5.主程序初始化

主程序变量为sMainExecutable,它通过instantiateFromLoadedImage函数实现主程序的初始化。查看源码如下:

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

该方法创建了一个ImageLoader实例对象,其创建方法为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
}
复制代码

其作用为创建主程序映射,返回一个ImageLoder类型image对象,即主程序。其中sniffLoadCommands函数获取Mach-O文件load Command相关信息,并对其进行各种校验。

6.主程序执行流程

1.流程分析

通过上面的分析,我们已经跟踪到了以下的程序流程:
_dyld_start -> dyldbootstrap::start -> dyld::main
继续跟踪主程序执行执行流程,进入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]);
}
复制代码

此过程会为所有插入的dylib调用initialzers,进入runInitializers函数,代码如下:

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


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

runInitializers中的核心代码是processInitializers,在processInitializers函数中,对镜像列表调用recursiveInitialization函数进行递归实例化。进入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
			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();
}

复制代码

至此,程序加载流程如下:
_dyld_start -> dyldbootstrap::start -> dyld::main -> initializeMainExecutable -> runInitializers -> processInitializers -> recursiveInitialization(递归)

下面着重分析recursiveInitialization函数。通过注释不难理解下面这段代码是核心。

recursiveInitialization核心代码

看注释,这三个部分是一个渐进的过程:

  1. 将要初始化镜像文件;
  2. 初始化镜像文件;
  3. 完成镜像文件初始化。

这里有两个函数我们重点关注一下:notifySingledoInitialization

2. notifySingle探索

dyld源码全局搜索notifySingle,找到notifySingle函数实现。见下图:

image.png

其中关键代码是1112行的sNotifyObjCInit,调用该函数,并且参数为镜像文件的路径和machO头。再全局搜索sNotifyObjCInit,找到该函数定义初始化的位置。在registerObjCNotifiers函数中,sNotifyObjCInit被设置为init函数。见下图:

sNotifyObjCInit初始化

registerObjCNotifiers函数在何处被调用呢?全局搜索registerObjCNotifiers函数。见下图:

image.png

找到了调用registerObjCNotifiers的地方,_dyld_objc_notify_register函数。全局搜索_dyld_objc_notify_register函数 ,没有找到被调用的地方,但是很多函数都会有这样的一个注释:

// Also note, this function must be called after _dyld_objc_notify_register.
复制代码

也就是说必须在调用_dyld_objc_notify_register之后,才可以执行相关的流程。另外,通过字面了解,objc通知注册,与libobjc.A.dylib源码有什么关系?
解开谜底的时候到了,在libobjc.A.dylib源码中搜索_dyld_objc_notify_register,在objc初始化方法objc_init()中找到了调用。见下图:

image.png

在调用_dyld_objc_notify_register函数时,传入了三个参数。

  • map_images的函数地址
  • load_images函数实现
  • unmap_image函数实现

那么回到dyld源码,不难理解:

  • sNotifyObjCMapped = mapped = &map_images
  • sNotifyObjCInit = init = load_images
  • sNotifyObjCUnmapped = unmapped = unmap_image

也就是说:

objc_init()dyld中注册了三个函数,在dyld进行动静态库加载过程时,当特定环境满足的条件下,这三个函数会调用执行。

2. map_images和load_images调用时机

从上面的流程中我们大概摸清了一个逻辑,就是dyld进行动静态库加载,初始化主程序的时候,会加载libObjc.dylib库,而objc库会向dyld中注册三个方法。那么这三个方法什么时候被调用呢?

dyld不能编译运行,只能从libObjc.dylib入手,同样通过bt来查看运行堆栈。第三个方法是时卸载镜像文件的时候才会被调用,所以我们暂且只研究前面两个方法。我们分别在map_images方法和load_images方法中添加断点,看谁先被执行!运行程序:

  • map_images优先被执行,查看运行堆栈,其流程为:
    _dyld_objc_notify_register --> registerObjCNotifiers --> notifyBatchPartial --> map_images
    image.png

  • 继续运行程序,断点运行到load_images:
    _dyld_objc_notify_register --> registerObjCNotifiers --> load_images
    image.png

至此,可以得出结论,map_images的调用会先于load_images。即实现类后,才能调用+load方法

但是我们依然需要通过源码来验证一下!调用_dyld_objc_notify_register之后,会执行registerObjCNotifiers方法,在该方法中,会循环调用sNotifyObjCInit方法,也就是load_images方法。

image.png

load_images方法调用位置找到了,map_images在哪调用呢?进入notifyBatchPartial函数,在这里成功找到了sNotifyObjCMapped,也就是map_images的调用的位置,见下图:

image.png

总结:dyld进行动静态库加载,初始化主程序的时候,会加载libObjc.dylib库,而objc库会向dyld中注册三个方法。这三个方法分别是:map_imagesload_imagesunmap_image,并且map_images会优先于load_images方法的调用!

问题:我们上面摸清了dyld:main做的一些主要工作,主程序的执行过程,并且分析出_dyld_objc_notify_register实现了libObjc.dylibdyld的函数注册,以及函数的执行!但是依然有些问题没有解决,比如上面案例中c++何时加载的,dyld如何调用objc_init()的?

3.doInitialization函数分析

虽然我们已经知道函数的注册过程,但是依然没有摸清objc_init()被调用的时机。recursiveInitialization中分析了notifySingle之后,我们还有一个doInitialization方法没有探究。doInitialization源码见下图:

image.png

dyld作为动态连接器,进行动态库的加载工作,libobjc.A.dylib库也是它要加载的内容。进入doInitialization函数doImageInit流程中。见下图:

image.png

怎么理解?看注释! libSystem initializer must run first,这个注释提示的很明确,libSystem库必须优先初始化!继续看doInitialization函数doModInitFunctions的实现。见下图:

doModInitFunctions

doModInitFunctions方法加载了所有Cxx文件。如何验证呢?bt一下!依然结合上面的案例,在c++函数中添加断点,查看运行堆栈信息,见下图:

image.png

所以c++调用流程就是:
_dyld_start -> dyldbootstrap::start -> dyld::main -> dyld::initializeMainExecutable -> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> doInitialization -> doModInitFunctions -> c++

4.objc_init()调用时机

回调函数注册流程还没找到呢?但是从上面的分析,至少可以知道和libSystem库有关!那么我们想要找的流程肯定在libSystem库中。下载Libsystem-1292.60.1源码。大海捞针肯定不行,首先要找到个入口。回到我们最熟悉的libobjc.A.dylib源码,在objc_init()中添加断点,bt查看运行堆栈信息,进行反向推导。见下图:

image.png

通过上图可以发现,_os_object_init调用了_objc_init,并且_os_object_init来自于libdispatch.dylib。前面已经分析了,libSystem库需要优先被加载,同时上面的堆栈信息也得到了验证。下面在libSystem库中全局搜索libSystem_initializerlibSystem_initializer源码实现:

image.png

根据上面的堆栈信息,libSystem_initializer->libdispatch_init会进入流程libdispatch_init

extern void libdispatch_init(void); // from libdispatch.dylib
复制代码

查看源码,发现这部分来自libdispatch.dylib库
没啥好说的,下载libdispatch.dylib源码,打开源码搜索libdispatch_init。发现以下流程:

libdispatch_init关键初始化流程

全局搜索_os_object_init(),找到以下源码内容,第一行就调用了_objc_init();方法。

image.png

OK!闭环完成!回调函数的注册流程也打通!总结一下:
objc_init()调用流程:

  • _dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

这个过程完成了上面三个回调函数的注册!
所以可以简单的理解为sNotifySingle这里是添加通知即addObserver_objc_init中调用_dyld_objc_notify_register相当于发送通知,即push,而sNotifyObjcInit相当于通知的处理函数,即selector

7.main函数

继续前面的案例,程序在调用load方法c++函数后,step over过掉断点,走完dyldbootstrap::start后,会寻找main函数

image.png

lldb使用register read读取寄存器,发现rax = main函数。

dyld汇编源码实现

注意:
main是写定的函数,写入内存,读取到dyld;如果修改了main函数的名称,会报错。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享