13-dyld

802 阅读7分钟

前言

dyld(the dynamic link editor)动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。

一、dyld加载流程

dyld的加载流程,在我之前的文章iOS应用程序加载大致流程分析中已经分析了(当时版本是dyld-750.6)。

dyld最新版本 👉 Apple Source中搜索dyld-832.7.3并下载。

现在我们来看看最新版的代码的不同之处。

1.1dyld-832.7.3的优化点

1.1.1 主程序可执行文件

    // Grab the cdHash of the main executable from the environment
    // 从环境中获取主可执行文件的cdHash值
    uint8_t mainExecutableCDHashBuffer[20];
    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);

区别于dyld-750.6,新版本使用hexStringToBytes()替换hexToBytes(),同时调用getHostInfo获取主程序Header,Slide(ASLR的偏移值)等信息。

1.1.2 ptrauth_calls

#if __has_feature(ptrauth_calls)
	// Check and see if kernel disabled JOP pointer signing (which lets us load plain arm64 binaries)
	if ( const char* disableStr = _simple_getenv(apple, "ptrauth_disabled") ) {
		if ( strcmp(disableStr, "1") == 0 )
			sKeysDisabled = true;
	}
	else {
		// needed until kernel passes ptrauth_disabled for arm64 main executables
		if ( (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_V8) || (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_ALL) )
			sKeysDisabled = true;
	}
#endif

针对ptrauth_calls的处理,也是新版中才有的👇

如果加载普通的arm64二进制文件 👉 检查内核是否禁用了JOP指针签名,如果未禁用,那么需要直到内核为arm64主可执行文件中传值ptrauth_disabled

1.1.3 platform 的处理

dyld-750.6中针对所有镜像文件,会关联platform ID信息,这样调试器就能告诉进程当前的平台类型(是iOS 还是 MacOS),相关代码👇

	// Set the platform ID in the all image infos so debuggers can tell the process type
	// FIXME: This can all be removed once we make the kernel handle it in rdar://43369446
    // 这块处理可以被删除,一旦我们在`内核`中处理了`platform`
	if (gProcessInfo->version >= 16) {
		__block bool platformFound = false;
		((dyld3::MachOFile*)mainExecutableMH)->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) {
			if (platformFound) {
				halt("MH_EXECUTE binaries may only specify one platform");
			}
			gProcessInfo->platform = (uint32_t)platform;
			platformFound = true;
		});
		if (gProcessInfo->platform == (uint32_t)dyld3::Platform::unknown) {
			// There were no platforms found in the binary. This may occur on macOS for alternate toolchains and old binaries.
			// It should never occur on any of our embedded platforms.
#if __MAC_OS_X_VERSION_MIN_REQUIRED
			gProcessInfo->platform = (uint32_t)dyld3::Platform::macOS;
#else
			halt("MH_EXECUTE binaries must specify a minimum supported OS version");
#endif
		}
	}

#if __MAC_OS_X_VERSION_MIN_REQUIRED
	// Check to see if we need to override the platform
	const char* forcedPlatform = _simple_getenv(envp, "DYLD_FORCE_PLATFORM");
	if (forcedPlatform) {
		if (strncmp(forcedPlatform, "6", 1) != 0) {
			halt("DYLD_FORCE_PLATFORM is only supported for platform 6");
		}
		const dyld3::MachOFile* mf = (dyld3::MachOFile*)sMainExecutableMachHeader;
		if (mf->allowsAlternatePlatform()) {
			gProcessInfo->platform = PLATFORM_IOSMAC;
		}
	}

	// if this is host dyld, check to see if iOS simulator is being run
    // 如果是模拟器调试的情况:实际使用的是主机的dyld进行加载,那么此时要确保模拟器正处于运行的状态
	const char* rootPath = _simple_getenv(envp, "DYLD_ROOT_PATH");
	if ( (rootPath != NULL) ) {
		// look to see if simulator has its own dyld
        // 模拟器是否有自己的dyld
		char simDyldPath[PATH_MAX]; 
		strlcpy(simDyldPath, rootPath, PATH_MAX);
		strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
		int fd = my_open(simDyldPath, O_RDONLY, 0);
		if ( fd != -1 ) {
			const char* errMessage = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue, &result);
			if ( errMessage != NULL )
				halt(errMessage);
			return result;
		}
	}
	else {
        // 在路径"DYLD_ROOT_PATH"中未找到dyld,如果是模拟器运行的程序,那么报错 👉 "DYLD_ROOT_PATH not set"
		((dyld3::MachOFile*)mainExecutableMH)->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) {
			if ( dyld3::MachOFile::isSimulatorPlatform(platform) )
				halt("attempt to run simulator program outside simulator (DYLD_ROOT_PATH not set)");
		});
	}
#endif

而在新版本dyld-832.7.3中并没有判断if (gProcessInfo->version >= 16),而是直接处理👇

	// Set the platform ID in the all image infos so debuggers can tell the process type
	// FIXME: This can all be removed once we make the kernel handle it in rdar://43369446
	// The host may not have the platform field in its struct, but there's space for it in the padding, so always set it
    // 主机可能在其结构中没有platform 字段,但在填充中有空间,所以总是设置它
    {
      // 这里的代码和dyld-750.6一样
    }

1.1.4 arm64e的处理

新版本的arm64e处理👇

#if TARGET_OS_OSX && __has_feature(ptrauth_calls)
	// on Apple Silicon macOS, only Apple signed ("platform binary") arm64e can be loaded
    // 在Apple Silicon macOS上,只有Apple签名(“平台二进制”)arm64e可以被加载
	sOnlyPlatformArm64e = true;

	// internal builds, or if boot-arg is set, then non-platform-binary arm64e slices can be run
    // 内部构建,或者如果设置了boot-arg,则只有非平台二进制的arm64e架构可以运行
	if ( const char* abiMode = _simple_getenv(apple, "arm64e_abi") ) {
		if ( strcmp(abiMode, "all") == 0 )
			sOnlyPlatformArm64e = false;
	}
#endif

1.1.5 dyld3:闭包方式的加载流程

新版本中,dyld3的闭包模式处理加载流程,增加了对useClosures == "2"这种情况的判👇

	// AMFI相关(Apple Mobile File Integrity苹果移动文件保护)
	// Check if we should force dyld3.  Note we have to do this outside of the regular env parsing due to AMFI
	if ( dyld3::internalInstall() ) {
		if (const char* useClosures = _simple_getenv(envp, "DYLD_USE_CLOSURES")) {
			if ( strcmp(useClosures, "0") == 0 ) {
				sClosureMode = ClosureMode::Off;
			} else if ( strcmp(useClosures, "1") == 0 ) {
	#if !__i386__ // don't support dyld3 for 32-bit macOS
				sClosureMode = ClosureMode::On;
				sClosureKind = ClosureKind::full;
	#endif
			} else if ( strcmp(useClosures, "2") == 0 ) {
				sClosureMode = ClosureMode::On;
				sClosureKind = ClosureKind::minimal;
			} else {
				dyld::warn("unknown option to DYLD_USE_CLOSURES.  Valid options are: 0 and 1\n");
			}

		}
	}

同时,增加了针对PLATFORM_IOS(iOS系统下)ARM64架构的处理👇

#if TARGET_OS_OSX
	switch (gProcessInfo->platform) {
#if (TARGET_OS_OSX && TARGET_CPU_ARM64)
		case PLATFORM_IOS:
			sClosureMode = ClosureMode::On; // <rdar://problem/56792308> Run iOS apps on macOS in dyld3 mode
			[[clang::fallthrough]];
#endif
		case PLATFORM_MACCATALYST:
			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;
			break;
		case PLATFORM_DRIVERKIT:
			gLinkContext.driverKit = true;
			gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
			break;
	}
#endif

dyld3对闭包的构建,也做了优化,封装buildClosureCachePath函数,交由闭包自己去处理路径👇

#if !TARGET_OS_SIMULATOR
	if ( _simple_getenv(envp, "DYLD_JUST_BUILD_CLOSURE") != nullptr ) {
#if TARGET_OS_IPHONE
		char tempClosurePath[PATH_MAX];
		if ( dyld3::closure::LaunchClosure::buildClosureCachePath(sExecPath, envp, false, tempClosurePath) )
			sJustBuildClosure = true;
#endif
		// If the env vars for the data contain look wrong, don't want to launch the app as that would bring up the UI
		if (!sJustBuildClosure) {
			_exit(EXIT_SUCCESS);
		}
	}
#endif

而旧版本的是直接在_main函数中处理,其实这里并不关心👇

#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

1.1.6 boot-args处理

新版本中特别对 boot-args进行了处理,分别针对macOSiOS模拟器👇

#if !TARGET_OS_SIMULATOR
	if ( getpid() == 1 ) {
		// Get the value as set by the boot-args
        // 获取关于boot-args相关的参数值
		uint64_t commPageValue = 0;
		size_t commPageValueSize = sizeof(commPageValue);
		if ( sysctlbyname("kern.dyld_flags", &commPageValue, &commPageValueSize, nullptr, 0) != 0 ) {
			// Try again with the old name
			// TODO: Remove this when we are always on new enough kernels
			sysctlbyname("kern.dyld_system_flags", &commPageValue, &commPageValueSize, nullptr, 0);
		}

		commPageValue &= CommPageBootArgMask;
		// logToConsole("dyld: got comm page flags 0x%llx\n", commPageValue);

		// If we are PID 1 (launchd) and on macOS, then we should check if the simulator support dylibs
		// are roots or not.
		// If they are not roots at launchd time, and the file system is read-only, then we know for sure
		// they will not be roots later
        // 系统静态库root权限的处理
#if DYLD_SIMULATOR_ROOTS_SUPPORT
		bool fileSystemIsWritable = true;

		// logToConsole("dyld: in launchd\n");
		struct statfs statBuffer;
		int statResult = statfs("/", &statBuffer);
		if ( statResult == 0 ) {
			if ( !strcmp(statBuffer.f_fstypename, "apfs") ) {
				if ( (statBuffer.f_flags & (MNT_RDONLY | MNT_SNAPSHOT)) == (MNT_RDONLY | MNT_SNAPSHOT) ) {
					// logToConsole("dyld: got statfs flags 0x%llx\n", statBuffer.f_flags);
					fileSystemIsWritable = false;
				}
			}
		} else {
			int error = errno;
			logToConsole("dyld: could not stat '/', errno = %d\n", error);
		}

		// If the file system is read-only, then we can check now whether any of the simulator support
		// dylibs are roots
		bool libsystemKernelIsRoot 		= false;
		bool libsystemPlatformIsRoot 	= false;
		bool libsystemPThreadIsRoot 	= false;
		if ( !fileSystemIsWritable && (sSharedCacheLoadInfo.loadAddress != nullptr)) {
			dyld3::closure::FileSystemPhysical fileSystem;
			libsystemKernelIsRoot 	= !dyld3::RootsChecker::uuidMatchesSharedCache("/usr/lib/system/libsystem_kernel.dylib",
																				   &fileSystem, sSharedCacheLoadInfo.loadAddress);
			libsystemPlatformIsRoot = !dyld3::RootsChecker::uuidMatchesSharedCache("/usr/lib/system/libsystem_platform.dylib",
																				   &fileSystem, sSharedCacheLoadInfo.loadAddress);
			libsystemPThreadIsRoot 	= !dyld3::RootsChecker::uuidMatchesSharedCache("/usr/lib/system/libsystem_pthread.dylib",
																				   &fileSystem, sSharedCacheLoadInfo.loadAddress);
		}
		commPageValue |= (fileSystemIsWritable ? CommPageFlags::fileSystemCanBeModified : CommPageFlags::None);
		commPageValue |= (libsystemKernelIsRoot ? CommPageFlags::libsystemKernelIsRoot : CommPageFlags::None);
		commPageValue |= (libsystemPlatformIsRoot ? CommPageFlags::libsystemPlatformIsRoot : CommPageFlags::None);
		commPageValue |= (libsystemPThreadIsRoot ? CommPageFlags::libsystemPThreadIsRoot : CommPageFlags::None);
#endif // DYLD_SIMULATOR_ROOTS_SUPPORT

		logToConsole("dyld: setting comm page to 0x%llx\n", commPageValue);
		if ( sysctlbyname("kern.dyld_flags", nullptr, 0, &commPageValue, sizeof(commPageValue)) != 0 ) {
			// Try again with the old name
			// TODO: Remove this when we are always on new enough kernels
			sysctlbyname("kern.dyld_system_flags", nullptr, 0, &commPageValue, sizeof(commPageValue));
		}
	}

#if DYLD_SIMULATOR_ROOTS_SUPPORT
	// Set the roots checker to the state from the comm page
    // comm page的状态的root权限检查
	{
		uint64_t dyldFlags = *((uint64_t*)_COMM_PAGE_DYLD_SYSTEM_FLAGS);
		bool fileSystemCanBeModified = dyldFlags & CommPageFlags::fileSystemCanBeModified;
		bool libsystemKernelIsRoot = dyldFlags & CommPageFlags::libsystemKernelIsRoot;
		bool libsystemPlatformIsRoot = dyldFlags & CommPageFlags::libsystemPlatformIsRoot;
		bool libsystemPThreadIsRoot = dyldFlags & CommPageFlags::libsystemPThreadIsRoot;
		sRootsChecker.setFileSystemCanBeModified(fileSystemCanBeModified);
		sRootsChecker.setLibsystemKernelIsRoot(libsystemKernelIsRoot);
		sRootsChecker.setLibsystemPlatformIsRoot(libsystemPlatformIsRoot);
		sRootsChecker.setLibsystemPThreadIsRoot(libsystemPThreadIsRoot);
	}
#endif // DYLD_SIMULATOR_ROOTS_SUPPORT

#endif // !TARGET_OS_SIMULATOR

1.1.7 bootToken

		// <rdar://60333505> bootToken is a concat of boot-hash kernel passes down for app and dyld's uuid
        // bootToken是引导【app哈希值 + dyld的uuid的哈希值】,由内核传递
		uint8_t bootTokenBufer[128];
		unsigned bootTokenBufferLen = 0;
		if ( const char* bootHashStr = _simple_getenv(apple, "executable_boothash") ) {
			if ( hexStringToBytes(bootHashStr, bootTokenBufer, sizeof(bootTokenBufer), bootTokenBufferLen) ) {
				if ( ((dyld3::MachOFile*)&__dso_handle)->getUuid(&bootTokenBufer[bootTokenBufferLen]) )
					bootTokenBufferLen += sizeof(uuid_t);
			}
		}
        // 存储bootToken到dyld::Array中
        dyld3::Array<uint8_t> bootToken(bootTokenBufer, bootTokenBufferLen, bootTokenBufferLen);
1.1.8 aot images
#if defined(__x86_64__) && !TARGET_OS_SIMULATOR
		if (dyld::isTranslated()) {
			struct dyld_all_runtime_info {
				uint32_t image_count;
				dyld_image_info* images;
				uint32_t uuid_count;
				dyld_uuid_info* uuids;
				uint32_t aot_image_count;
				dyld_aot_image_info* aots;
				dyld_aot_shared_cache_info aot_cache_info;
			};

			dyld_all_runtime_info* runtime_info;
			int ret = syscall(0x7000004, &runtime_info);
			if (ret == 0) {
				for (int i = 0; i < runtime_info->uuid_count; i++) {
					dyld_image_info image_info = runtime_info->images[i];
					dyld_uuid_info uuid_info = runtime_info->uuids[i];

					// add the arm64 cambria runtime to uuid info
					addNonSharedCacheImageUUID(uuid_info);

					struct stat sb;
					if (stat(image_info.imageFilePath, &sb) == 0) {
						fsid_t fsid = {{0, 0}};
						fsobj_id_t fsobj = {0};
						ino_t inode = sb.st_ino;
						fsobj.fid_objno = (uint32_t)inode;
						fsobj.fid_generation = (uint32_t)(inode>>32);
						fsid.val[0] = sb.st_dev;

						dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_MAP_A, image_info.imageFilePath, &(uuid_info.imageUUID), fsobj, fsid, image_info.imageLoadAddress);
					}
				}

				// add aot images to dyld_all_image_info
				addAotImagesToAllAotImages(runtime_info->aot_image_count, runtime_info->aots);

				// add the arm64 cambria runtime to dyld_all_image_info
				addImagesToAllImages(runtime_info->image_count, runtime_info->images);

				// set the aot shared cache info in dyld_all_image_info
				dyld::gProcessInfo->aotSharedCacheBaseAddress = runtime_info->aot_cache_info.cacheBaseAddress;
				memcpy(dyld::gProcessInfo->aotSharedCacheUUID, runtime_info->aot_cache_info.cacheUUID, sizeof(uuid_t));
			}
		}
#endif

新版本的添加了对aot images镜像文件的处理 👉 将aot images都添加到了所有镜像文件的表中,然后dyld统一按顺序加载。

1.2 dyld3的优化点

上述代码中可以看出,dyld3采用的是ClosureMode闭包模式,以回调的方式加载。闭包模式加载速度更快,效率更高iOS13动态库第三方库都使ClosureMode加载。

1.2.1 dyld3的加载流程

  1. 找到/创建mainClosure后,通过launchWithClosure启动主程序,启动失败后会有重新创建mainClosure,接着重复重新启动的逻辑;成功后返回result(主程序入口main)
  2. launchWithClosure中的逻辑和dyld2启动主程序逻辑基本相同。

1.2.2 dyld2的加载流程

dyld2的加载流程之前iOS应用程序加载大致流程分析中是分析过的👇

  1. 主程序表初始化 👉 instantiateFromLoadedImage
  2. 插入动态库 👉 loadInsertedDylib,其中主程序动态库都会添加到allImages中,并执行loadAllImages
  3. 链接主程序表 和 所有动态库 👉 link
  4. 符号绑定 👉 包括非懒加载符号和弱符号
  5. 初始化所有 👉 initializeMainExecutable
  6. 主程序入口处理

1.3 _main的整体流程

image.png

总结

image.png