本文主要介绍dyld源码执行流程
,应用启动加载过程
、类、分类加载
,都不可避免的触及dyld
,所以了解dyld源码
可以让我们更好的理解iOS应用的工作原理
什么是dyld
dyld
(the dynamic link editor)是苹果的动态链接器
,是苹果操作系统的一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld
负责余下的工作。而且它是开源的
,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。
dyld下载地址:https://opensource.apple.com/tarballs/dyld,本文使用的是dyld-832.7.3
版本
dyld加载流程分析
- 新建一个工程
DyldDemo
,在ViewController.m
中实现+ (void)load {}
方法,并添加断点,运行程序,通过bt
查看函数调用栈
从堆栈信息可以看出dyld
是从_dyld_start
开始的
【1】__dyld_start
在dyld源码
的dyldStartup.s
中找到了 __dyld_start
函数,此函数由汇编实现,兼容各个平台架构,arm64
架构汇编代码如下
#include
.globl __dyld_start
#if __arm64__ && !TARGET_OS_SIMULATOR
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp
and sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables
#if __LP64__
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add x2, x28, #16 // get argv into x2
#else
ldr w0, [x28] // get app's mh into x0
ldr w1, [x28, #4] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add w2, w28, #8 // get argv into x2
#endif
adrp x3,___dso_handle@page
add x3,x3,___dso_handle@pageoff // get dyld's mh in to x4
mov x4,sp // x5 has &startGlue
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
mov x16,x0 // save entry point address in x16
#if __LP64__
ldr x1, [sp]
#else
ldr w1, [sp]
#endif
cmp x1, #0
b.ne Lnew
// LC_UNIXTHREAD way, clean up stack and jump to result
#if __LP64__
add sp, x28, #8 // restore unaligned stack pointer without app mh
#else
add sp, x28, #4 // restore unaligned stack pointer without app mh
#endif
#if __arm64e__
braaz x16 // jump to the program's entry point
#else
br x16 // jump to the program's entry point
#endif
// LC_MAIN case, set up stack for call to main()
Lnew: mov lr, x1 // simulate return address into _start in libdyld.dylib
#if __LP64__
ldr x0, [x28, #8] // main param1 = argc
add x1, x28, #16 // main param2 = argv
add x2, x1, x0, lsl #3
add x2, x2, #8 // main param3 = &env[0]
mov x3, x2
Lapple: ldr x4, [x3]
add x3, x3, #8
#else
ldr w0, [x28, #4] // main param1 = argc
add x1, x28, #8 // main param2 = argv
add x2, x1, x0, lsl #2
add x2, x2, #4 // main param3 = &env[0]
mov x3, x2
Lapple: ldr w4, [x3]
add x3, x3, #4
#endif
cmp x4, #0
b.ne Lapple // main param4 = apple
#if __arm64e__
braaz x16
#else
br x16
#endif
#endif // __arm64__ && !TARGET_OS_SIMULATOR
源码中可以看到一条bl命令
,根据注释可以知道是跳转到dyldbootstrap::start()函数
:
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
【2】dyldbootstrap::start()函数
dyldbootstrap::start()
函数中做了很多dyld初始化
相关的工作
- dyld重定位:
rebaseDyld(dyldsMachHeader);
- mach消息初始化:
mach_init();
- 栈溢出保护:
__guard_setup(apple);
//
// 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
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
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();
// 进入dyld::_main()函数
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
【3】dyld::_main()
dyld::_main()
是整个App启动的关键函数,此函数里面做了很多事情
【3.1】配置环境变量
这一步主要是设置运行参数
、环境变量
等。代码在开始的时候,将入参mainExecutableMH
赋值给了sMainExecutableMachHeader
,这是一个macho_header结构体
,表示的是当前主程序的Mach-O头部信息,加载器依据Mach-O头部信息就可以解析整个Mach-O文件信息
- 设置上下文信息:
setContext(mainExecutableMH, argc, argv, envp, apple);
- 配置进程受限模式:
configureProcessRestrictions(mainExecutableMH, envp);
-
checkEnvironmentVariables(envp);
检测环境变量 - 配置环境变量
DYLD_PRINT_OPTS
和DYLD_PRINT_ENV
打印参数
// 第一步:配置环境变量
// 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);
#if !TARGET_OS_SIMULATOR
// Trace dyld's load
notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
// Trace the main executable's load
notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif
uintptr_t result = 0;
// 主程序的MachO_header
sMainExecutableMachHeader = mainExecutableMH;
// 主程序的ASLR值
sMainExecutableSlide = mainExecutableSlide;
// 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
{
__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 TARGET_OS_OSX
gProcessInfo->platform = (uint32_t)dyld3::Platform::macOS;
#else
halt("MH_EXECUTE binaries must specify a minimum supported OS version");
#endif
}
}
#if TARGET_OS_OSX
// Check to see if we need to override the platform
const char* forcedPlatform = _simple_getenv(envp, "DYLD_FORCE_PLATFORM");
if (forcedPlatform) {
dyld_platform_t forcedPlatformType = 0;
if (strncmp(forcedPlatform, "6", 1) == 0) {
forcedPlatformType = PLATFORM_MACCATALYST;
} else if (strncmp(forcedPlatform, "2", 1) == 0) {
forcedPlatformType = PLATFORM_IOS;
} else {
halt("DYLD_FORCE_PLATFORM is only supported for platform 2 or 6.");
}
const dyld3::MachOFile* mf = (dyld3::MachOFile*)sMainExecutableMachHeader;
if (mf->allowsAlternatePlatform()) {
gProcessInfo->platform = forcedPlatformType;
}
}
// if this is host dyld, check to see if iOS simulator is being run
const char* rootPath = _simple_getenv(envp, "DYLD_ROOT_PATH");
if ( (rootPath != NULL) ) {
// look to see if simulator has its own dyld
char simDyldPath[PATH_MAX];
strlcpy(simDyldPath, rootPath, PATH_MAX);
strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
int fd = dyld3::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 {
((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
CRSetCrashLogMessage("dyld: launch started");
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
// Pickup the pointer to the exec path.
sExecPath = _simple_getenv(apple, "executable_path");
// Remove interim apple[0] transition code from dyld
if (!sExecPath) sExecPath = apple[0];
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
// kernel is not passing a real path for main executable
if ( strncmp(sExecPath, "/var/containers/Bundle/Application/", 35) == 0 ) {
if ( char* newPath = (char*)malloc(strlen(sExecPath)+10) ) {
strcpy(newPath, "/private");
strcat(newPath, sExecPath);
sExecPath = newPath;
}
}
#endif
if ( sExecPath[0] != '/' ) {
// have relative path, use cwd to make absolute
char cwdbuff[MAXPATHLEN];
if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
// maybe use static buffer to avoid calling malloc so early...
char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
strcpy(s, cwdbuff);
strcat(s, "/");
strcat(s, sExecPath);
sExecPath = s;
}
}
// Remember short name of process for later logging
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;
#if TARGET_OS_OSX && __has_feature(ptrauth_calls)
// on Apple Silicon macOS, only Apple signed ("platform binary") arm64e can be loaded
sOnlyPlatformArm64e = true;
// internal builds, or if boot-arg is set, then non-platform-binary arm64e slices can be run
if ( const char* abiMode = _simple_getenv(apple, "arm64e_abi") ) {
if ( strcmp(abiMode, "all") == 0 )
sOnlyPlatformArm64e = false;
}
#endif
// 配置进程受限模式
configureProcessRestrictions(mainExecutableMH, envp);
// 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");
}
}
}
#if TARGET_OS_OSX
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);
defaultUninitializedFallbackPaths(envp);
}
#if TARGET_OS_OSX
switch (gProcessInfo->platform) {
#if (TARGET_OS_OSX && TARGET_CPU_ARM64)
case PLATFORM_IOS:
sClosureMode = ClosureMode::On; // 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
// 如果设置了DYLD_PRINT_OPTS则调用printOptions()打印参数
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
// 如果设置了DYLD_PRINT_ENV则调用printEnvironmentVariables()打印环境变量
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
【3.2】加载共享缓存
- 首先检查共享缓存是否禁用:
checkSharedRegionDisable()
,iOS必须开启共享缓存才能正常工作(iOS cannot run without shared region) - 接下来调用
mapSharedCache()
加载共享缓存,在mapSharedCache()
里实际是调用了loadDyldCache()
函数,从代码可以看出,共享缓存加载分为三种情况mapSharedCache(mainExecutableSlide);
——>loadDyldCache(opts, &sSharedCacheLoadInfo);
- 仅加载到当前进程,调用
mapCachePrivate()
,iOS系统不回进入这个分支 - 共享缓存已加载,不做任何处理。
- 当前进程首次加载共享缓存,调用
mapCacheSystemWide()
- 仅加载到当前进程,调用
if ( sJustBuildClosure )
sClosureMode = ClosureMode::On;
// TODO: 第二步:加载共享缓存
// load shared cache
// 检查共享缓存是否禁用,iOS必须开启共享缓存
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
mapSharedCache(mainExecutableSlide);
#endif
}
动态库共享缓存区
由于iOS系统中 UIKit
、Foundation
等系统动态库
每个应用都会通过 dyld 加载到内存中,因此,为了节约空间,苹果将这些系统库放在了一个地方:动态库共享缓存区
(dyld shared cache
)
PIC技术(Position Independent Code:位置代码独立)
以NSLog
举例说明
- 【编译时】:工程中所有引用了共享缓存区中的系统库方法,编译时都会放到
间接符号表
中,运行时绑定到真实函数地址 - 【运行时】:当
dyld
将应用进程加载到内存中时,根据load commands
加载需要的系统动态库文件,然后去做相应的符号绑定
(如NSLog
,dyld
就会去找到Foundation
中NSLog
的真实地址,映射到_DATA
段符号表的NSLog
上)
这个过程被称为PIC技术
(Position Independent Code)位置代码独立
。
这里提一下fishhook
的工作原理:将编译后系统库函数所指向的符号,在运行时重绑定到用户指定的函数地址,然后将原系统函数的真实地址赋值到用户指定的指针上
【3.3】dyld2 和 dyld3(闭包模式)
在iOS 13系统
中,iOS将全面采用新的dyld3
以替代之前版本的dyld2
。dyld3
带来了可观的性能提升,减少了APP的启动时间
。
dyld2
主要过程如下:
- 解析
Mach-O
的Header
和Load Commands
,找到其依赖的库,并递归找到所有依赖的库 - 加载
Mach-O
文件 - 进行符号查找
- 绑定和变基
- 运行初始化程序
上面的所有过程都发生在 App 启动时,包含了大量的计算和I/O
所以苹果开发团队为了加快启动速度
,在 WWDC2017 - 413 -
App Startup Time: Past, Present, and Future[4]
正式提出了dyld3
。
下面我们将分析dyld3
的流程
【3.3.1】获取mainClosure
- 首先从共享缓存中获取
mainClosure
- 验证
mainClosure
是否有效(dyld闭包版本需要与 dyld缓存版本一致) - 如果没有找到一个有效的
缓存闭包
,则生成新的
// dyld3 启用闭包模式,加载速度更快
sLaunchModeUsed = DYLD_LAUNCH_MODE_USING_CLOSURE;
const dyld3::closure::LaunchClosure* mainClosure = nullptr;
dyld3::closure::LoadedFileInfo mainFileInfo;
mainFileInfo.fileContent = mainExecutableMH;
mainFileInfo.path = sExecPath;
// FIXME: If we are saving this closure, this slice offset/length is probably wrong in the case of FAT files.
mainFileInfo.sliceOffset = 0;
mainFileInfo.sliceLen = -1;
struct stat mainExeStatBuf;
if ( dyld3::stat(sExecPath, &mainExeStatBuf) == 0 ) {
mainFileInfo.inode = mainExeStatBuf.st_ino;
mainFileInfo.mtime = mainExeStatBuf.st_mtime;
}
// check for closure in cache first
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
// 从共享缓存中获取 mainClosure
mainClosure = sSharedCacheLoadInfo.loadAddress->findClosure(sExecPath);
if ( gLinkContext.verboseWarnings && (mainClosure != nullptr) )
dyld::log("dyld: found closure %p (size=%lu) in dyld shared cache\n", mainClosure, mainClosure->size());
if ( mainClosure != nullptr )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
}
// We only want to try build a closure at runtime if its an iOS third party binary, or a macOS binary from the shared cache
bool allowClosureRebuilds = false;
if ( sClosureMode == ClosureMode::On ) {
allowClosureRebuilds = true;
} else if ( (sClosureMode == ClosureMode::PreBuiltOnly) && (mainClosure != nullptr) ) {
allowClosureRebuilds = true;
}
if ( (mainClosure != nullptr) && ! closureValid(mainClosure, mainFileInfo, mainExecutableCDHash, true, envp) ) {
// 验证mainClosure是否失效
mainClosure = nullptr;
sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
}
// bootToken is a concat of boot-hash kernel passes down for app and dyld's 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);
}
}
dyld3::Array bootToken(bootTokenBufer, bootTokenBufferLen, bootTokenBufferLen);
// If we didn't find a valid cache closure then try build a new one
if ( (mainClosure == nullptr) && allowClosureRebuilds ) {
// if forcing closures, and no closure in cache, or it is invalid, check for cached closure
if ( !sForceInvalidSharedCacheClosureFormat )
mainClosure = findCachedLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
if ( mainClosure == nullptr ) {
// if no cached closure found, build new one
mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
if ( mainClosure != nullptr )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
}
}
// exit dyld after closure is built, without running program
if ( sJustBuildClosure )
_exit(EXIT_SUCCESS);
【3.3.2】通过mainClosure启动程序
// TODO: 【第三步3.2】通过闭包模式启动程序
// try using launch closure
if ( mainClosure != nullptr ) {
// dyld3开始启动
CRSetCrashLogMessage("dyld3: launch started");
if ( mainClosure->topImage()->fixupsNotEncoded() )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
Diagnostics diag;
bool closureOutOfDate;
bool recoverable;
// 通过闭包启动程序
bool launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
if ( !launched && closureOutOfDate && allowClosureRebuilds ) {
// closure is out of date, build new one
// 没有启动成功,并且closure过期,重新创建mainClosure
mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
if ( mainClosure != nullptr ) {
diag.clearError();
sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
if ( mainClosure->topImage()->fixupsNotEncoded() )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
else
sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
}
}
if ( launched ) {
gLinkContext.startedInitializingMainExecutable = true;
if (sSkipMain)
// 拿到主程序main函数,并返回
result = (uintptr_t)&fake_main;
return result;
}
else {
if ( gLinkContext.verboseWarnings ) {
dyld::log("dyld: unable to use closure %p\n", mainClosure);
}
if ( !recoverable )
halt(diag.errorMessage());
}
}
}
下面是dyld2的流程
【3.4】实例化主程序
// instantiate ImageLoader for main executable
// TODO: 实例化主程序
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
实例化主程序就是把需要的主程序的部分信息加载到内存中,通过instantiateMainExecutable
方法返回ImageLoader
类型的实例对象,然后对主程序进行签名
将实例化的image
添加到镜像文件数组中,在这注意一点主程序的image
是第一个
添加到数组中的
查看函数调用,真正实例化主程序流程:instantiateFromLoadedImage -> instantiateMainExecutable -> sniffLoadCommands
sniffLoadCommands
,有几个参数(请结合mach-O
文件看)
-
compressed
:根据LC_DYLD_INFO
和LC_DYLD_INFO_ONYL
等来决定 -
segCount
:segment段命令数量, 最大不能超过255
个 -
libCount
:依赖库数量,LC_LOAD_DYLIB
(Foundation、UIKit等) , 最大不能超过4095
个 -
codeSignCmd
:应用签名 -
encryptCmd
:应用加密信息
【3.5】加载插入动态库
越狱开发中,根据 DYLD_INSERT_LIBRARIES
环境变量,可以决定是否加载插入的动态库。
越狱的插件就是基于这个原理来实现的,只需要下载插件,就可以影响到应用,有部分防护手段就用到了这个环境变量。
【3.6】链接主程序
这里会多次调用link
函数,循环加载动态库,对主程序的依赖库进行rebase
、符号绑定
(非懒加载、弱符号)等等
【3.6.1】链接主程序
// link main executable
// TODO: 开始链接主程序
gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
【3.6.2】链接主程序完成后,判断sInsertedDylibCount
插入的动态库数量是否大于0
,然后循环链接插入的动态库
// 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);
}
}
}
【3.6.3】查看link函数
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
//dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
// 起始时间。用于记录时间间隔
uint64_t t0 = mach_absolute_time();
//递归加载主程序依赖的库.完成之后发通知。
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
// we only do the loading step for preflights
if ( preflightOnly )
return;
uint64_t t1 = mach_absolute_time();
context.clearAllDepths();
this->updateDepth(context.imageCount());
__block uint64_t t2, t3, t4, t5;
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
t2 = mach_absolute_time();
//Rebase修正ASLR!
this->recursiveRebaseWithAccounting(context);
context.notifyBatch(dyld_image_state_rebased, false);
t3 = mach_absolute_time();
if ( !context.linkingMainExecutable )
//绑定NoLazy符号
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
//绑定弱符号!
this->weakBind(context);
t5 = mach_absolute_time();
}
// interpose any dynamically loaded images
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
//递归应用插入的动态库
this->recursiveApplyInterposing(context);
}
// now that all fixups are done, make __DATA_CONST segments read-only
if ( !context.linkingMainExecutable )
this->recursiveMakeDataReadOnly(context);
if ( !context.linkingMainExecutable )
context.notifyBatch(dyld_image_state_bound, false);
uint64_t t6 = mach_absolute_time();
if ( context.registerDOFs != NULL ) {
std::vector dofs;
this->recursiveGetDOFSections(context, dofs);
//注册
context.registerDOFs(dofs);
}
//计算结束时间.
uint64_t t7 = mach_absolute_time();
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
fgTotalLoadLibrariesTime += t1 - t0;
fgTotalRebaseTime += t3 - t2;
fgTotalBindTime += t4 - t3;
fgTotalWeakBindTime += t5 - t4;
fgTotalDOF += t7 - t6;
// done with initial dylib loads
fgNextPIEDylibAddress = 0;
}
【3.6.4】插入动态库链接结束后,再进行主程序弱绑定
【3.7】初始化主程序initializeMainExecutable
接下来就是我们最关键的部分,初始化主程序initializeMainExecutable
// run all initializers
// 初始化主程序
initializeMainExecutable();
查看函数调用流程:
initializeMainExecutable()
->runInitializers()
->processInitializers()
->recursiveInitialization()
到这里就没办法跟了,
cmd + shift + o
搜索recursiveInitialization
// 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);
}
- 从
notifySingle
函数跟进去,最后来到(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
,这个回调函数,最终会调起_objc_init
里面的回调函数load_images
,这里需要下载objc4
源码进行分析
下面我们来分析objc4-818.2
源码
【3.8】load_images
- 在
objc
源码中搜索_objc_init
,发现在回调中会调用load_images
-
进入
load_images
方法
继续进入
call_load_methods
方法,调用类的+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) {
// 调用类的+load方法
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;
}
【3.9】doInitialization
继续接着【3.7】ImageLoader::recursiveInitialization
分析,load_images
回调结束后,会继续执行doInitialization
函数
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context); // 加载c++构造函数
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
c++构造方法,在
mach-O
的Data段
中对应__mod_init_func
【3.10】找到主程序入口
// find entry point for main executable
// 找到主程序的入口,调起main函数
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
if ( result != 0 ) {
// main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
*startGlue = 0;
}