老規矩,先說一下本篇文章說的內容。有兩大部分:分別是MachO
文件和DYLD
。
主要要說的是DYLD
加載image(不是圖片,是鏡像)
的總體流程。由於這兩部分都是概念性的知識且MachO部分的內容相對來講不算太多,因此就將兩部分知識點用這一篇文章歸納算咯🐶。編程
標題是屬於第二大塊DYLD裏面的內容,若是是衝着標題來的能夠直接跳到DYLD去看。bootstrap
Mach-O爲Mach object文件格式的縮寫,它是一種用於可執行文件、目標代碼、動態庫的文件格式。緩存
.o
.a | .dylib | xxx.framework/xxx
.dsym
能夠經過終端file + 文件路徑
來查看文件的類型信息。bash
蘋果公司提出的一種程序代碼,可以同時兼容多種架構的二進制文件。如果不瞭解什麼是架構,看下面:
arm6四、arm64e、armv七、armv7s
這些就是不一樣的架構,具體有什麼區別與聯繫這裏再也不多說。 架構
寫過SDK的童鞋對此應該比較熟悉,在合併真機包和模擬器包的時候會使用如下指令:app
lipo -create [真機編譯路徑/xxx.framework/xxx]
[模擬器編譯路徑/xxx.framework/xxx] -output [合併後輸出的文件路徑]
複製代碼
想起來了吧?這個命令就是爲了合併不一樣架構(真機/模擬器)
的二進制文件。ide
合併的想起來了,別急,下面還有一個拆分的命令:函數
// 從通用二進制文件拆分出不一樣的架構
lipo [通用二進制文件路徑] -thin [要拆的架構] -output [拆出的二進制輸出的路徑]
複製代碼
首先來整一張圖。
從圖中能夠看出,MachO文件能夠分爲Header、Load commands、Data
三部分。post
回到標題,"main函數是整個程序的入口"
這句話想必你們從開始學習編程的那一刻就聽到過了吧!可是有沒有想過main函數爲何是程序的入口?main函數在哪裏調用的呢?以及main函數調用以前作了哪些事呢?性能
想知道上面問題的答案,慢慢往下看,dyld。
dyld(the dynamic link editor)是蘋果的動態連接器,是蘋果操做系統 一個重要組成部分,在系統內核作好程序準備工做以後,交由dyld負責餘下的工做。並且它是開源的,任何人能夠經過蘋果官網下載它的源碼來閱讀理解它的運做方式,瞭解系統加載動態庫的細節。dyld源碼。
咱們知道,每個類都有一個load方法,而且這個load方法的調用時機特別的早,比程序入口main函數調用的還要早。
包括上篇文章的代碼注入之因此寫到load方法中也是由於這個緣由,在真正的代碼邏輯執行以前就交換了某些類的方法,則在代碼邏輯執行過程使用的方法就都是交換過之後的方法了。
爲了探究dyld,咱們先在任意類的load方法上打一個斷點,運行,能夠看到在load方法執行前有9個調用堆棧。
點擊調用堆棧中的第一個_dyld_start
可查看到彙編代碼。注意斷點的前一行dyldbootstrap::start
,和左邊調用堆棧中的第二步相同,說明就是執行了這句代碼跳到了堆棧第二步。(查詢彙編bl
指令的含義就可驗證這個猜測)。
另外若是對C++語言有所瞭解的話,應該就知道dyldbootstrap
是C++中的一個命名空間,start是這個命名空間的一個函數。
接下來咱們就在下載的dyld源碼中全局搜索這個命名空間dyldbootstrap
而且查找裏面是否是有一個start函數。
接下來咱們回過頭看剛剛的調用堆棧,第三步是dyld::_main
,仔細一看這不正是咱們start函數最後返回值處調用的一個方法嗎?
看到dyld::_main
,好了,main
函數找到了,main
函數調用以前也沒幹啥事啊?
哈哈😂,先別急說我是標題黨,這個_main
並不是程序入口main
。跳進方法實現一看,這個方法實現600多行,還真很多。
_main
函數的代碼實現吧。
Step1: 設置運行環境。
主要是設置主程序的運行參數、環境變量等。
將參數mainExecutableMH
賦值給了sMainExecutableMachHeader
,這是一個macho_header
結構體,表示的是當前主程序的MachO頭部信息
。
// 將主程序的MachO頭部信息賦值給sMainExecutableMachHeader
sMainExecutableMachHeader = mainExecutableMH; // MH = MachOHeader
// 同理,保存主程序的內存地址偏移值
sMainExecutableSlide = mainExecutableSlide;
複製代碼
接着調用setContext()設置上下文信息,包括一些回調函數、參數、標誌信息等。設置的回調函數都是dyld模塊自身實現的,如loadLibrary()函數實際調用的是libraryLocator(),負責加載動態庫。
// 設置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
複製代碼
配置進程是否受限、檢查環境變量
// 配置進程是否受限
configureProcessRestrictions(mainExecutableMH, envp);
··· ···
// 檢查環境變量
checkEnvironmentVariables(envp);
// 在Scheme中配置對於的環境變量可打印對應環境變量的值
// 配置相應的環境變量(DYLD_PRINT_OPTS/DYLD_PRINT_ENV)便可打印對應的信息
if (sEnv.DYLD_PRINT_OPTS) {
printOptions(argv);
}
if ( sEnv.DYLD_PRINT_ENV ) {
printEnvironmentVariables(envp);
}
複製代碼
Step2: 加載共享緩存
這裏要說一下iOS裏的共享庫、動態庫和靜態庫的區別。
共享庫(真正的動態庫)
。"文件夾"
,把某些功能及所用到的資源文件所有放到這個"文件夾"
裏面了,在程序編譯連接期間靜態庫的代碼會被編譯到主程序的MachO文件中。通過上面的解釋,想必已經知道加載共享緩存
是作什麼的了吧?我認爲就是讀取共享庫的緩存區,將App使用到的系統共享庫沒加載到內存的加載到內存,已經加載的記錄一下內存地址。
// 檢查共享緩存是不是禁用狀態
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
··· ···
// 映射/加載共享緩存
mapSharedCache();
複製代碼
檢查共享緩存禁用狀態那個方法跳進去看源碼能夠知道,iOS系統共享緩存沒法被禁用。
Step3: 實例化主程序
聽名字就知道這一步是作什麼的。操做系統自己也是一個應用,只不過這個應用是用來管理其餘應用的。既然是應用,那麼這個應用內確定會有變量/對象,很明顯,這一步就是經過主程序的相關信息建立一個主程序對象。此時的App相對於操做系統來講就是其中的一個變量。
// 實例化主程序,建立主程序對象sMainExecutable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH,
mainExecutableSlide, sExecPath);
複製代碼
跳到這個方法實現裏面能夠發現,這個方法就是建立來一個ImageLoader類型的對象image(這裏不是圖片,是指主程序的鏡像)
而且添加到了某個地方保存起來。
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path) {
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
// 建立image對象,image指的是主程序
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
// 添加主程序鏡像
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}
複製代碼
Step4: 加載插入的庫
這裏插入的庫是dyld源碼中註釋的直譯,我認爲這裏"插入的庫"
應該指的就是動態庫,包括App開發中用到的動態庫以及咱們代碼注入
時注入的動態庫。
從代碼上看是遍歷DYLD_INSERT_LIBRARIES
環境變量指向的那個連續的空間,取出所有的插入的庫,依次加載。
// 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;
複製代碼
點進loadInsertedDylib(*lib);
方法發現裏面又調用了一個load(path, context, cacheIndex);
方法,在這個方法中一層層的調用了loadPhase0、loadPhase一、loadPhase二、loadPhase三、loadPhase四、loadPhase五、loadPhase6
等方法,具體實現沒有詳細研究,大概看了一下發現有些對加載的庫進行一些簽名驗證、cryptid判斷等操做。(有興趣的能夠本身研究研究源碼)
Step5: 連接主程序和插入的庫
// 連接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
··· ···
// 連接插入的庫
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();
}
複製代碼
連接主程序和插入的庫是分兩次調用的,在AllImages中第一個image是主程序的image,後面纔是插入的庫的image。
經過link
函數連接主程序和插入的庫,在link
函數中會遞歸的將當前image進行符號綁定。注意:這裏符號綁定只會綁定nolazy的庫,對於lazy標記的庫會在運行時動態進行綁定連接。
Step6: 初始化主程序
前面那些步驟已經將須要配置和加載的東西都完成了,接下來就須要初始化咱們的主程序,相似於建立對象的alloc
已經執行完了,接下來就是init
了。
// 初始化主程序
initializeMainExecutable();
複製代碼
點進去查看方法實現以下:
void initializeMainExecutable() {
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
// 先初始化所有的插入庫
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
// 從1開始,由於第0個是主程序的image
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
// 執行主程序的初始化方法
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
// 若是配置了這兩個環境變量,則會打印想對應的狀態信息
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
複製代碼
其實看到這個initializeMainExecutable()
方法,就會發現這個方法就是最開始那個調用堆棧裏面第四步,也就是說調用堆棧的後續步驟須要從這個函數繼續追蹤了。那麼接下來就開始追蹤吧。
找到堆棧下一步runInitializers
,恰好代碼中就有這個函數的調用
processInitializers
接着調用
recursiveInitialization
再跳,哎?咋跳到聲明文件了?實現文件不讓看嗎?
image
,在
doInitialization
方法前會發送
state
爲
dyld_image_state_dependents_initialized
的通知,這個通知會調用
libobjc
的
load_images
,最後去依次調用各個OC類的load方法以及分類的load方法。(這裏就是load方法的調用時機)。
接下來咱們跳進notifySingle
裏面看看,哎?又跳到聲明瞭,老方法
load_images
,可是咱們找遍了也沒找到哪裏有調用
load_images
。
既然要找這個回調方法的實現,那咱們就得先找找這個回調是在哪賦的值?
load_images
,運行
從這裏能夠看出
load_images
是
libobjc
裏面的,所以咱們能夠下載
runtime源碼
從這個調用堆棧裏面能夠發現
load_images
是
_objc_init
函數裏面的,所以咱們在runtime源碼裏面全局搜索
_objc_init
。
能夠看到
load_images
是
_dyld_objc_notify_register
函數的第二個參數,很明顯
_dyld_objc_notify_register
是dyld庫裏面的方法。在dyld源碼裏面搜索
_dyld_objc_notify_register
。
再跳進
registerObjCNotifiers
方法實現,能夠看到
objc
傳過來的
load_images
在dyld中賦值給力
sNotifyObjCInit
,而咱們上面的那個回調函數就是
sNotifyObjCInit
。說明那一步回調實質上調用的就是
objc
裏面的
load_images
函數。
說了這麼多,還不知道
load_images
函數到底長啥樣,在
objc
源碼中搜索
load_images
找到方法實現。
在跳進
call_load_methods()
函數,函數實現以下
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;
}
複製代碼
這裏的call_class_loads();
方法內部就是調用每一個類的load
方法。
找到了load方法的調用時機,還沒完,再回到剛開始的_main
函數中,初始化主程序這一步算是分析完了,可是下面還有代碼,繼續分析。
// find entry point for main executable
// 找到主可執行程序的入口點
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;
}
複製代碼
getEntryFromLC_MAIN()
方法找到了主可執行程序的入口了,也就是那個程序的入口main()
函數,非剛剛分析的_main()
函數。
又通過一系列的配置,最後_main()
函數的返回值處返回了咱們找到的程序入口main()
函數。
又到了總結的時刻,總結了如下六點......
哈哈,寫這篇文章快要累死我了,可是收穫也是不少的,學到了App加載過程當中經歷了哪些步驟,後續文章咱們可能就會利用這些東西的加載順序的不一樣來破解或者防禦App。
真正的總結
MachO文件
dyld,main函數以前都作了什麼?
_dyld_start
開始執行,進入_main
函數。doModInitFunctions
函數,會調用帶有__attribute__((constructor))
的C函數。_main()
調用結束返回程序入口main()
函數,開始進入主程序的main()
函數。