前言
今天我們重點來分析一下,iOS App運行時,在main()方法執(zhí)行之前,程序到底做了哪些事?
準(zhǔn)備工作
示例,新建一個iOS應(yīng)用工程,查看方法加載的順序
__attribute__((constructor)) void lg_cFunction() {
// printf();
NSLog(@"%s -- 來了", __func__);
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
NSLog(@"%s -- 來了", __func__);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
---------------------------------分割線---------------------------------
@implementation ViewController
+ (void)load {
NSLog(@"%s -- 來了", __func__);
}
@end
運行后
2020-09-28 11:06:39.756959+0800 Test1[49592:4931930] +[ViewController load] -- 來了
2020-09-28 11:06:39.757473+0800 Test1[49592:4931930] lg_cFunction -- 來了
2020-09-28 11:06:39.757671+0800 Test1[49592:4931930] main -- 來了
2020-09-28 11:06:39.800886+0800 Test1[49592:4931930] result is 0
發(fā)現(xiàn)當(dāng)前3個方法的調(diào)用順序是
load --> lg_cFunction(c++方法) -->main入口,why?
編譯過程
帶著上述方法調(diào)用順序的疑問,我們先來大致了解下,App編譯的一個過程:

大致流程是:
源文件(.h .m .cpp)-->預(yù)編譯(檢查語法)-->編譯(轉(zhuǎn)化為匯編)-->匯編(生成機器碼文件) -->鏈接(也包括一些庫的鏈接)-->生成可執(zhí)行文件(在生成的.app中右鍵打開包文件,里頭的exec)
其中鏈接這一步蘋果系統(tǒng)使用的就是dyld庫來完成的。那什么是dyld呢?
dyld動態(tài)鏈接器
dyld 是英文 the dynamic link editor 的簡寫,翻譯過來就是動態(tài)鏈接器,是蘋果操作系統(tǒng)的一個重要的組成部分。在 iOS/Mac OSX 系統(tǒng)中,僅有很少量的進(jìn)程只需要內(nèi)核就能完成加載,基本上所有的進(jìn)程都是動態(tài)鏈接的,所以 Mach-O 鏡像文件中會有很多對外部的庫和符號的引用,但是這些引用并不能直接用,在啟動時還必須要通過這些引用進(jìn)行內(nèi)容的填補,這個填補工作就是由 動態(tài)鏈接器dyld來完成的,也就是符號綁定。
找入口
你想知道系統(tǒng)調(diào)用load之前走了什么流程?很簡單,在load方法里打斷點,然后lldb bt指令查看調(diào)用堆棧信息。
| 指令名稱 | 釋義 |
|---|---|
| bt | 查看調(diào)用堆棧信息,加all可打印多有thread的堆棧 |

上圖紅框處可知,最開始是從
_dyld_start_開始的,我們?nèi)炙阉?code>_dyld_start_,尋找入口。
看注釋,我們注意到會調(diào)用
dyldbootstrap::start
再次全局搜索
dyldbootstrap,找到start函數(shù):
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)用dyld::_main,這個就是我們要找的入口!
dyld::_main流程大致分析
這個函數(shù)實現(xiàn)代碼量巨大,我們一步步看。
- 首先看函數(shù)的返回值
result,如下圖:
再搜索result的賦值地方,

上圖是初始化

上圖是一個if特殊情況條件里的返回,不考慮。

上圖是個宏編譯的if條件的,不考慮。

上圖也是一個if條件中的賦值,并return,不作考慮。

同理,不考慮。

首先3是宏條件編譯,不考慮,然后1和2是主要賦值的地方,都用到一個共同的變量
sMainExecutable,應(yīng)該是關(guān)鍵。

也是if條件里的,不考慮。
最終發(fā)現(xiàn),賦值result的都是通過變量sMainExecutable,那我們再搜索sMainExecutable的賦值情況:

第一個就找到了,通過方法
instantiateFromLoadedImage,再看注釋// instantiate ImageLoader for main executable -->為主可執(zhí)行文件實例化ImageLoader,接著我們重點看看instantiateFromLoadedImage函數(shù)
// The kernel maps in main executable before dyld gets control. We need to
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
關(guān)鍵代碼是ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}
首先sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);

根據(jù)注釋,應(yīng)該是
定義mach-o文件的格式,定義什么格式呢?這時需要用到查看
mach-o文件的軟件MachOView,舉個例子來看看:先找到工程的.app文件所在位置:

再右鍵顯示包內(nèi)容:

選擇exec可執(zhí)行文件

拖入到MachOView中:


所以,sniffLoadCommands定義的就是mach-o里的區(qū)間Load Commands里的格式。
格式定義完成后,接著進(jìn)行初始化instantiateMainExecutable
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif


不論是壓縮模式ImageLoaderMachOCompressed,還是標(biāo)準(zhǔn)模式ImageLoaderMachOClassic,最終都是生成ImageLoader對象,完成一個初始化sMainExecutable的過程。

至此,我們通過返回值result反向搜索賦值,找到sMainExecutable的初始化流程(第6577行),那么在sMainExecutable的初始化前,又走了哪些流程?我們一點點的往下看:














上述所有圖,大致描述了dyld::_main的大致流程:
- 環(huán)境變量配置
- 共享緩存處理
- 主程序表初始化
- 插入動態(tài)庫
- 鏈接主程序表
- 鏈接動態(tài)庫
- 弱符號綁定
- 初始化所有
- 主程序入口處理
第8步初始化流程詳細(xì)分析
大家肯定想知道:我們平時寫的對象到底是如何初始化的呢,說白了就是我們之前討論的_objc_init是在哪里觸發(fā)被調(diào)用的呢?帶著這個問題,我們首先看看initializeMainExecutable源碼:

再看看runInitializers源碼:

繼續(xù)processInitializers

繼續(xù)recursiveInitialization


很明顯,這里面,需要分成兩部分探索,一部分是notifySingle函數(shù),一部分是doInitialization函數(shù)。
首先探索notifySingle函數(shù)
小技巧-->全局搜索notifySingle(函數(shù)

紅框里是核心代碼
再搜索sNotifyObjCInit,看看哪里賦值處理

上圖賦值的是在函數(shù)
registerObjCNotifiers,再搜索其被調(diào)用的地方
是在
_dyld_objc_notify_register進(jìn)行了調(diào)用,但是_dyld_objc_notify_register的函數(shù)需要在libobjc源碼中搜索
終于,是在 _objc_init中,這不正是我們最開始要找的問題所在嗎,哈哈!
在_objc_init源碼中調(diào)用了_dyld_objc_notify_register,并傳入了參數(shù)load_images,那么sNotifyObjCInit = load_images,而load_images中會調(diào)用所有的+load方法。
整個
load方法的調(diào)用鏈路就是:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一個回調(diào)處理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
load方法的調(diào)用棧信息

前面的流程跟我們之前分析的一樣:
_dyld_start-->dyld::main-->initialzeMainExecutetable()初始化主程序表-->dyld::notifySingle回調(diào)結(jié)果-->load_images加載文件,緊接著就調(diào)用了load方法,具體調(diào)用的位置如下圖:
cxx方法調(diào)用棧信息
同理,在cxx方法處打斷點,查看調(diào)用棧:

與
load不同的是,在recursiveInitialization之后,


和load不同的是,在
doInitialization里觸發(fā)的cxx方法。那我們具體看看doInitialization的流程是如何處理的。
先看
doImageInit
再看
doModInitFunctions,和doImageInit差不多的流程
接著搜索
LC_SEGMENT_COMMAND
以64位為例,看看
LC_SEGMENT_64
看來是
_TEXT區(qū)間相關(guān),我們查看mach-o符號文件:
所以
doModInitFunctions就是在編譯調(diào)用上圖紅框處的區(qū)間里所有的函數(shù),其中就包含cxx函數(shù)的調(diào)用觸發(fā)。
cxx函數(shù)調(diào)用鏈
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions-->func()
那么回到之前的問題,objc_init是在哪里被調(diào)用呢?在initializeMainExecutable沒找到答案,那么接下來,只能使用終極大招了-->符號斷點。
符號斷點查看objc_init

前面的流程和
cxx函數(shù)調(diào)用大致相同,在第3步時,會調(diào)用libSystem庫,再去到libdispatch庫,然后觸發(fā)_os_object_init-->_objc_init,請看下列圖:


上圖可知:objc_init調(diào)用鏈如下:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctionslibSystem_initlializer-->libdispatch_init-->_os_object_init-->_objc_init_objc_init-->注冊觀察者_(dá)dyld_objc_notify_register(&map_images, load_images, unmap_image)-->第2個參數(shù)load_images == sNotifyObjCInit,然后再在dyld加載的ImageLoader::recursiveInitialization這一步里notifySingle-->sNotifyObjCInit觸發(fā)回調(diào),讓_objc_init和dyld加載過程形成一個閉環(huán)。
main入口
上面我們知道了load和cxx函數(shù)的調(diào)用鏈,還剩下main()了,它是在哪里被調(diào)用的呢?智能在cxx函數(shù)里打斷點,然后打開匯編模式,跟著斷點一步步看了。

然后按住按鍵control,點擊step over

上圖紅框里發(fā)現(xiàn),其實和之前分析的流程基本一致,還是回到了
_dyld_start,看來我們只有回到最初的匯編代碼里,去尋找main入口了:

在第3步也是_dyld_start的最后,找到了main(),此時才調(diào)用,當(dāng)然比load和cxx函數(shù)的調(diào)用時機都晚!
總結(jié)
借用Style_月月的iOS-底層原理 15:dyld加載流程的dyld加載流程圖:

