dyld加載流程圖
建議大家在閱讀文章的時候傻工,結(jié)合流程圖閱讀。這樣方便理解這個流程孵滞,可以將圖片下載到本地中捆,一邊閱讀一邊比對。
1坊饶、dyld
1.1 簡介
dyld(The dynamic link editor)
--- Apple的動態(tài)鏈接器泄伪。
是蘋果操作系統(tǒng)一個重要的組成部分,在應用被編譯打包成Mach-O
文件之后匿级,交由dyld
負責鏈接蟋滴,加載程序。在MacOS系統(tǒng)中痘绎,其在/usr/lib/dyld
目錄下津函。
1.2 共享緩存
在日常開發(fā)的過程中,我們會用到很多的系統(tǒng)庫孤页,比如:UIKit
尔苦、Foundation
等等;這些系統(tǒng)庫都是dyld
幫我們加載到內(nèi)存中的散庶。但是不同的APP會用到相同的系統(tǒng)庫蕉堰,如果每一個APP運行的時候,dyld
都去加載一遍悲龟,那豈不是對內(nèi)存極大的浪費屋讶。于是,為了節(jié)省空間须教,Apple將這些系統(tǒng)庫統(tǒng)一的放在了一個地方:動態(tài)庫共享區(qū)(dyld shared cache)
2皿渗、dyld加載流程
-
2.1 Demo準備
在探索dyld
加載流程之前斩芭,我們先做好準備工作。
首先請大家思考一個問題:我們APP的啟動乐疆,是先執(zhí)行main
函數(shù)嗎划乖?在main
函數(shù)之前,還有其他的操作嗎挤土?
這里我們創(chuàng)建一個Demo琴庵,一起來探索一下。我們在ViewController
中重寫load
方法仰美,然后在main.m
中添加一個C++
方法迷殿,然后觀察一下它們的執(zhí)行順序:
我們會發(fā)現(xiàn),作為APP入口的main
函數(shù)并不是第一個被執(zhí)行的函數(shù)咖杂。這又是為什么呢庆寺?
接下來我們就一步一步的探索。 -
2.2 APP啟動流程探索
通過上面诉字,我們知道懦尝,load
方法是最先被執(zhí)行的,那么我們就在load
處打一個斷點壤圃,來看看一下load
之前還有沒有什么操作陵霉。
通過bt
指令,在控制臺打印堆棧信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 2.1
* frame #0: 0x0000000104ea9d8c test`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
frame #1: 0x00000001a922c25c libobjc.A.dylib`<redacted> + 944
frame #2: 0x0000000104ef621c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 464
frame #3: 0x0000000104f075e8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
frame #4: 0x0000000104f05878 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #5: 0x0000000104f05940 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #6: 0x0000000104ef66d8 dyld`dyld::initializeMainExecutable() + 216
frame #7: 0x0000000104efb928 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216
frame #8: 0x0000000104ef5208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #9: 0x0000000104ef5038 dyld`_dyld_start + 56
可以看到在[ViewController load]
還有很多dyld
的方法被執(zhí)行埃唯。最早的一個是_dyld_start
撩匕。
這個時候就需要我們?nèi)ハ螺ddyld源碼
去分析一下了。源碼地址
本次使用的是dyld-832.7.3
-
2.2.1 _dyld_start
我們在源碼中搜索_dyld_start
墨叛,會發(fā)現(xiàn)是由匯編實現(xiàn)的止毕,我們找到arm64架構
對應的代碼如下:
看到這里大家可能會懵,這些匯編代碼都是干什么用的漠趁?
其實我們沒必要每一句代碼都弄清楚扁凛,我們只需要找到關鍵信息,理解其意圖就可以了闯传。首先我們會看到下面這一段代碼以及注釋:
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
通過注釋可以看到谨朝,這段代碼進入的是dyldbootstrap::start
這個函數(shù);這里請注意甥绿,有沒有感覺這個函數(shù)名有點熟悉舔庶。沒錯狐史,我們在上面打印堆棧信息的時候,_dyld_start
緊跟著的就是dyldbootstrap::start
。
-
2.2.2 dyldbootstrap::start
dyldbootstrap::start
是指dyldbootstrap
這個命名空間作用域內(nèi)的start
函數(shù)朦促。
cmd
+Shift
+o
搜索dyldbootstrap
:
在作用域內(nèi)搜索start
函數(shù):
可以看到start
函數(shù)的返回值是通過dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue)
獲得的暇矫;這是dyld
的main
函數(shù)并级。 - 2.2.3 dyld::main()
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
......
......
......
}
由于main
函數(shù)的代碼過長,這里就不展示出來了阱洪,下面我們把關鍵的部分講清楚就可以了。
-
2.2.3-1 配置相關環(huán)境變量
1?? :獲取相關環(huán)境信息
如上面的:
getHostInfo(mainExecutableMH, mainExecutableSlide);
就是獲取當前運行環(huán)境的架構信息菠镇。
sMainExecutableMachHeader = mainExecutableMH;
設置MachHeader
(這個我就不多做解釋冗荸,不了解的同學可以看這里3、iOS強化 --- Mach-O 文件).
sMainExecutableSlide = mainExecutableSlide;
設置slide
利耍;這個slide
就是ASLR計算出來的一個隨機值蚌本,保證程序每一次運行的偏移值都不一樣,防止黑客通過固定地址進行惡意攻擊堂竟。
2?? :設置上下文信息魂毁,檢測進程是否受限
i
玻佩、 調(diào)用setContext
函數(shù)出嘹,傳入MachHeader
以及一些參數(shù)設置上下文。
ii
咬崔、 configureProcessRestrictions
檢測進程是否受限税稼,在上下文中做出對應的處理。
// _main函數(shù)
setContext(mainExecutableMH, argc, argv, envp, apple);
......
configureProcessRestrictions(mainExecutableMH, envp);
3?? :配置環(huán)境變量
// 檢查設置的環(huán)境變量
checkEnvironmentVariables(envp);
// 如果DYLD_FALLBACK為nil垮斯,將其設置為默認值
defaultUninitializedFallbackPaths(envp);
-
2.2.3-2 共享緩存
1?? :檢查是否開啟了共享緩存 checkSharedRegionDisable
(iOS下不會被禁用)郎仆。
2?? :加載共享緩存庫 mapSharedCache ---> loadDyldCache
;加載共享緩存有幾種情況:
i
:僅加載到當前進程 mapCachePrivate
兜蠕,模擬器僅支持加載到當前進程扰肌。
ii
:共享緩存是第一次被加載,就去做加載操作 mapCacheSystemWide
熊杨。
iii
:共享緩存不是第一次被加載曙旭,那么就不做任何處理。
-
2.2.3-3 實例化主程序
// 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";
}
isCompatibleMachO((const uint8_t*)mh, path)
--- 通過macho_header
里面的magic
晶府、cputype
桂躏、cpusubtype
去檢測是否兼容。
當檢測通過之后川陆,執(zhí)行
instantiateMainExecutable
(實例化image
)剂习,接著將image
添加到鏡像列表中(addImage(image)
)。
instantiateMainExecutable
中较沪,使用sniffLoadCommands
來實例化主程序鳞绕;下面我們來簡單了解一下這個函數(shù):
void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
const linkedit_data_command** codeSigCmd,
const encryption_info_command** encryptCmd)
{
*compressed = false;
*segCount = 0;
*libCount = 0;
*codeSigCmd = NULL;
*encryptCmd = NULL;
......
......
......
// fSegmentsArrayCount is only 8-bits
if ( *segCount > 255 )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
// fSegmentsArrayCount is only 8-bits
if ( *libCount > 4095 )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
if ( needsAddedLibSystemDepency(*libCount, mh) )
*libCount = 1;
// dylibs that use LC_DYLD_CHAINED_FIXUPS have that load command removed when put in the dyld cache
if ( !*compressed && (mh->flags & MH_DYLIB_IN_CACHE) )
*compressed = true;
}
○ compressed
--- 根據(jù)LC_DYLD_INFO_ONLY
來決定。
○ segCount
--- MachO文件中segment
的數(shù)量尸曼,最大不能超過 255
個们何。
○ libCount
--- 依賴庫數(shù)量,最大不能超過 4095
個骡苞。
○ codeSigCmd
--- 應用簽名垂蜗。
○ encryptCmd
--- 應用加密信息
-
2.2.3-4 加載插入動態(tài)庫
○ 首先利用DYLD_INSERT_LIBRARIES
環(huán)境變量來判斷楷扬,是否需要插入動態(tài)庫。
○ 然后調(diào)用loadInsertedDylib
加載插入動態(tài)庫贴见。
○ 最后記錄插入動態(tài)庫的數(shù)量烘苹,sInsertedDylibCount = sAllImages.size()-1;
。
插入動態(tài)庫
這整個是一個名字片部,這樣的機制給我逆向的時候的代碼注入提供了可能镣衡。 -
2.2.3-5 鏈接主程序
-
2.2.3-6 鏈接動態(tài)庫
-
2.2.3-7 符號綁定
○sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
遞歸綁定符號表
○sMainExecutable->weakBind(gLinkContext);
弱符號綁定 -
2.2.3-8 執(zhí)行初始化方法
我們在上面打印的函數(shù)調(diào)用棧里面,main
之后就是:
frame #6: 0x0000000104ef66d8 dyld`dyld::initializeMainExecutable() + 216
我們進入initializeMainExecutable
內(nèi)部看一下:
我們發(fā)現(xiàn)
initializeMainExecutable
內(nèi)部有一個循環(huán)遍歷档悠,每次都會執(zhí)行runInitializers
廊鸥。cmd
+ shift
+ o
搜索runInitializers
,跟進去:發(fā)現(xiàn)
runInitializers
中又調(diào)用processInitializers
為初始化做準備辖所。那么我們跟進processInitializers
:注意看
processInitializers
里面的for循環(huán)
惰说,recursiveInitialization
遞歸初始化鏡像。
同樣的搜索recursiveInitialization
缘回,跟進去:
我們會發(fā)現(xiàn)吆视,在
recursiveInitialization
中關鍵的兩步是:i
: 初始化鏡像 --- doInitialization
ii
:鏡像初始化完成后,發(fā)送廣播通知 --- notifySingle
notifySingle
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
//dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
if ( handlers != NULL ) {
dyld_image_info info;
info.imageLoadAddress = image->machHeader();
info.imageFilePath = image->getRealPath();
info.imageFileModDate = image->lastModified();
for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
const char* result = (*it)(state, 1, &info);
if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
//fprintf(stderr, " image rejected by handler=%p\n", *it);
// make copy of thrown string so that later catch clauses can free it
const char* str = strdup(result);
throw str;
}
}
}
if ( state == dyld_image_state_mapped ) { // 是否被映射
// <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
// <rdar://problem/50432671> Include UUIDs for shared cache dylibs in all image info when using private mapped shared caches
if (!image->inSharedCache()
|| (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {
dyld_uuid_info info;
if ( image->getUUID(info.imageUUID) ) {
info.imageLoadAddress = image->machHeader();
addNonSharedCacheImageUUID(info);
}
}
}
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
uint64_t t1 = mach_absolute_time();
uint64_t t2 = mach_absolute_time();
uint64_t timeInObjC = t1-t0;
uint64_t emptyTime = (t2-t1)*100;
if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
timingInfo->addTime(image->getShortName(), timeInObjC);
}
}
// mach message csdlc about dynamically unloaded images
if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
notifyKernel(*image, false);
const struct mach_header* loadAddress[] = { image->machHeader() };
const char* loadPath[] = { image->getPath() };
notifyMonitoringDyld(true, 1, loadAddress, loadPath);
}
}
這個函數(shù)中酥宴,重點代碼是:
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
全局搜索sNotifyObjCInit
啦吧,并沒有發(fā)現(xiàn)其函數(shù)實現(xiàn),但是我們找到了它的賦值操作:
這樣我們繼續(xù)跟進拙寡,查找
registerObjCNotifiers
在哪里被調(diào)用了:?????? :這個時候授滓,我們發(fā)現(xiàn)在函數(shù)
_dyld_objc_notify_register
中,registerObjCNotifiers
被調(diào)用肆糕。_dyld_objc_notify_register
的函數(shù)調(diào)用般堆,我們dyld
源碼中并沒有找到,這個時候擎宝,我們需要去libobjc
源碼中去尋找郁妈。(本次使用的是objc4-818.2
)
- 在
objc4-818.2
中我們找到了這個一段代碼
我們仔細的比對會發(fā)現(xiàn),_dyld_objc_notify_register
傳入的第二個參數(shù)是load_images
绍申,那么也就是說給sNotifyObjCInit
賦的值就是load_images
噩咪;而load_images
會調(diào)用所有的+load
方法。因此:notifySingle
是一個回調(diào)函數(shù)
极阅。 -
load_images
接著跟進call_load_methods
:
我們會發(fā)現(xiàn)call_load_methods
的核心是通過do-while
循環(huán)調(diào)用call_class_loads()
胃碾。那我們就繼續(xù)跟進:
這里就可以很清晰的看到,最終循環(huán)調(diào)用了所有的+load
方法筋搏。
這個時候我們來梳理一下
load
的函數(shù)調(diào)用棧:_dyld_start
-->dyldbootstrap::start
-->dyld::_main
-->dyld::initializeMainExecutable
-->ImageLoader::runInitializers
-->ImageLoader::processInitializers
-->ImageLoader::recursiveInitialization
-->dyld::notifySingle
-->sNotifyObjCInit
-->load_images
_dyld_objc_notify_register
_objc_init
_objc_init
我們繼續(xù)往下探索
doInitialization
我們會發(fā)現(xiàn)俄周,
doInitialization
主要分成兩部分:i
:doImageInit
ii
:doModInitFunctions
-
doImageInit
進入doImageInit
源碼發(fā)現(xiàn)吁讨,其核心是一個for循環(huán)
,注意看代碼的注釋峦朗,libSystem
的初始化必須先執(zhí)行建丧。 -
doModInitFunctions
doModInitFunctions
這個方法內(nèi)部會調(diào)用全局C++
對象的構造函數(shù)__attribute__((constructor))
的C函數(shù)
。
這個我們可以通過測試Demo的函數(shù)調(diào)用棧來驗證一下:
探索到這里波势,我們?nèi)匀粵]有發(fā)現(xiàn)_objc_init
被調(diào)用的相關信息翎朱。這個時候,我們就需要下一個符號斷點尺铣,來查看_objc_init
對應的堆棧信息了拴曲。
符號斷點埋下之后,運行程序凛忿,
bt
打印堆棧信息:大家觀察此時的函數(shù)調(diào)用棧澈灼,是不是清晰了很多。
我們先來捋一下
_objc_init
的函數(shù)調(diào)用棧(此時用的是模擬器侄非,整理函數(shù)調(diào)用棧的時候蕉汪,省略了模擬器相關的一些函數(shù)調(diào)用):_dyld_start
-->dyldbootstrap::start
-->dyld::_main
-->dyld::initializeMainExecutable
-->ImageLoader::runInitializers
-->ImageLoader::processInitializers
-->ImageLoader::recursiveInitialization
-->ImageLoaderMachO::doInitialization
-->ImageLoaderMachO::doModInitFunctions
-->libSystem_initializer(libSystem.B.dylib)
-->libdispatch_init(libdispatch.dylib)
-->_os_object_init(libdispatch.dylib)
-->_objc_init(libobjc.A.dylib)
這里我們一起來回憶一下,在初始化
_objc_init
的時候逞怨,調(diào)用_dyld_objc_notify_register
,load_images
被注冊福澡;在我們分析notifySingle
的時候叠赦,發(fā)現(xiàn)了重要的函數(shù)sNotifyObjCInit
,并且我們之后還發(fā)現(xiàn)sNotifyObjCInit = init = load_images
革砸。這樣就形成了一個閉環(huán)除秀。
- 2.2.3-9 尋找主程序入口
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
探索了那么多,我們來串一下dyld::main
的調(diào)用流程: