前言

一、编译和加载流程简述

二、dyld简介

三、__dyld_start

四、dyldbootstrap::start

五、dyld::_main (核心流程)这里主要分析dyld2的流程

六、总结


最近很多同事在找工作的过程中,都被问到了应用程序运行的时候,main函数之前都做了哪些事情。这就涉及到了dyld的加载流程。因为dyld的知识点确实太多了,我们就先从比较核心的流程来分析一下。

首先我们创建一个demo工程,然后加入一个c++函数,并设置函数属性为constructor,然后在ViewController里面加入+(void)load方法。

__attribute__((constructor))void myTest() {
    NSLog(@"1");
}

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        NSLog(@"3");
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
@implementation ViewController
+ (void)load {
    NSLog(@"2");
}
- (void)viewDidLoad {
    [super viewDidLoad];
}
@end

运行之后,打印顺序为 213。那么为什么加载顺序为 +(void)load -> c++ -> main呢?带着这个问题来进行探索。

一、编译和加载流程简述

基本的编译流程如下:

源文件(.h .m .cpp )-> 预编译 -> 编译 (llvm-cl) -> 汇编 (llvm-as) -> 链接(llvm-ld)动静态库 -> 可执行文件 EXEC。
关于动静态库,可以查看强化篇中具体对静态库和动态库的分析。

基本的加载流程如下:

App启动 -> _dyld_start -> 通过dyld加载 libSytem -> libDispath -> libObjc -> runtime注册回调函数 -> 加载Image -> 执行 map_Images / load_Images (继续加载新image) -> 调用main函数。
从基本的加载流程上可以看出,一切都要从dyld进行分析。

二、dyld简介

首先我们在demo工程里面的+(void)load方法中打一个断点,然后看下它的堆栈:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1 6.1
  * frame #0: 0x00000001014acc97 dyldDemo`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
    frame #1: 0x00007fff20181ff2 libobjc.A.dylib`load_images + 1439
    frame #2: 0x00000001014c0e2c dyld_sim`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 425
    frame #3: 0x00000001014cfba5 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 437
    frame #4: 0x00000001014cdec7 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 191
    frame #5: 0x00000001014cdf68 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
    frame #6: 0x00000001014c126b dyld_sim`dyld::initializeMainExecutable() + 199
    frame #7: 0x00000001014c5f56 dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4789
    frame #8: 0x00000001014c01c2 dyld_sim`start_sim + 122
    frame #9: 0x0000000106a38a8e dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2093
    frame #10: 0x0000000106a36168 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 1198
    frame #11: 0x0000000106a30224 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 450
    frame #12: 0x0000000106a30025 dyld`_dyld_start + 37
(lldb) 

可以看到,一切都是从_dyld_start开始。

应用被编译打包成可执行文件格式为Mach-O的文件之后,启动的时候,会把 dyld 加载到进程的地址空间里,然后把后续的启动过程交由dyld负责动态链接,加载程序。

dyld 主要有两个版本:dyld2 和 dyld3,他们的区别简单来说如下:

(1)dyld2 是从 iOS 3.1 引入,一直持续到 iOS 12。dyld2 有个比较大的优化是加入了dyld shared cache(共享缓存)

(2)dyld3 的最重要的特性就是启动闭包,闭包里包含了启动所需要的缓存信息,从而提高启动速度。

我下载了一份dyld-852的源码:源码下载地址


三、__dyld_start

#if __arm__
	.text
	.align 2
__dyld_start:
	mov	r8, sp		// save stack pointer
	sub	sp, #16		// make room for outgoing parameters
	bic     sp, sp, #15	// force 16-byte alignment

	// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
    .... 省略
#endif /* __arm__ */

通过源码查看__dyld_start,_dyld_start是用汇编写的。
当我们点开一个应用,系统内核会开启一个进程,然后由dyld开始加载这个可执行文件。

四、dyldbootstrap::start

紧跟着的核心流程是:
call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)。
我们继续进入dyldbootstrap::start中(如果是模拟器会走start_sim)。

#define DYLD_INITIALIZER_SUPPORT  0
//
//  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.
//
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>
    //发出 kdebug 跟踪点以指示 dyld 引导程序已启动
    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
    //如果内核必须滑动 dyld,我们需要修复负载敏感位置 // 我们必须在使用任何全局变量之前执行此操作
    rebaseDyld(dyldsMachHeader);

	// kernel sets up env pointer to be just past end of agv array
    //内核将 env 指针设置为刚好经过 agv 数组的末尾
	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

	_subsystem_init(apple);

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

在这个函数中的主要流程如下:
1、执行rebaseDyld,根据计算出来的ASLR的slide来重定位macho 以及 初始化 , 允许 dyld 使用 mach 消息传递。
2、执行__guard_setup,栈溢出保护。
3、调用dyld的main函数:dyld::_main。

其中macho_header 对应的就是一下结构体:

struct mach_header_64 {
	uint32_t	magic;		/* mach magic number identifier */
	cpu_type_t	cputype;	/* cpu specifier */
	cpu_subtype_t	cpusubtype;	/* machine specifier */
	uint32_t	filetype;	/* type of file */
	uint32_t	ncmds;		/* number of load commands */
	uint32_t	sizeofcmds;	/* the size of all the load commands */
	uint32_t	flags;		/* flags */
	uint32_t	reserved;	/* reserved */
};

ASLR今后我们会具体分析。

五、dyld::_main (核心流程),这里主要分析dyld2的流程

dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
这个函数的代码太多了,显然一行一行分析估计要炸,所以我们只分析核心部分。

1、配置相关环境变量(平台信息,版本信息,文件路径以及主机信息等等)

2、设置上下文信息 setContext

3、检测进程是否受限,在上下文中做出对应处理configureProcessRestrictions,检测环境变量checkEnvironmentVariables。

4、加载共享缓存库

    // iOS cannot run without shared region
	// load shared cache
	checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
	if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
		if ( sSharedCacheOverrideDir)
			mapSharedCache(mainExecutableSlide);
#else
		mapSharedCache(mainExecutableSlide);
#endif

		// If this process wants a different __DATA_CONST state from the shared region, then override that now
		if ( (sSharedCacheLoadInfo.loadAddress != nullptr) && (gEnableSharedCacheDataConst != sharedCacheDataConstIsEnabled) ) {
			uint32_t permissions = gEnableSharedCacheDataConst ? VM_PROT_READ : (VM_PROT_READ | VM_PROT_WRITE);
			sSharedCacheLoadInfo.loadAddress->changeDataConstPermissions(mach_task_self(), permissions,
																		 (gLinkContext.verboseMapping ? &dyld::log : nullptr));
		}
	}

(1)检测共享缓存禁用状态 checkSharedRegionDisable (ios下不会被禁用)
(2)加载共享缓存库 mapSharedCache -> loadDyldCache (模拟器仅支持加载到当前进程) 。

static void mapSharedCache(uintptr_t mainExecutableSlide)
{
	dyld3::SharedCacheOptions opts;
	opts.cacheDirOverride	= sSharedCacheOverrideDir;
	opts.forcePrivate		= (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);
#if __x86_64__ && !TARGET_OS_SIMULATOR
	opts.useHaswell			= sHaswell;
#else
	opts.useHaswell			= false;
#endif
	opts.verbose			= gLinkContext.verboseMapping;
    // <rdar://problem/32031197> respect -disable_aslr boot-arg
    // <rdar://problem/56299169> kern.bootargs is now blocked
	opts.disableASLR		= (mainExecutableSlide == 0) && dyld3::internalInstall(); // infer ASLR is off if main executable is not slid
    //sSharedCacheLoadInfo 全局变量,加载的共享动态库缓存在这里保存
	loadDyldCache(opts, &sSharedCacheLoadInfo);

	// update global state
	if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
		gLinkContext.dyldCache 								= sSharedCacheLoadInfo.loadAddress;
		dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
		dyld::gProcessInfo->sharedCacheSlide                = sSharedCacheLoadInfo.slide;
		dyld::gProcessInfo->sharedCacheBaseAddress          = (unsigned long)sSharedCacheLoadInfo.loadAddress;
		sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
		dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, sSharedCacheLoadInfo.path, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
	}
}

bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
    results->loadAddress        = 0;
    results->slide              = 0;
    results->errorMessage       = nullptr;

#if TARGET_OS_SIMULATOR
    // simulator only supports mmap()ing cache privately into process
    return mapCachePrivate(options, results);
#else
    if ( options.forcePrivate ) {
        // mmap cache into this process only
        return mapCachePrivate(options, results);
    }
    else {
        // fast path: when cache is already mapped into shared region
        //快速路径:当缓存已经映射到共享区域时
        bool hasError = false;
        if ( reuseExistingCache(options, results) ) {
            hasError = (results->errorMessage != nullptr);
        } else {
            // slow path: this is first process to load cache
            hasError = mapCacheSystemWide(options, results);
        }
        return hasError;
    }
#endif
}

满足options.forcePrivate 这个条件的话,只加载当前进程。
reuseExistingCache如果缓存已经加载不再处理,如果第一次加载执行mapCacheSystemWide这个函数。
得出结论:动态库的共享缓存是最先被加载的(我们自己开发的动态库不可以)

5. 实例化主程序 , 检测可执行程序格式

	// instantiate ImageLoader for main executable
		sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
//内核在 dyld 获得控制之前映射到主可执行文件中。 我们需要为已经映射到主可执行文件中的 // 制作一个 ImageLoader*。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
		ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
		addImage(image);
		return (ImageLoaderMachO*)image;
}

这个函数其实就是在加载我们的macho文件,然后实例化创建一个imageLoader,并添加到全局变量sAllImages中,最后返回一个主程序实例。

static void addImage(ImageLoader* image)
{
	// add to master list
    allImagesLock();
	//添加到sAllImages
	sAllImages.push_back(image);
    allImagesUnlock();
    ....省略
}

其中真正实例化主程序是用ImageLoaderMachO作用域中sniffLoadCommands 这个函数去做的 。

void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
											unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
											const linkedit_data_command** codeSigCmd,
											const encryption_info_command** encryptCmd)
{
    *compressed = false;
	*segCount = 0;
	*libCount = 0;
	*codeSigCmd = NULL;
	*encryptCmd = NULL;
	/*
	...省略掉.
	*/
	// fSegmentsArrayCount is only 8-bits
	if ( *segCount > 255 )
		dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);

	// fSegmentsArrayCount is only 8-bits
	if ( *libCount > 4095 )
		dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);

	if ( needsAddedLibSystemDepency(*libCount, mh) )
		*libCount = 1;
}

这个函数就是根据 Load Commands 来加载主程序。
核心点如下 :
compressed (是否压缩) -> 根据 LC_DYLD_INFO_ONYL 来决定。
segCount 段命令数量 , 最大不能超过 255 个。
libCount 依赖库数量 , LC_LOAD_DYLIB (Foundation / UIKit ..) , 最大不能超过 4095 个。
codeSigCmd , 应用签名。
encryptCmd , 应用加密信息。
经过以上步骤 , 主程序的实例化就已经完成了 .

6. 加载插入所有动态库

// load any inserted libraries
		if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
			for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
				loadInsertedDylib(*lib);
		}

根据 DYLD_INSERT_LIBRARIES 环境变量来决定是否需要加载插入的动态库。如果需要插入,会循环去根据动态库的路径,加入动态库。

7.链接主程序

link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

8.链接插入的所有动态库

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

9.弱绑定主程序

在链接所有插入的镜像文件后进行弱绑定主程序 (不需要浪费性能再去强引用,也更能方便释放)

sMainExecutable->weakBind(gLinkContext);

至此 , 配置环境变量 -> 加载共享缓存 -> 实例化主程序 -> 加载动态库 -> 链接主程序 -> 链接动态库 就已经完成了

10.初始化主程序

initializeMainExecutable(); 

11. 通知dyld此进程进入 main()

//通知任何监控进程此进程即将进入 main()
notifyMonitoringDyldMain();

六、总结:dyld 启动的大致流程:

1、配置环境变量

2、加载共享缓存

3、实例化主程序

4、加载插入的动态库

5、链接主程序

6、链接插入的动态库

7、弱绑定主程序

8、初始化主程序

9、通知dyld可以进入main函数