一瞧毙、dyld簡介
dyld( the dynamic link editor 動(dòng)態(tài)鏈接器)饶号,是蘋果操作系統(tǒng)的一個(gè)重要的組成部分纯趋。在iOS/Mac OSX系統(tǒng)中俏蛮,僅有很少量的進(jìn)程只需要內(nèi)核就能完成加載桐猬,基本上所有的進(jìn)程都是動(dòng)態(tài)鏈接的麦撵,所以Mach-O鏡像文件中會(huì)有很多對(duì)外部的庫和符號(hào)的引用,但是這些引用并不能直接用溃肪,在啟動(dòng)時(shí)還必須要通過這些引用進(jìn)行內(nèi)容的填補(bǔ)厦坛,這個(gè)填補(bǔ)工作就是由動(dòng)態(tài)鏈接器dyld來完成的,也就是符號(hào)綁定乍惊。
二杜秸、dyld流程分析
dyld的主要作用是加載Mach-O鏡像文件,鏈接外部庫和符號(hào)綁定润绎。所有想要查看其內(nèi)部方法執(zhí)行順序撬碟,需要在main函數(shù)執(zhí)行前去分析。
那在load方法內(nèi)添加斷點(diǎn)莉撇,查看調(diào)用棧信息呢蛤。
1、_dyld_start分析
通過棧信息發(fā)現(xiàn)最早執(zhí)行的函數(shù)就是_dyld_start,進(jìn)入_dyld_start查看匯編執(zhí)行順序
在dyld源碼中全局搜索_dyld_start方法棍郎, 該方法做了底層環(huán)境區(qū)分其障,arm64的源碼如下
#if __arm64__
.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__
2、dyldbootstrap::start分析
_dyld_start匯編內(nèi)調(diào)用dyldbootstrap::start涂佃,根據(jù)dyldbootstrap找到方法start的實(shí)現(xiàn)
//
// 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>
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);
}
解釋:
- 調(diào)用rebaseDyld() 修正Mach-O文件指針(基地址復(fù)位)
- __guard_setup 棧溢出保護(hù)
- 調(diào)用dyld::_main
2.1 rebaseDyld分析
源碼實(shí)現(xiàn)
//
// On disk, all pointers in dyld's DATA segment are chained together.
// They need to be fixed up to be real pointers to run.
//
static void rebaseDyld(const dyld3::MachOLoaded* dyldMH)
{
// walk all fixups chains and rebase dyld
const dyld3::MachOAnalyzer* ma = (dyld3::MachOAnalyzer*)dyldMH;
assert(ma->hasChainedFixups());
uintptr_t slide = (long)ma; // all fixup chain based images have a base address of zero, so slide == load address
__block Diagnostics diag;
ma->withChainStarts(diag, 0, ^(const dyld_chained_starts_in_image* starts) {
ma->fixupAllChainedFixups(diag, starts, slide, dyld3::Array<const void*>(), nullptr);
});
diag.assertNoError();
// now that rebasing done, initialize mach/syscall layer
mach_init();
// <rdar://47805386> mark __DATA_CONST segment in dyld as read-only (once fixups are done)
ma->forEachSegment(^(const dyld3::MachOFile::SegmentInfo& info, bool& stop) {
if ( info.readOnlyData ) {
::mprotect(((uint8_t*)(dyldMH))+info.vmAddr, (size_t)info.vmSize, VM_PROT_READ);
}
});
}
解釋:
Mach-O 文件都是固定不變的励翼,但是每一次運(yùn)行同一個(gè)方法的指針地址都不同,地址空間布局隨機(jī)化處理(Address Space Layout Randomization辜荠,簡稱 ASLR)是為了更安全汽抚,增加分析應(yīng)用代碼成本。
dyld重定位元數(shù)據(jù)中的指針是比較耗時(shí)操作伯病,在應(yīng)用每次啟動(dòng)都會(huì)執(zhí)行操作造烁。如果可執(zhí)行文件太大或者指針類型太多都會(huì)導(dǎo)致指針重定位耗時(shí)增加,所以在這個(gè)環(huán)節(jié)我們是可以做一些優(yōu)化午笛,也應(yīng)該在平時(shí)開發(fā)過程注意代碼的規(guī)范減少這些環(huán)境的耗時(shí)惭蟋。
dyld重定位分析
ASLR和CodeSign
ASLR:是Address Space Layout Randomization(地址空間布局隨機(jī)化)的簡稱苟翻。App在被啟動(dòng)的時(shí)候瞎暑,程序會(huì)被映射到邏輯地址空間拳昌,這個(gè)邏輯地址空間有一個(gè)起始地址肖爵,ASLR技術(shù)讓這個(gè)起始地址是隨機(jī)的。這個(gè)地址如果是固定的赘被,黑客很容易就用起始地址+函數(shù)偏移地址找到對(duì)應(yīng)的函數(shù)地址碾篡。
Code Sign:就是蘋果代碼加密簽名機(jī)制砍的,但是在Code Sign操作的時(shí)候驼卖,加密的哈希不是針對(duì)整個(gè)文件氨肌,而是針對(duì)每一個(gè)Page的。這個(gè)就保證了dyld在加載的時(shí)候酌畜,可以對(duì)每個(gè)page進(jìn)行獨(dú)立的驗(yàn)證怎囚。
3、dyld::_main分析
dyld::_main源碼太長桥胞,根據(jù)下面9個(gè)步驟拆解分析
3.1 環(huán)境變量配置
根據(jù)環(huán)境變量設(shè)置相應(yīng)的值恳守,獲取當(dāng)前運(yùn)行的架構(gòu)信息,判斷dyld版本做處理
dyld3和dyld2的差異
//Check and see if there are any kernel flags (檢查是否有任何內(nèi)核標(biāo)志)
dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));
// Grab the cdHash of the main executable from the environment (從環(huán)境中獲取主可執(zhí)行文件)
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
// Set the platform ID in the all image infos so debuggers can tell the process type (在鏡像信息中設(shè)置平臺(tái)贩虾,這樣調(diào)試器就可以告訴進(jìn)程類型)
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;
});
}
CRSetCrashLogMessage("dyld: launch started");
setContext(mainExecutableMH, argc, argv, envp, apple);
// Pickup the pointer to the exec path. (提取指向exec路徑)
sExecPath = _simple_getenv(apple, "executable_path");
// <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
if (!sExecPath) sExecPath = apple[0];
#if __IPHONE_OS_VERSION_MIN_REQUIRED && !TARGET_OS_SIMULATOR
// <rdar://54095622> kernel is not passing a real path for main executable (更新exec的全路徑)
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;
}
}
// 檢查環(huán)境變量 設(shè)置環(huán)境變量
checkEnvironmentVariables(envp);
// 設(shè)置回退路徑
defaultUninitializedFallbackPaths(envp);
// 如果設(shè)置DYLD_PRINT_OPTS,打印參數(shù)
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
// 如果設(shè)置DYLD_PRINT_ENV,打印環(huán)境變量
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
// 設(shè)置臨時(shí)路徑
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;
}
}
// dyld3 設(shè)置啟動(dòng)閉包模式
if ( sJustBuildClosure )
sClosureMode = ClosureMode::On;
// 獲取當(dāng)前運(yùn)行環(huán)境的架構(gòu)信息
getHostInfo(mainExecutableMH, mainExecutableSlide);
3.2 共享緩存
檢查是否開啟了共享緩存催烘,創(chuàng)建啟動(dòng)閉包,加載共享緩存缎罢。
// load shared cache 檢查共享緩存是否開啟
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
// 設(shè)置共享緩存配置參數(shù)
mapSharedCache();
}
/*
enum class ClosureMode {
// Unset means we haven't provided an env variable or boot-arg to explicitly choose a mode
Unset,
// On means we set DYLD_USE_CLOSURES=1, or we didn't have DYLD_USE_CLOSURES=0 but did have
// -force_dyld3=1 env variable or a customer cache on iOS
On,
// Off means we set DYLD_USE_CLOSURES=0, or we didn't have DYLD_USE_CLOSURES=1 but did have
// -force_dyld2=1 env variable or an internal cache on iOS
Off,
// PreBuiltOnly means only use a shared cache closure and don't try build a new one
PreBuiltOnly
};
*/
// If we haven't got a closure mode yet, then check the environment and cache type 檢查環(huán)境和緩存類型
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();
}
}
#if !TARGET_OS_SIMULATOR
if ( sClosureMode == ClosureMode::Off ) {
if ( gLinkContext.verboseWarnings )
dyld::log("dyld: not using closure because of DYLD_USE_CLOSURES or -force_dyld2=1 override\n");
} else {
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 ( ::stat(sExecPath, &mainExeStatBuf) == 0 ) {
mainFileInfo.inode = mainExeStatBuf.st_ino;
mainFileInfo.mtime = mainExeStatBuf.st_mtime;
}
// check for closure in cache first (首先檢查共享緩存是否存在)
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
// 根據(jù)可執(zhí)行文件路徑取出共享緩存包
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());
}
// 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 (運(yùn)行時(shí)構(gòu)建閉包)
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 = nullptr;
// If we didn't find a valid cache closure then try build a new one (沒有找到一個(gè)有效的緩存包伊群,那么嘗試構(gòu)建一個(gè)新的)
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);
if ( mainClosure == nullptr ) {
// if no cached closure found, build new one
mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp);
}
}
// exit dyld after closure is built, without running program
// (關(guān)閉后退出dyld,不運(yùn)行程序)
if ( sJustBuildClosure )
_exit(EXIT_SUCCESS);
// try using launch closure
if ( mainClosure != nullptr ) {
CRSetCrashLogMessage("dyld3: launch started");
bool launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, &result, startGlue);
if ( !launched && allowClosureRebuilds ) {
// closure is out of date, build new one
mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp);
if ( mainClosure != nullptr ) {
launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, &result, startGlue);
}
}
if ( launched ) {
#if __has_feature(ptrauth_calls)
// start() calls the result pointer as a function pointer so we need to sign it.
result = (uintptr_t)__builtin_ptrauth_sign_unauthenticated((void*)result, 0, 0);
#endif
if (sSkipMain)
result = (uintptr_t)&fake_main;
return result;
}
else {
if ( gLinkContext.verboseWarnings ) {
dyld::log("dyld: unable to use closure %p\n", mainClosure);
}
}
}
}
在launchWithClosure方法內(nèi)部策精,根據(jù)已知的共享緩存包舰始, 取出共享緩存的所有鏡像,取出鏡像的方法表咽袜,記錄加載的鏡像丸卷。初始化allImage,allImage添加鏡像询刹。
3.3 主程序初始化(imageLoader)
調(diào)用instantiateFromLoadedImage函數(shù)實(shí)例化了一個(gè)ImageLoader對(duì)象
// instantiate ImageLoader for main executable (為可執(zhí)行文件實(shí)例化ImageLoader)
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
3.4 插入動(dòng)態(tài)庫
遍歷DYLD_INSERT_LIBRARIES環(huán)境變量谜嫉,調(diào)用loadInsertedDylib加載
3.5 鏈接主程序
3.6 鏈接動(dòng)態(tài)庫
鏈接動(dòng)態(tài)庫在鏈接主程序之后,以便將所有動(dòng)態(tài)庫都能被插入
3.7 符號(hào)綁定
1凹联、如果對(duì)應(yīng)地址在共享緩存中骄恶,找到該鏡像的符號(hào)綁定表直接使用。
2匕垫、主程序符號(hào)綁定僧鲁,先綁定引用的庫,再綁定鏡像文件
3象泵、綁定已插入鏡像
4寞秃、符號(hào)綁定方法
3.8 執(zhí)行初始化方法
查找runInitializers方法實(shí)現(xiàn),在ImageLoader文件內(nèi)找到其實(shí)現(xiàn)
初始化主要執(zhí)行的方法為processInitializers
各個(gè)鏡像初始化時(shí)先找到初始化方法偶惠,判斷方法是否實(shí)現(xiàn)春寿,執(zhí)行各個(gè)鏡像的初始化方法。
3.9 尋找主程序入口(main函數(shù))
根據(jù)Mach-O文件查找main函數(shù)入口地址