iOS之深入解析App启动dyld加载流程的底层原理

dyld 简介

一、什么是dyld?

在这里插入图片描述

  • dyld 是英文 the dynamic link editor 的简写,意为动态链接器,是苹果操作系统的一个重要的组成部分。
  • 在 iOS/Mac OSX 系统中,仅有很少量的进程只需要内核就能完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容的填补,这个填补工作就是由 动态链接器dyld 来完成的,也就是符号绑定。
  • 动态链接器 dyld 在系统中以一个用户态的可执行文件形式存在,一般应用程序会在 Mach-O 文件部分指定一个 LC_LOAD_DYLINKER 的加载命令,此加载命令指定了 dyld 的路径,通常它的默认值是 /usr/lib/dyld 。
  • 系统内核在加载 Mach-O 文件时,都需要用 dyld(位于 /usr/lib/dyld )程序进行链接。
  • dyld 源码下载地址:dyld 源码
二、dyld共享缓存
  • 下图展示了应用程序的编译流程,在链接阶段会将应用程序的动态库(.so、.framework、.dylib)和静态库(.a、.lib) 和应用程序进行链接。

在这里插入图片描述

  • 静态库(.a、.lib)可以看成是一堆对象文件 (object files) 的归档。当链接这样一个库到应用中时,静态链接器static linker 将会从库中收集这些对象文件并把它们和应用的对象代码一起打包到一个单独的二进制文件中。这意味着应用的可执行文件大小将会随着库的数目增加而增长。
  • 另外,当应用启动时,应用的代码(包含库的代码)将会一次性地导入到程序的地址空间中去。
  • 动态库(.so、.framework、.dylib)是可以被多个 app 的进程共用的,所以在内存中只会存在一份;如果是静态库,由于每个 app 的 Mach-O 文件中都会存在一份,则会存在多份。
  • 相对静态库,使用动态库可以减少 app 占用的内存大小。动态库不能直接运行,而是需要通过系统的 动态链接加载器dyld 进行加载到内存后执行。
  • dyld 加载时,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被 dyld 映射到内存中,之后,当任何 Mach-O 映像加载时,dyld 首先会检查该 Mach-O 映像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的情况下,这种做法对程序启动性能是有明显提升的。
三、 静态链接和动态链接
  • 静态链接如下:

在这里插入图片描述

  • 如果多个进程需要引用到静态库,在内存中就会存在多份拷贝,如上图中进程1 用到了静态库1、5,进程2也用到了进程1、5,那么静态库1、5在编译期就分别被链接到了进程1和进程2中,假设静态库1占用2M内存,如果有20个这样的进程需要用到静态库1,将占用40M的空间。
    在这里插入图片描述
  • 静态库的特点如下:
    • 静态库对函数库的链接是在编译期完成的。执行期间代码装载速度快。
    • 使可执行文件变大,浪费空间和资源(占空间)。
    • 对程序的更新、部署与发布不方便,需要全量更新。如果 某一个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。
  • 动态库在程序编译时并不会链接到目标代码中,而是在运行时才被载入。不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,避免了空间浪费问题。同时也解决了静态库对程序的更新的依赖,用户只需更新动态库即可。

在这里插入图片描述

  • 动态库在内存中只存在一份拷贝,如果某一进程需要用到动态库,只需在运行时动态载入即可。
    在这里插入图片描述
  • 动态库的特点:
    • 动态库把对一些库函数的链接载入推迟到程序运行时期(占时间)。
    • 可以实现进程之间的资源共享。(因此动态库也称为共享库)
    • 将一些程序升级变得简单,不需要重新编译,属于增量更新。
四、 Load dylibs
  • 从主执行文件header获取到需要加载的所依赖的动态库列表,而header早就被内核映射过。然后它需要找到每个dylib,然后打开文件,读取文件起始位置,确保它是Mach-O文件。接着会找到代码签名并将其注册到内核。然后在dylib文件的每个segment 上调用 mmap()。
  • 应用所依赖的dylib文件可能会再依赖其他dylib,所以dyld所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载100到400 个dylib文件,但大部分都是系统的dylib,它们会被预先计算和缓存起来,加载速度很快。
五、Fix-ups
  • 在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个dylib 调用另一个 dylib,这是就需要很多间接层。
  • Mach-O中有很多符号,有指向当前 Mach-O 的,也有指向其他 dylib 的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?
  • Mach-O中采用了PIC技术,全称是Position Independ code。意味着代码可以被加载到间接的地址上。当你的程序要调用printf的时候,会先在 __DATA 段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分:rebasing和binding。

在这里插入图片描述

  • Rebasing:在镜像内部调整指针的指向; Binding: 将指针指向镜像外部的内容。
    • 之所以需要Rebase,是因为刚刚提到的 ASLR 使得地址随机化,导致起始地址不固定,另外由于 Code Sign,导致不能直接修改 Image。
    • Rebase的时候只需要增加对应的偏移量即可。(待Rebase的数据都存放在__LINKEDIT中,可以通过MachOView查看:Dynamic Loader Info -> Rebase Info)
    • Binding就是将这个二进制调用的外部符号进行绑定的过程。 比如我们objc代码中需要使用到NSObject,即符号OBJC_CLASS$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要Binding这个操作将对应关系绑定到一起。
    • Rebase解决了内部的符号引用问题,而外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的。
六、dyld 2 和 dyld 3

在这里插入图片描述

  • 在 iOS 13之前,所有的第三方App都是通过dyld 2来启动 App 的,主要过程如下:
    • 解析 Mach-O的Header 和 Load Commands,找到其依赖的库,并递归找到所有依赖的库;
    • 加载Mach-O文件;
    • 进行符号查找;
    • 绑定和变基;
    • 运行初始化程序;
  • dyld 3被分为了三个组件:
    • 一个进程外的 Mach-O 解析器预先处理了所有可能影响启动速度的search path、@rpaths 和环境变量,然后分析Mach-O的Header和依赖,并完成了所有符号查找的工作。最后将这些结果创建成一个启动闭包,这是一个普通的daemon进程,可以使用通常的测试架构;
    • 一个进程内的引擎,用来运行启动闭包。这部分在进程中处理验证启动闭包的安全性,然后映射到dylib之中,再跳转到main函数不需要解析 Mach-O 的 Header 和依赖,也不需要符号查找。
    • 一个启动闭包缓存服务系统App的启动闭包被构建在一个Shared Cache 中,我们甚至不需要打开一个单独的文件。对于第三方的App,我们会在App安装或者升级的时候构建这个启动闭包。在iOS、tvOS、watchOS中,这一切都是App启动之前完成的。在macOS上,由于有Side Load App,进程内引擎会在首次启动的时候启动一个daemon进程,之后就可以使用启动闭包启动了。

Mach-O 文件

关于 Mach-O 文件请参考我之前的博客:iOS逆向之MachO文件

虚拟内存

一、什么是虚拟内存?
  • 虚拟内存是一层间接寻址 。
  • 虚拟内存是在物理内存上建立的一个逻辑地址空间。建立在进程和物理内存之间的中间层,它向上(应用)提供了一个连续的逻辑地址空间,向下隐藏了物理内存的细节。
  • 虚拟内存被划分为一个个大小相同的Page(64位系统上是16KB),提高管理和读写的效率。 Page又分为只读和读写的Page。
二、虚拟内存的应用
  • 虚拟内存解决的是管理所有进程使用 物理RAM 的问题。通过添加间接层来让每个进程使用 逻辑地址空间,它可以映射到RAM 上的某个物理页上。这种映射 不是一对一 的,逻辑地址可能映射不到 RAM 上,也有可能有多个逻辑地址映射到同一个物理RAM 上。
  • 虚拟内存使得逻辑地址可以没有实际的物理地址,也可以让多个逻辑地址对应到一个物理地址。
    • 针对第一种情况(逻辑地址可能映射不到 RAM ):在应用执行的时候,它被分配的逻辑地址空间都是可以访问的,当应用访问一个逻辑Page,而在对应的物理内存中并不存在的时候,这时候就发生了一次Page fault。当Page fault发生的时候,会中断当前的程序,在物理内存中寻找一个可用的Page,然后从磁盘中读取数据到物理内存,接着继续执行当前程序。
    • 而第二种情况(多个逻辑地址映射到同一个物理RAM 上)就是多进程共享内存。
  • 对于文件可以不用一次性读入整个文件,可以使用分页映射 mmap()的方式获取。也就是把文件 某个片段 映射到进程逻辑内存的 某个页 上。当某个想要读取的页没有在内存中,就会触发 page fault,内核只会读入那一页,实现文件的 懒加载。也就是说 【Mach-O】 文件中的 __TEXT 段可以映射到多个进程,并可以懒加载,且进程之间 共享内存。
  • __DATA 段是可读写的。这里使用到了Copy-On-Write 技术,简称【COW】。 也就是多个进程共享一页内存空间时,一旦有进程要做写操作,它会先将这页内存内容复制一份出来,然后重新映射逻辑地址到新的RAM 页上。也就是这个进程自己拥有了那页内存的拷贝。这就涉及到了 clean/dirty page 的概念。dirty page 含有进程自己的信息,而clean page 可以被内核重新生成(重新读磁盘)。多以 dirty page 的代价大于 clean page。

exec()

  • exec() 是一个系统调用。
  • 系统内核把应用程序映射到新的地址空间,且每次起始位置都是随机的(因为ASLR)。并将起始位置到0x000000 这段范围的进程权限都标记为不可读写不可执行。
  • 如果是32 位进程,这个范围至少是4kb ;如果是64位进程则至少是4GB。
  • NULL指针引用和指针截断误差都是会被它捕获,这个范围也叫做 PAGEZERO。

在这里插入图片描述

App 加载流程

该部分主要分析 dyld 的加载流程,即在 App 启动启动执行 main 函数之前,iOS 的底层还做了什么工作。

一、抛砖引玉
  • 新建一个 project,在 ViewController 中重写 load 方法,在 main 中添加一个 C++ 方法,即kcFunc,请问它们的打印先后顺序是什么?
	@implementation ViewController
	
	+ (void)load {
    
    
	    NSLog(@"%s",__func__);
	}
	
	- (void)viewDidLoad {
    
    
	    [super viewDidLoad];
	    
	}
	@end
	__attribute__((constructor)) void kcFunc(){
    
    
	    printf("来了 : %s \n",__func__);
	}
  • 运行程序,查看 load、kcFunc、main 的打印顺序,下面是打印结果,通过结果可以看出其顺序是 load --> C++方法 --> main,如下所示:
	2020-10-10 04:20:01.959255+0800 [64334:2554630] +[ViewController load]
	来了 : kcFunc 
	2020-10-10 04:20:01.959903+0800 [64334:2554630] Hello
  • 为什么会出现这样的顺序?按照常规的思维理解,main 不是程序的入口函数吗?为什么不是 main 最先执行?下面根据这个问题,我们来探索在执行到 main 函数之前,到底还做了什么?
二、App的启动点
  • 在应用程序的入口 main() 函数之前打上断点,查看堆栈信息,可以看出:先于 main 函数调用的是 start,同时这一流程是由libdyld.dylib库执行的 ,如下所示:

在这里插入图片描述

  • 为了看到更详细的调用过程,在项目中的 ViewController 的 + (void) load 方法打上断点。详细堆栈信息如下:

在这里插入图片描述

三、_dyld_start
  • App启的时候,调用流程是从 _dyld_start 开始的,在 dyld 源码中搜索_dyld_start ,可以在在 dyldStartup.s 文件中找到,我们发现它是用汇编实现的,尽管在不同架构下有所区别,但都是会调用 dyldbootstrap 命名空间下的 start 方法,这和上面的堆栈顺序也是相同的。
	call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
  • dyldbootstrap::start 源码实现如下:
	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中,主要过程为:
    • ① 使用全局变量之前,对dyld进行rebase操作,以修复为 real pointer 来运行;
    • ② 设置参数和环境变量;
    • ③ 读取 app二进制文件 Mach-O 的header 得到偏移量 appSlide,然后调用dyld 命名空间下的_main 方法。
四、dyld::_main
  • dyld::_main 是 dyld 的入口,内核加载了 dyld,然后跳转到 _dyld_start 来设置一些寄存器的值之后进入这个方法,返回 _dyld_start所跳转到的目标程序的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;
}
  • 环境变量配置:根据环境变量设置相应的值以及获取当前运行架构:
	// Grab the cdHash of the main executable from the environment
	// 从环境变量中获取主要可执行文件的 cdHash
	uint8_t mainExecutableCDHashBuffer[20];
	const uint8_t* mainExecutableCDHash = nullptr;
	if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
		mainExecutableCDHash = mainExecutableCDHashBuffer;
#if __MAC_OS_X_VERSION_MIN_REQUIRED
    if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
    
    
		pruneEnvironmentVariables(envp, &apple);
		// set again because envp and apple may have changed or moved
		setContext(mainExecutableMH, argc, argv, envp, apple);
	}
	else
#endif
	{
    
    
		// 环境变量配置
		// 检查设置的环境变量
		checkEnvironmentVariables(envp);
		// 如果DYLD_FALLBACK为nil,设置为默认值
		defaultUninitializedFallbackPaths(envp);
	}
#if __MAC_OS_X_VERSION_MIN_REQUIRED
	if ( gProcessInfo->platform == PLATFORM_IOSMAC ) {
    
    
		gLinkContext.rootPaths = parseColonList("/System/iOSSupport", NULL);
		gLinkContext.iOSonMac = true;
		if ( sEnv.DYLD_FALLBACK_LIBRARY_PATH == sLibraryFallbackPaths )
			sEnv.DYLD_FALLBACK_LIBRARY_PATH = sRestrictedLibraryFallbackPaths;
		if ( sEnv.DYLD_FALLBACK_FRAMEWORK_PATH == sFrameworkFallbackPaths )
			sEnv.DYLD_FALLBACK_FRAMEWORK_PATH = sRestrictedFrameworkFallbackPaths;
	}
	else if ( ((dyld3::MachOFile*)mainExecutableMH)->supportsPlatform(dyld3::Platform::driverKit) ) {
    
    
		gLinkContext.driverKit = true;
		gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
	}
#endif
	// 如果设置了DYLD_PRINT_OPTS环境变量,则打印参数
	if ( sEnv.DYLD_PRINT_OPTS )
		printOptions(argv);
	// 如果设置了DYLD_PRINT_ENV环境变量,则打印环境变量
	if ( sEnv.DYLD_PRINT_ENV ) 
		printEnvironmentVariables(envp);

	// Parse this envirionment variable outside of the regular logic as we want to accept
	// this on binaries without an entitelment
#if !TARGET_OS_SIMULATOR
	if ( _simple_getenv(envp, "DYLD_JUST_BUILD_CLOSURE") != nullptr ) {
    
    
#if TARGET_OS_IPHONE
		const char* tempDir = getTempDir(envp);
		if ( (tempDir != nullptr) && (geteuid() != 0) ) {
    
    
			// Use realpath to prevent something like TMPRIR=/tmp/../usr/bin
			char realPath[PATH_MAX];
			if ( realpath(tempDir, realPath) != NULL )
				tempDir = realPath;
			if (strncmp(tempDir, "/private/var/mobile/Containers/", strlen("/private/var/mobile/Containers/")) == 0) {
    
    
				sJustBuildClosure = true;
			}
		}
#endif
		// If we didn't like the format of TMPDIR, just exit.  We don't want to launch the app as that would bring up the UI
		if (!sJustBuildClosure) {
    
    
			_exit(EXIT_SUCCESS);
		}
	}
#endif

	if ( sJustBuildClosure )
		sClosureMode = ClosureMode::On;
	// 获取当前运行环境的架构信息
	getHostInfo(mainExecutableMH, mainExecutableSlide);
  • 共享缓存:检查是否开启了共享缓存,以及共享缓存是否映射到共享区域,例如UIKit、CoreFoundation等;
	// load shared cache
	// 共享缓存
	// 检查共享缓存是否开启(iOS中必须开启)
	checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
	if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
    
    
#if TARGET_OS_SIMULATOR
		if ( sSharedCacheOverrideDir)
		// 检查共享缓存是否映射到共享区域
			mapSharedCache();
#else
		mapSharedCache();
#endif
	}

	// If we haven't got a closure mode yet, then check the environment and cache type
	if ( sClosureMode == ClosureMode::Unset ) {
    
    
		// First test to see if we forced in dyld2 via a kernel boot-arg
		if ( dyld3::BootArgs::forceDyld2() ) {
    
    
			sClosureMode = ClosureMode::Off;
		} else if ( inDenyList(sExecPath) ) {
    
    
			sClosureMode = ClosureMode::Off;
		} else if ( sEnv.hasOverride ) {
    
    
			sClosureMode = ClosureMode::Off;
		} else if ( dyld3::BootArgs::forceDyld3() ) {
    
    
			sClosureMode = ClosureMode::On;
		} else {
    
    
			sClosureMode = getPlatformDefaultClosureMode();
		}
	}
  • 主程序的初始化:调用 instantiateFromLoadedImage 函数实例化了一个 ImageLoader 对象;
		CRSetCrashLogMessage(sLoadingCrashMessage);
		// instantiate ImageLoader for main executable
		// 主程序初始化,加载可执行文件,并生成一个ImageLoader对象
		sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
		gLinkContext.mainExecutable = sMainExecutable;
		gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
  • 插入动态库:遍历 DYLD_INSERT_LIBRARIES 环境变量,调用 loadInsertedDylib 加载;
		// load any inserted 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 
		// 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 动态库:
		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);
				}
			}
		}
  • 弱符号绑定:
		// apply interposing to initial set of images
		for(int i=0; i < sImageRoots.size(); ++i) {
    
    
			sImageRoots[i]->applyInterposing(gLinkContext);
		}
		ImageLoader::applyInterposingToDyldCache(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);
			}
		}
		
		// <rdar://problem/12186933> do weak binding only after all inserted images linked
		sMainExecutable->weakBind(gLinkContext);
		gLinkContext.linkingMainExecutable = false;

		sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);

		CRSetCrashLogMessage("dyld: launch, running initializers");
  • 执行初始化方法:
	#if SUPPORT_OLD_CRT_INITIALIZATION
		// Old way is to run initializers via a callback from crt1.o
		if ( ! gRunInitializersOldWay ) 
			initializeMainExecutable(); 
	#else
		// run all initializers
		initializeMainExecutable(); 
	#endif
  • 寻找主程序入口即 main 函数:从Load Command读取LC_MAIN入口,如果没有,就读取LC_UNIXTHREAD,这样就来到了日常开发中熟悉的main函数;
五、notifySingle 函数
  • 全局搜索notifySingle(函数,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
	if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
    
    
		uint64_t t0 = mach_absolute_time();
		dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
		(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		uint64_t t1 = mach_absolute_time();
		uint64_t t2 = mach_absolute_time();
		uint64_t timeInObjC = t1-t0;
		uint64_t emptyTime = (t2-t1)*100;
		if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
    
    
			timingInfo->addTime(image->getShortName(), timeInObjC);
		}
	}
  • 全局搜索sNotifyObjCInit,发现没有找到实现,有赋值操作:
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;

	// call 'mapped' function with all images mapped so far
	try {
    
    
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
    
    
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
	for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
    
    
		ImageLoader* image = *it;
		if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
    
    
			dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
			(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		}
	}
}
  • 搜索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);
}
  • 在objc4-781源码中搜索_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images,而load_images会调用所有的+load方法。所以综上所述,notifySingle是一个回调函数;
六、load 函数加载
  • 通过objc源码中 _objc_init 源码实现,进入 load_images 的源码实现;看看是否 load_images 中调用了所有的 load 函数。
  • 进入 load_images 的源码实现如下:
void
load_images(const char *path __unused, const struct mach_header *mh) {
    
    
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
    
    
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
    
    
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
  • 进入 call_load_methods 源码实现,可以发现其核心是通过 do-while 循环调用 +load 方法:
void call_load_methods(void)
{
    
    
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
    
    
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
    
    
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}
  • 进入call_class_loads源码实现,了解到这里调用的load方法证实前文提及的类的load方法:
static void call_class_loads(void)
{
    
    
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    
    
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
    
    
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}
  • load_images调用了所有的load函数,以上的源码分析过程正好对应堆栈的打印信息:
    在这里插入图片描述
七、doInitialization 函数
  • 走到objc的_objc_init函数,发现走不通了,我们回退到recursiveInitialization递归函数的源码实现,发现我们忽略了一个函数doInitialization:

在这里插入图片描述

  • 进入doInitialization函数的源码实现:

在这里插入图片描述

  • 进入doImageInit源码实现,其核心主要是for循环加载方法的调用,这里需要注意的一点是,libSystem的初始化必须先运行:

在这里插入图片描述

  • 进入doModInitFunctions源码实现,这个方法中加载了所有Cxx文件:

在这里插入图片描述

dyld 加载流程总结

dyld 整体加载流程,如下图所示:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Forever_wj/article/details/109042442