瞭解 dyld
的加載流程能夠幫咱們更系統的瞭解 iOS
應用的本質 . 不管是在逆向方向或者在底層研究方面 , dyld
都是必不可少的領域 . 對流程梳理清楚能夠幫助咱們更好地瞭解一些基礎原理 . 例如咱們以前講 分類底層原理詳細研究流程 , load方法調用機制解析 , 都不可避免的提到 dyld
.c++
本篇文章就整個加載流程進行梳理分析 , 並不會特別細 , 畢竟整個流程太多 , 須要提點的都會有所介紹 .bootstrap
提示 : 瞭解本文前先請對 Mach-O 文件有所瞭解 .緩存
dyld
全名 The dynamic link editor . 它是蘋果的動態連接器,是蘋果操做系統一個重要組成部分 ,在應用被編譯打包成可執行文件格式的 Mach-O
文件以後 ,交由 dyld
負責連接 , 加載程序 。bash
dyld
是開源的,咱們能夠經過 官網 下載它的源碼來閱讀理解它的運做方式,瞭解系統加載動態庫的細節 。微信
我這裏下載的是 dyld-635.2
.架構
解讀 dyld
有一個必不可少的東西 - 共享緩存 .app
因爲 iOS
系統中 UIKit
/ Foundation
等庫每一個應用都會經過 dyld
加載到內存中 , 所以 , 爲了節約空間 , 蘋果將這些系統庫放在了一個地方 : 動態庫共享緩存區 (dyld shared cache) . ( Mac OS 同樣有 ) .ide
所以 , 相似 NSLog
的函數實現地址 , 並不會也不可能會在咱們本身的工程的 Mach-O
中 , 那麼咱們的工程想要調用 NSLog
方法 , 如何能找到其真實的實現地址呢 ?函數
其流程以下 :post
在工程編譯時 , 所產生的
Mach-O
可執行文件中會預留出一段空間 , 這個空間其實就是符號表 , 存放在_DATA
數據段中 ( 由於_DATA
段在運行時是可讀可寫的 )編譯時 : 工程中全部引用了共享緩存區中的系統庫方法 , 其指向的地址設置成符號地址 , ( 例如工程中有一個
NSLog
, 那麼編譯時就會在Mach-O
中建立一個NSLog
的符號 , 工程中的NSLog
就指向這個符號 )運行時 : 當
dyld
將應用進程加載到內存中時 , 根據load commands
中列出的須要加載哪些庫文件 , 去作綁定的操做 ( 以NSLog
爲例 ,dyld
就會去找到Foundation
中NSLog
的真實地址寫到_DATA
段的符號表中NSLog
的符號上面 )
這個過程被稱爲 PIC
技術 . ( Position Independent Code : 位置代碼獨立 )
瞭解了系統函數的整個加載過程 , 咱們來看 fishhook
的函數名稱 :
rebind_symbols :: 重綁定符號
也就簡單明瞭了.
fishhook
原理就是 :
將編譯後系統庫函數所指向的符號 , 在運行時重綁定到用戶指定的函數地址 , 而後將原系統函數的真實地址賦值到用戶指定的指針上.
新建一個空 app
工程 , 在 ViewController
中添加 load
方法 .
+ (void)load{
NSLog(@"load 來了");
}
複製代碼
load
方法添加斷點 . 運行程序 . 查看函數調用棧 .
經過 lldb
: bt
+ up
/ down
指令來到入口 _dyld_start
處 .
上圖第 11
行 : call
就是調用函數的指令 , ( 同 bl ) . 這個函數也就是咱們 app
開始的地方 .
當咱們點開一個應用 , 系統內核會開啓一個進程 , 而後由 dyld
開始加載這個可執行文件 .
dyldbootstrap::start
就是指 dyldbootstrap
這個命名空間做用域裏的 start
函數 .
來到源碼中 , 搜索 dyldbootstrap
, 而後找到 start
函數 .
cmd + shift + j
能夠定位文件位置
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[],
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
slide = slideOfMainExecutable(dyldsMachHeader);
bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
shouldRebase = true;
#endif
if ( shouldRebase ) {
rebaseDyld(dyldsMachHeader, slide);
}
mach_init();
const char** envp = &argv[argc+1];
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
複製代碼
這個函數首先有兩個參數咱們要說明一下 :
1️⃣、
const struct macho_header* appsMachHeader
, 這個參數就是Mach-O
的header
. 關於這個header
, Mach-O文件 這篇文章中Mach-O 文件結構
裏有詳細描述 .2️⃣、
intptr_t slide
, 這個其實就是 ALSR , 說白了就是經過一個隨機值 ( 也就是咱們這裏的 slide ) 來實現地址空間配置隨機加載 .
當某個特定進程,在存儲器中所可以使用與控制的地址空間在運行時隨機進行分配 , 可使某些攻擊者沒法事先獲知地址 ,令攻擊者難以經過固定地址獲取函數或者內存值進行攻擊 .
Mac OS X Lion10.7
開始全部的應用程序均提供了ASLR
支持 .3️⃣、 物理地址 =
ALSR
+ 虛擬地址 ( 偏移 ) .
那麼接下來 , 這個函數到底作了什麼呢 ?
流程以下 :
首先 , 根據計算出來的 ASLR
的 slide
來重定向 macho
.
初始化 , 容許 dyld
使用 mach
消息傳遞 .
棧溢出保護 .
初始化完成後調用 dyld
的 main
函數 ,dyld::_main
.
直接點擊跳轉到 dyld
- main
函數中 . 該函數是加載 app
的主要函數.
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
// *函數太長 , 這裏就不貼了.*/
}
複製代碼
這個函數主要流程以下 :
setContext
.configureProcessRestrictions
, 檢測環境變量 checkEnvironmentVariables
.
- 熟悉越獄插件的同窗應該都很清楚 , 某些環境變量會直接影響該庫是否會被加載 , 有些防禦操做就是基於這個原理來作的 . ( 後續更新越獄篇章攻防會詳細講述和演示 )
DYLD_PRINT_OPTS
與 DYLD_PRINT_ENV
, 你們能夠在以下圖中配置玩一玩 .
getHostInfo
.該流程主要步驟以下 :
1️⃣ : 檢測共享緩存禁用狀態 checkSharedRegionDisable
. ( iOS 下不會被禁用 ) .
2️⃣ : 加載共享緩存庫 , mapSharedCache
-> loadDyldCache
.這裏加載共享緩存有幾種狀況 :
mapCachePrivate
, ( 模擬器僅支持加載到當前進程 ) .mapCacheSystemWide
.sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
複製代碼
實例化主程序 , 檢測可執行程序格式 .
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
裏就會經過 header
裏的 magic
, cputype
, cpusubtype
去檢測是否兼容 .
instantiateMainExecutable
實例化這個 image , 並添加到
static std::vector<ImageLoader*> sAllImages;
這個全局的鏡像列表中去 , 設置好上下文 .
instantiateMainExecutable
裏 , 真正實例化主程序是用 sniffLoadCommands
這個函數去作的 . 有的同窗可能對這個函數比較熟悉了 . 咱們來稍微看一下 .
仍是 ImageLoaderMachO
這個做用域裏的 sniffLoadCommands
函數 .
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;
}
複製代碼
這個函數就是根據 Load Commands
來加載主程序 .
這裏幾個參數咱們稍微說明下 :
compressed
-> 根據 LC_DYLD_INFO_ONYL
來決定 .segCount
段命令數量 , 最大不能超過 255
個.libCount
依賴庫數量 , LC_LOAD_DYLIB (Foundation / UIKit ..)
, 最大不能超過 4095
個.codeSigCmd
, 應用簽名 , 在 應用簽名原理及重簽名 (重籤微信應用實戰) 這篇文章中有很是詳細的講述 , 建議讀一讀 .encryptCmd
, 應用加密信息 , ( 咱們俗稱的應用加殼 , 咱們非越獄環境重簽名都是須要砸過殼的應用才能調試 , 關於應用的砸殼 , 後續逆向文章越獄篇裏會實際操做演練 ) .通過以上步驟 , 主程序的實例化就已經完成了 .
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
複製代碼
熟悉越獄插件的同窗應該很清楚這個機制了 . 根據 DYLD_INSERT_LIBRARIES
環境變量來決定是否須要加載插入的動態庫 .
越獄的插件就是基於這個原理來實現只須要下載插件 , 就能夠影響到應用 . 有部分防禦手段就用到了這個環境變量 ( 後續逆向文章會帶着你們本身寫一個越獄插件 , 這個很簡單 , 而後會講一講越獄環境插件如何防禦 . ) .
sInsertedDylibCount = sAllImages.size()-1;
記錄插入動態庫的數量 .
// link main executable
gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
if ( sInsertedDylibCount > 0 ) {
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();
}
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing(gLinkContext);
}
}
複製代碼
點擊進入 link
函數 , link
函數中有一系列 recursiveLoadLibraries
, recursiveBindWithAccounting -> recursiveBind
, 也就是遞歸進行符號綁定的過程 .
link
函數執行完畢以後 , dyld :: main
會調用 sMainExecutable->weakBind(gLinkContext);
進行弱綁定 , 懶加載綁定 , 也就是說弱綁定必定發生在 其餘庫連接綁定完成以後 .
綁定的過程就是咱們上述 1.2
章節中所講的共享緩存綁定的過程 .
走到了這裏 , 主程序已經實例化完畢 , 但尚未加載 ,
framework
已經加載完畢了 , 那講到這插一句題外話 , 不一樣framework
, 誰先會被加載 ? 其實根據二進制順序有關 ,Xcode
中能夠自由調整 .
拖動就能夠本身調整順序了 , 編譯順序就會根據這個順序來 , 一樣你可使用 MachOView
來查看二進制順序 .
至此 , 配置環境變量 -> 加載共享緩存 -> 實例化主程序 -> 加載動態庫 -> 連接動態庫 就已經完成了 .
繼續往 dyld :: main
下面找 , 咱們會看到
initializeMainExecutable();
複製代碼
那麼咱們回到函數調用棧看下 .
經過查看源碼查看 , 結合函數調用棧 , 咱們跟進去調用流程 . initializeMainExecutable
-> runInitializers
-> processInitializers
-> 遞歸調用 recursiveInitialization
.
到了這裏 , 直接點擊 進不去了 , 同理 , cmd
+ shift
+ o
, 搜索 recursiveInitialization
. 來到函數實現 , 找到以下代碼 :
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
複製代碼
調用 notifySingle
函數 .
⚠️ : 重頭戲來了 . 根據函數調用棧咱們發現 , 下一步是調用 load_images
, 但是這個 notifySingle
裏並無找到 load_images
的影子 . 可是咱們看到了這麼個東西 :
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
複製代碼
這是個回調函數的調用 ,
sNotifyObjCInit
上面判斷了並不會爲空 , 那就表明必定是有值的 . 那咱們搜索一下sNotifyObjCInit
, 看看何時被賦的值 .
直接本文件搜索 , 看到以下 :
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}
// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem) for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) { ImageLoader* image = *it; if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) { dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0); (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()); } } } 複製代碼
也就是說 , 這個函數調用 , 其第二個參數賦值給了 sNotifyObjCInit
, 而後在 notifySingle
裏被執行 .
那麼咱們搜索一下 registerObjCNotifiers
, 看看其在何時被調用的 , 搜索發現 :
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
, 運行來到斷點 , 看函數調用棧 .
runtime
被加載的整個流程 , 來到
objc 750
的代碼中直接搜索
_objc_init
.
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);
}
複製代碼
來到這裏 , 咱們就看到了 _dyld_objc_notify_register
被調用 , 傳遞了三個參數 , 這三個分別表明 在 分類底層原理詳細研究 中咱們也有詳細講述過 .
map_images
:dyld
將image
加載進內存時 , 會觸發該函數.load_images
:dyld
初始化image
會觸發該方法. ( 咱們所熟知的load
方法也是在此處調用 ) .unmap_image
:dyld
將image
移除時 , 會觸發該函數 .
固然 , 你能夠經過 lldb
驗證一下 .
那麼這個 load_images
, 就調用了各個類的 load
方法 ( call_load_methods
) . 關於這個請看 分類底層原理詳細研究 與 load方法調用機制解析 這兩篇文章 .
要聲明一下的是 :
那麼也就是說 :
- 1️⃣、 當
dyld
加載到開始連接主程序的時候 , 遞歸調用recursiveInitialization
函數 .- 2️⃣、 這個函數第一次執行 , 進行
libsystem
的初始化 . 會走到doInitialization
->doModInitFunctions
->libSystemInitialized
.- 3️⃣、
Libsystem
的初始化 , 它會調用起libdispatch_init
,libdispatch
的init
會調用_os_object_init
, 這個函數裏面調用了_objc_init
.- 4️⃣、
_objc_init
中註冊並保存了map_images
,load_images
,unmap_image
函數地址.- 5️⃣ : 註冊完畢繼續回到
recursiveInitialization
遞歸下一次調用 , 例如libobjc
, 當libobjc
來到recursiveInitialization
調用時 , 會觸發libsystem
調用到_objc_init
裏註冊好的回調函數進行調用 . 就來到了libobjc
, 調用load_images
.
跟咱們上面截圖的函數調用棧如出一轍 .
dyld
來到 doInitialization
時 ,
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);
}
複製代碼
在 doModInitFunctions
中 , 值得一提的是會調用 c++
的構造方法 .
演示以下 :
打印結果 :這種 c++
構造方法存儲在 __DATA
段 , __mod_init_func
節中.
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
複製代碼
找到真正 main
函數入口 並返回.
以上即是 dyld
加載應用的完整流程 . 建議你們仔細探索 .