在我們初學(xué)iOS的時候,分析一個程序的執(zhí)行流程都是從main函數(shù)開始的考传。但是在main函數(shù)之前其實(shí)也做了不少操作吃型,值得我們分析一下。
我們知道一個類的load
的方法是先于main
函數(shù)執(zhí)行的僚楞,通過對load
方法設(shè)置一個斷點(diǎn)勤晚,查看調(diào)用椨诟觯可知程序在加載過程中大致所執(zhí)行的一些方法。
其中可見dyld
(the dynamic link editor)蛤袒,它是蘋果的動態(tài)鏈接器乘粒,是蘋果操作系統(tǒng)一個重要組成部分,在系統(tǒng)內(nèi)核做好程序準(zhǔn)備工作之后挺邀,交由dyld負(fù)責(zé)余下的工作揉忘。通過分析dyld
的源碼,我們來分析dyld
做了什么。
準(zhǔn)備分析
1端铛、通過分析_dyld_start的匯編實(shí)現(xiàn)泣矛。發(fā)現(xiàn)調(diào)用了dyldbootstrap::start
方法。
# call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
movl 8(%rbp),%esi # param2 = argc into %esi
leaq 16(%rbp),%rdx # param3 = &argv[0] into %rdx
movq __dyld_start_static(%rip), %r8
leaq __dyld_start(%rip), %rcx
subq %r8, %rcx # param4 = slide into %rcx
leaq ___dso_handle(%rip),%r8 # param5 = dyldsMachHeader
leaq -8(%rbp),%r9
2禾蚕、在下載好的dyld源碼中搜索dyldbootstrap
,在這個命名空間中尋找start
方法您朽。
在這個方法中,通過slideOfMainExecutable
得到因ASLR
產(chǎn)生的偏移换淆。通過rebaseDyld
重綁定哗总。通過__guard_setup
來?xiàng)R绯霰Wo(hù)。
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[],
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
這個start
的方法的返回值是調(diào)用了一個main
函數(shù),將start的一些值作為參數(shù)傳到main
倍试。
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
3讯屈、dyld
也可以看做一個程序的執(zhí)行,它的main函數(shù)和我們?nèi)粘i_發(fā)應(yīng)用的main函數(shù)類似县习,都可以看做程序的入口涮母。接下來我們主要便是分析main
函數(shù)的實(shí)現(xiàn)。
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
加載過程
0x01 配置環(huán)境准颓,設(shè)置環(huán)境變量等
- 設(shè)置上下文
setContext(mainExecutableMH, argc, argv, envp, apple);
- 配置進(jìn)程是否受限
configureProcessRestrictions(mainExecutableMH);
- 檢查環(huán)境變量
checkEnvironmentVariables(envp);
- 根據(jù)
Xcode
設(shè)置的環(huán)境變量哈蝇,來打印程序相應(yīng)參數(shù)。
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
會打印程序相關(guān)的目錄攘已、用戶級別炮赦、插入的動態(tài)庫、動態(tài)庫的路徑等样勃。
opt[0] = "/var/containers/Bundle/Application/731D64D1-8B04-491B-A512-4010011413E6/dyld.app/dyld"
CA_DEBUG_TRANSACTIONS=0
TMPDIR=/private/var/mobile/Containers/Data/Application/EF8F63AD-B59A-42E7-92EE-076BC9F664D0/tmp
__CF_USER_TEXT_ENCODING=0x1F5:0:0
SHELL=/bin/sh
SQLITE_ENABLE_THREAD_ASSERTIONS=1
OS_ACTIVITY_DT_MODE=YES
HOME=/private/var/mobile/Containers/Data/Application/EF8F63AD-B59A-42E7-92EE-076BC9F664D0
DYLD_PRINT_TO_STDERR=YES
CFFIXED_USER_HOME=/private/var/mobile/Containers/Data/Application/EF8F63AD-B59A-42E7-92EE-076BC9F664D0
NSUnbufferedIO=YES
PATH=/usr/bin:/bin:/usr/sbin:/sbin
LOGNAME=mobile
XPC_SERVICE_NAME=UIKitApplication:dyoung.dyld[0x1b53][62]
DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktraceRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
CLASSIC=0
DYLD_PRINT_OPTS=1
DYLD_PRINT_ENV=1
USER=mobile
XPC_FLAGS=0x1
CA_ASSERT_MAIN_THREAD_TRANSACTIONS=0
DYLD_LIBRARY_PATH=/usr/lib/system/introspection
- 通過
getHostInfo
獲取machO頭部獲取當(dāng)前運(yùn)行架構(gòu)的信息吠勘。
static void getHostInfo(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide)
0x02 加載共享緩存庫。
- 判斷共享緩存庫是否被禁用峡眶。
iOS cannot run without shared region
剧防,注釋說明iOS平臺下是不能被禁用的。
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
- 通過
mapSharedCache()
函數(shù)加載辫樱、進(jìn)入函數(shù)內(nèi)部其主要實(shí)現(xiàn)是loadDyldCache
這個函數(shù)峭拘。其中作了如下三種判斷
if ( options.forcePrivate ) {
// mmap cache into this process only
//只加載到當(dāng)前緩存。
return mapCachePrivate(options, results);
}
else {
// fast path: when cache is already mapped into shared region
//快速路徑,如果已經(jīng)加載的話就不處理了鸡挠。
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;
}
0x03 實(shí)例化主程序(Mach0,程序的可執(zhí)行文件)
- 實(shí)例化過程:
instantiateFromLoadedImage
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
//isCompatibleMachO 是檢查mach-o的subtype是否是當(dāng)前cpu可以支持
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);//將image添加到imagelist拣展。所以我們在Xcode使用image list命令查看的第一個便是我們的machO
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
0x04 加載插入庫
- 通過
loadInsertedDylib
方法執(zhí)行插入動態(tài)庫的加載彭沼。在實(shí)現(xiàn)中調(diào)用load
方法返回imageLoader
對象,
imageLoader
是一個抽象基類备埃,專門用于輔助加載特定可執(zhí)行文件格式的類姓惑,對于程序中需要的依賴庫、插入庫按脚,會創(chuàng)建一個對應(yīng)的image對象于毙,對這些image進(jìn)行鏈接,調(diào)用各image的初始化方法等等辅搬,包括對runtime的初始化望众。
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
//遍歷DYLD_INSERT_LIBRARIES的環(huán)境變量。
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
0x05 鏈接主程序伞辛,并加載系統(tǒng)和第三方的動態(tài)庫
- 在
main
中通過link
鏈接主程序。
//main 函數(shù)中
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
- 內(nèi)部通過imageLoader的實(shí)例對象去調(diào)用
link
方法夯缺。
//image調(diào)用link
image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
- 遞歸加載我們所需要的依賴的系統(tǒng)庫和第三方庫蚤氏。
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
- 對依賴庫進(jìn)行重定位。相當(dāng)于加上ASLR滑塊踊兜。
this->recursiveRebase(context);
- 遞歸綁定符號表和弱綁定竿滨。
綁定就是將這個二進(jìn)制調(diào)用的外部符號進(jìn)行綁定的過程。
比如我們objc代碼中需要使用到NSObject, 即符號OBJC_CLASS$_NSObject捏境,但是這個符號又不在我們的二進(jìn)制中于游,在系統(tǒng)庫 Foundation.framework中,因此就需要binding這個操作將對應(yīng)關(guān)系綁定到一起垫言。
lazyBinding就是在加載動態(tài)庫的時候不會立即binding, 當(dāng)?shù)谝淮握{(diào)用這個方法的時候再實(shí)施binding贰剥。 做到的方法也很簡單: 通過dyld_stub_binder 這個符號來做。
lazy binding的方法第一次會調(diào)用到dyld_stub_binder, 然后dyld_stub_binder負(fù)責(zé)找到真實(shí)的方法筷频,并且將地址bind到樁上蚌成,下一次就不用再bind了。
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
this->weakBind(context);
- 插入動態(tài)庫凛捏。
if ( sInsertedDylibCount > 0 ) {//有的話就開始鏈接加載担忧。
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];//1過濾到主程序。
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
// 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);
}
}
0x06 初始化函數(shù)坯癣,承前啟后
一瓶盛、dyld流程分析
-> 在main函數(shù)中我們進(jìn)入initializeMainExecutable
->runInitializers
初始化主程序
->processInitializers
->recursiveInitialization
循環(huán)初始化
->關(guān)鍵函數(shù) :notifySingle
。在這個方法中調(diào)用了objc
的loadImages
。通過command+shift+o
全局搜索尋找實(shí)現(xiàn)惩猫。
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
發(fā)現(xiàn)一個函數(shù)指針的調(diào)用:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
通過在本文件搜索sNotifyObjCInit
函數(shù)指針芝硬,我們找到了賦值的地方。
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
sNotifyObjCInit = init;//賦值函數(shù)帆锋。
全局搜索registerObjCNotifiers
調(diào)用的地方
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);
}
再次全局搜索_dyld_objc_notify_register
便找不到這個方法調(diào)用吵取。于是我們通過Xcode設(shè)置符號斷點(diǎn)來分析。如圖所示锯厢,我們則能推斷出這個方法的調(diào)用是在runtime
中皮官。
二、runtime流程分析
分析runtime
源碼实辑∞嗲猓可知上面的函數(shù)是在其初始化的時候進(jìn)行調(diào)用的。
load_images
賦值到dyld
中的sNotifyObjCInit
指針剪撬。
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
在load_images
中摄乒,完成call_load_methods
的調(diào)用。
load_images(const char *path __unused, const struct mach_header *mh)
{
// 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
中残黑,通過doWhile
循環(huán)來調(diào)用call_class_loads
實(shí)現(xiàn)每個類的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;
}
三、_ _ attribute_ _((constructor))
是GCC的擴(kuò)展語法(黑魔法)梨水,由它修飾過的函數(shù)拭荤,會在main函數(shù)之前調(diào)用。原理是在ELF的.ctors段增加一條函數(shù)引用疫诽,加載器在執(zhí)行main函數(shù)前舅世,檢查.ctror section,并執(zhí)行里面的函數(shù)奇徒。
繼續(xù)dyld
分析雏亚。在imageLoader.cpp
文件中,notifySingle
調(diào)用之后摩钙,接著調(diào)用了doInitialization
方法罢低。
其中doModInitFunctions
會調(diào)用machO
文件中_mod_init_func
段的函數(shù),也就是我們在文件中所定義的全局C++
構(gòu)造函數(shù)腺律。
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
通過以上分析加載流程我們可得知函數(shù)的執(zhí)行順序?yàn)椋?/p>
load -> attribute((constructor)) -> main -> initialize
0x07 尋找應(yīng)用程序主函數(shù)入口
最后return
,dyld的main
函數(shù)結(jié)束奕短。
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getThreadPC();
至此,程序進(jìn)入了main函數(shù)匀钧,開啟了我們熟知的一切翎碑。