
我們都知道APP的入口函數(shù)是main(),而在main()函數(shù)調(diào)用之前,APP的加載過程是怎樣的呢?接下來我們一起來分析APP的加載流程。
一、利用斷點(diǎn)進(jìn)行追蹤
-
首先我們創(chuàng)建一個(gè)工程,什么代碼都不寫,在main()函數(shù)處進(jìn)行斷點(diǎn),會(huì)看到情況如下圖:
01
- 通過上圖我們可以看到,在調(diào)用堆棧中,我們只看到了star和main,并開啟了主線程,其它的什么都看不到。那要怎么才能看到調(diào)用堆棧詳細(xì)點(diǎn)的信息了?我們都知道,有一個(gè)方法比main()函數(shù)調(diào)用更早,那就是load()函數(shù),此時(shí)在控制器中寫一個(gè)load函數(shù),并斷點(diǎn)運(yùn)行,如下圖:

- 通過上圖,我們看到了比較詳細(xì)的函數(shù)調(diào)用順序,從第13行的_dyld_start到第3行的dyld:notifySingle,頻率出現(xiàn)最多的就是這個(gè)dyld的家伙,那么dyld是什么?它在做什么?簡單來說dyld是一個(gè)動(dòng)態(tài)鏈接器,用來加載所有的庫和可執(zhí)行文件。接下來我們將通過圖2的調(diào)用關(guān)系,去追蹤dyld到底在什么?
二、 dyld加載流程分析
1. 首先下載dyld源碼。
2. 打開dyld源碼工程,根據(jù)圖2的第12行dyldbootstrap:start為關(guān)鍵字搜索dyldbootstrap中調(diào)用的start方法,如下圖:

3. 該方法源碼如下,接下來我們對(duì)該方法的重點(diǎn)部分進(jìn)行分析:
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], intptr_t slide)
{
// 讀取macho文件的頭部信息
const struct macho_header* dyldsMachHeader = (const struct macho_header*)(((char*)&_mh_dylinker_header)+slide);
// 滑塊,設(shè)置偏移量,用于重定位
if ( slide != 0 ) {
rebaseDyld(dyldsMachHeader, slide);
}
uintptr_t appsSlide = 0;
// 針對(duì)偏移異常的監(jiān)測
dyld_exceptions_init(dyldsMachHeader, slide);
// 初始化machO文件
mach_init();
// 設(shè)置分段保護(hù),這里的分段下面會(huì)介紹,屬于machO文件格式
segmentProtectDyld(dyldsMachHeader, slide);
//環(huán)境變量指針
const char** envp = &argv[argc+1];
// 環(huán)境變量指針結(jié)束的設(shè)置
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// 在dyld中運(yùn)行所有c++初始化器
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
// 如果主可執(zhí)行文件被鏈接-pie,那么隨機(jī)分配它的加載地址
if ( appsMachHeader->flags & MH_PIE )
appsMachHeader = randomizeExecutableLoadAddress(appsMachHeader, envp, &appsSlide);
// 傳入頭文件信息,偏移量等。調(diào)用dyld的自己的main函數(shù)(這里并不是APP的main函數(shù))。
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);
}
-
3.1 函數(shù)的參數(shù)中我們看到有一個(gè)macho_header的參數(shù),這是一個(gè)什么東西呢?Mach-O其實(shí)是Mach Object文件格式的縮寫,是mac以及iOS中的可執(zhí)行文件格式,并且有自己的文件格式目錄,蘋果給出的mach文件如下圖:
04 - 3.2 首先我們點(diǎn)擊進(jìn)入macho_header這個(gè)結(jié)構(gòu)體看它的定義如下:
struct mach_header_64 {
uint32_t magic; /* 區(qū)分系統(tǒng)架構(gòu)版本 */
cpu_type_t cputype; /*CPU類型 */
cpu_subtype_t cpusubtype; /* CPU具體類型 */
uint32_t filetype; /* 文件類型 */
uint32_t ncmds; /* loadcommands 條數(shù),即依賴庫數(shù)量*/
uint32_t sizeofcmds; /* 依賴庫大小 */
uint32_t flags; /* 標(biāo)志位 */
uint32_t reserved; /* 保留字段,暫沒有用到*/
};
-
3.3 這里macho_header就是讀取macho文件的頭部信息,header里面會(huì)包含該二進(jìn)制文件的一些信息:如字節(jié)順序、架構(gòu)類型、加載指令的數(shù)量等??梢杂脕砜焖俅_認(rèn)一些信息,比如當(dāng)前文件用于32位還是64位、文件的類型等。那么macho文件在哪里可以找到了呢?如下圖,我們找到macho,并用MachOView來查看:
05 -
3.4 上面那個(gè)黑不溜秋的就是macho文件,是一個(gè)可執(zhí)行文件,我們來看下它加載的頭部信息有哪些?這些信息將會(huì)被傳到下一個(gè)函數(shù)中。這里簡單說下Number of Load Commands數(shù)字為22,代表22個(gè)庫文件,在LoadCommands有加載庫的對(duì)應(yīng)關(guān)系,Section中就是我們的數(shù)據(jù)DATA,包含了代碼,常量等數(shù)據(jù)。
06 3.5 小結(jié):star函數(shù)主要就是先讀取macho文件的頭部信息,設(shè)置虛擬地址偏移,這里的偏移主要用于重定向。接下來就是初始化macho文件,用于后續(xù)加載庫文件和DATA數(shù)據(jù),再運(yùn)行C++的初始化器,最后進(jìn)入dyly的主函數(shù)。
4. 接下來我們繼續(xù)追蹤,根據(jù)圖2的調(diào)用堆棧,我們知道在dyldbootstrap:star方法中調(diào)用了dyld::_main方法,也就是我們上面說到的進(jìn)入dyld的主程序,如下圖:

- 4.1 我們進(jìn)入方法繼續(xù)追蹤,截取部分源如下圖,我們發(fā)現(xiàn)這里有幾個(gè)if判斷,此處是在設(shè)置環(huán)境變量,也就是如果設(shè)置了這些環(huán)境變量,Xcode就會(huì)在控制臺(tái)打印相關(guān)的詳細(xì)信息:
if ( sProcessIsRestricted )
pruneEnvironmentVariables(envp, &apple);
else
checkEnvironmentVariables(envp, ignoreEnvironmentVariables);
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
getHostInfo();
- 4.2 當(dāng)我們?cè)O(shè)置了相關(guān)的環(huán)境變量,此時(shí)Xcode就會(huì)打印程序相關(guān)的目錄、用戶級(jí)別、插入的動(dòng)態(tài)庫、動(dòng)態(tài)庫的路徑等,演示圖下圖:

- 4.3 設(shè)置環(huán)境變量之后,接下來會(huì)調(diào)用getHostInfo()來獲取machO頭部獲取當(dāng)前運(yùn)行架構(gòu)的信息,函數(shù)代碼如下:
static void getHostInfo()
{
#if 1
struct host_basic_info info;
mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
mach_port_t hostPort = mach_host_self();
kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count);
if ( result != KERN_SUCCESS )
throw "host_info() failed";
sHostCPU = info.cpu_type;
sHostCPUsubtype = info.cpu_subtype;
#else
size_t valSize = sizeof(sHostCPU);
if (sysctlbyname ("hw.cputype", &sHostCPU, &valSize, NULL, 0) != 0)
throw "sysctlbyname(hw.cputype) failed";
valSize = sizeof(sHostCPUsubtype);
if (sysctlbyname ("hw.cpusubtype", &sHostCPUsubtype, &valSize, NULL, 0) != 0)
throw "sysctlbyname(hw.cpusubtype) failed";
#endif
}
- 4.4 接著往下看,這里會(huì)對(duì)macho文件進(jìn)行實(shí)例化:
try {
// 實(shí)例化主程序,也就是machO這個(gè)可執(zhí)行文件
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
sMainExecutable->setNeverUnload();
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.processIsRestricted = sProcessIsRestricted;
// 加載共享緩存庫
checkSharedRegionDisable();
#if DYLD_SHARED_CACHE_SUPPORT
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
mapSharedCache();
#endif
- 4.5 進(jìn)入實(shí)例化主程序代碼如下,加載完畢后會(huì)返回一個(gè)ImageLoader鏡像加載類,這是一個(gè)抽象類,用于加載特定可執(zhí)行文件格式的類,對(duì)于程序中需要的依賴庫、插入庫,會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的image對(duì)象,對(duì)這些image進(jìn)行鏈接,調(diào)用各image的初始化方法等等,包括對(duì)runtime的初始化。
{
// isCompatibleMachO 是檢查mach-o的subtype是否是當(dāng)前cpu可以支持
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
//將image添加到imagelist。所以我們?cè)赬code使用image list命令查看的第一個(gè)便是我們的machO
addImage(image);
return image;
}
throw "main executable not a known format";
}
-
4.6 使用image list命令演示如下圖,看到的第一個(gè)0x000000010401c000地址就是macho這個(gè)可執(zhí)行文件的地址。
09
-
4.7 對(duì)macho文件進(jìn)行實(shí)例化后,會(huì)看到一個(gè)checkSharedRegionDisable()的方法,這里是在加載共享緩存庫。這個(gè)共享緩存庫是個(gè)什么東西呢? 其實(shí)我們可以理解為是系統(tǒng)公用的動(dòng)態(tài)庫(蘋果禁止第三方使用動(dòng)態(tài)庫)。如我們最常用的UIKit框架就在共享緩存庫中,舉個(gè)例子,微信、QQ、支付寶、天貓等APP都會(huì)使用到UIKit這個(gè)框架,如果每個(gè)應(yīng)用都加載UIKit,勢(shì)必會(huì)導(dǎo)致內(nèi)存緊張。所以實(shí)際是這些APP都會(huì)共享一套UIKit框架,應(yīng)用中用到了對(duì)應(yīng)了UIKit框架中的方法,dyld就會(huì)去拿對(duì)應(yīng)的資源供給這些APP使用。如下圖展示了越獄手機(jī)中System的library中framework的庫,也證明了這一點(diǎn):
共享緩存庫
5. 插入庫:我們繼續(xù)看該方法中的剩余源碼,這里將會(huì)加載所有插入庫,逆向中的代碼注入就是在這一步完成的,framework的詳細(xì)代碼注入流程請(qǐng)看我的這篇文章。這里有一個(gè)sAllImages.size()-1的操作,實(shí)際上是排除了主程序。
// 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;
6. 鏈接主程序:內(nèi)部通過imageLoader的實(shí)例對(duì)象去調(diào)用link方法,遞歸加載所依賴的系統(tǒng)庫和第三方庫。
// link main executable
gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
gLinkContext.linkingMainExecutable = false;
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
result = (uintptr_t)sMainExecutable->getMain();
7. 初始化函數(shù)

8. 運(yùn)行初始化程序:

-
8.1 遞歸:加載我們所需要的依賴的系統(tǒng)庫和第三方庫。
12
9. notifySingle函數(shù),這是一個(gè)與運(yùn)行時(shí)建立聯(lián)系的關(guān)鍵函數(shù):

- 9.1 我們發(fā)現(xiàn)notifySingle這個(gè)函數(shù)中調(diào)用了load_images方法,點(diǎn)進(jìn)去發(fā)現(xiàn)這是一個(gè)函數(shù)指針,里面并沒有找到load_images的調(diào)用,通過對(duì)dyld文件的全局搜索,也沒有發(fā)現(xiàn)。所以此時(shí)我們推斷它是在運(yùn)行時(shí)調(diào)用的,正好objc運(yùn)行時(shí)代碼也是開源的,接下來我們下載objc源碼進(jìn)行分析。
void (*notifySingle)(dyld_image_states, const ImageLoader* image);
- 9.2 在objc_init中我們會(huì)發(fā)現(xiàn)調(diào)用,這里load_images。
_dyld_objc_notify_register(&map_images, load_images, unmap_image);

- 9.3 在load_images中完成call_load_methods的調(diào)用,這里就是加載所有類文件及分類文件的load方法:
load_images(const char *path __unused, const struct mach_header *mh)
{
// 如果這里沒有+load方法,則返回時(shí)不帶鎖
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// 發(fā)現(xiàn)load方法
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// 加載所有l(wèi)oad方法
call_load_methods();
}
- 9.4 call_load_methods方法調(diào)用,在call_load_methods中,通過doWhile循環(huán)來調(diào)用call_class_loads加載每個(gè)類的load方法,然后再加載分類的loads方法。
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. 循環(huán)調(diào)用所有類文件的laod方法
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2.調(diào)用所有分類方法
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;
}
-
9.5 根據(jù)上面的調(diào)用順序,我們知道是先加載類文件中的load方法,然后再加載分類文件中的load方法,演示如圖:
15
10. 在調(diào)用完notifySigin后,我們發(fā)現(xiàn)繼續(xù)調(diào)用了doInitialization,doModInitFunctions會(huì)調(diào)用machO文件中_mod_init_func段的函數(shù),也就是我們?cè)谖募兴x的全局C++構(gòu)造函數(shù)。
// let objc know we are about to initalize this image
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this);
// initialize this image
this->doInitialization(context);
-
10.1 所以通過上述代碼的調(diào)用順序我們知道先類文件load,再分類文件load,然后再是C++構(gòu)造函數(shù),最后就進(jìn)入了我們的main主程序!演示如下:
16
通過上面的分析,我們從斷點(diǎn)開始,查看方法的堆棧調(diào)用順序,一步一步追蹤dyld的加載流程,也就將main函數(shù)調(diào)用前的神秘面紗揭露無疑,你也可以根據(jù)上述的步驟自己動(dòng)手追蹤APP的加載過程,這樣會(huì)更加印象深刻!
總結(jié):main()函數(shù)調(diào)用之前,其實(shí)是做了很多準(zhǔn)備工作,主要是dyld這個(gè)動(dòng)態(tài)鏈接器在負(fù)責(zé),核心流程如下:
1. 程序執(zhí)行從_dyld_star開始
- 1.1. 讀取macho文件信息,設(shè)置虛擬地址偏移量,用于重定向。
- 1.2. 調(diào)用dyld::_main方法進(jìn)入macho文件的主程序。
2. 配置一些環(huán)境變量
- 2.1. 設(shè)置的環(huán)境變量方便我們打印出更多的信息。
- 2.1. 調(diào)用getHostInfo()來獲取machO頭部獲取當(dāng)前運(yùn)行架構(gòu)的信息。
3. 實(shí)例化主程序,即macho可執(zhí)行文件。
4. 加載共享緩存庫。
5. 插入動(dòng)態(tài)緩存庫。
6. 鏈接主程序。
7. 初始化函數(shù)。
- 7.1. 經(jīng)過一系列的初始化函數(shù)最終調(diào)用notifSingle函數(shù)。
- 7.2. 此回調(diào)是被運(yùn)行時(shí)_objc_init初始化時(shí)賦值的一個(gè)函數(shù)load_images
- 7.3. load_images里面執(zhí)行call_load_methods函數(shù),循環(huán)調(diào)用所用類以及分類的load方法。
- 7.4. doModInitFunctions函數(shù),內(nèi)部會(huì)調(diào)用全局C++對(duì)象的構(gòu)造函數(shù),即_ _ attribute_ _((constructor))這樣的函數(shù)。
8. 返回主程序的入口函數(shù),開始進(jìn)入主程序的main()函數(shù)。
我是Qinz,希望我的文章對(duì)你有幫助。








