【iOS應用啟動(一)】dyld與main函式

語言: CN / TW / HK

theme: channing-cyan

Hi 👋

| 我的個人專案 | 掃雷Elic 無盡天梯 | 夢見賬本 | |:----:|:----:|:----:| |型別|遊戲|財務| |AppStore|Elic|Umemi|

本文基於 dyld-832.7.3objc4-818.2 原始碼

前言

每個應用程式都會依賴很多的庫,每當應用程式啟動,都會將MachO中的可執行檔案載入到記憶體中。

那麼這個過程是怎樣的呢?

iOS應用啟動流程-簡要載入過程.png

一、 切入點

應用程式的入口是 main 函式,那麼 main 函式之前做了什麼呢?

我們在 main 函式下個斷點:

start-dyld01.png

(lldb) bt * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 * frame #0: 0x00000001001f9ef8 ObjcMsgSend`main(argc=1, argv=0x000000016fc0b878) at main.m:17:50 frame #1: 0x00000001a2d56140 libdyld.dylib`start + 4

發現並沒有有用的資訊。那麼在main之前下斷點試試吧。

start-dyld02.png

通過分析堆疊資訊發現切入點 _dyld_start

二、 dyld-動態連結器

2.1 _dyld_start

dyld原始碼中找到了對應實現,為彙編程式碼,我們找到一處重要的註釋:

start-dyld03.png

dyldbootstrap::start

start-dyld04.png

前面都是進行一些配置,最後一個看名字就很重要的 dyld::_main 我們進去看看

2.2 dyld::_main

一千多行程式碼,果然很重要。

通過註釋我們也可以看出:

  • 這裡是 dyld 的入口
  • 最終返回程式的 main() 函式

因為程式碼非常長,我對於關鍵點做了一些註釋方便大家對照原始碼進行檢視

start-dyld05.png

原始碼流程註釋對照

  • 只要設定了這兩個環境變數引數,在App啟動時就會列印相關引數、環境變數資訊

start-dyld17.png

  • 載入共享快取

start-dyld16.png

  • 為主程式初始化 ImageLoader

start-dyld15.png

  • 現在共享快取已經載入完畢了,設定版本化的dylib覆蓋

start-dyld14.png

  • 載入所有插入的庫,越獄外掛在這裡加入
  • 記錄插入的庫的數量,以便統一搜索將先檢視插入的庫,然後是main,然後是其他。

start-dyld13.png

  • 連結主程式

start-dyld12.png

  • 連結所有插入的庫。連結主可執行檔案後執行此操作,以使插入的dylib(例如libSystem)不在程式使用的dylib的前面

start-dyld11.png

  • 只有插入的庫可以插入。繫結所有插入的庫後,註冊插入資訊,以便連結工作

start-dyld10.png

  • 繫結並通知主要可執行檔案,現在插入的已被註冊
  • 繫結並通知現在已插入的已插入image已被註冊

start-dyld09.png

  • 執行所有初始化方法
  • 通知任何監視過程此過程即將進入main()

start-dyld08.png

  • 查詢main指標

start-dyld07.png

2.3 sMainExecutable

從上面的流程分析中可以看出,最終returnmain函式指標 來自於 sMainExecutable

可以定位 sMainExecutable 的初始化地方

start-dyld15.png

2.4 initializeMainExecutable 初始化主程式

核心原始碼及流程註釋

```C++ void initializeMainExecutable() {

pragma mark - Ryukie 記錄一下,進入初始化流程了

// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;

pragma mark - Ryukie 初始化所有插入的庫

// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
    for(size_t i=1; i < rootCount; ++i) {
        sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
    }
}

pragma mark - Ryukie 執行主要可執行檔案的初始化程式及其依賴的

// run initializers for main executable and everything it brings up 
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

...

} ```

結合前面我們在 +load 處斷點的堆疊資訊可以得到驗證:

start-dyld18.png

主線過程我們就分析到這裡,細節的流程後面的文章會繼續進行分析

三、 總結&流程圖

dyld:_main.png

  • 程式執行從 _dyld_start 開始
  • 進入 dyld_main 函式
  • 配置環境變數以及 rebase_dyld
  • 載入共享快取
  • 系統的動態庫都在這裡了
  • dyld2/dyld(ClosureMode) 決定以哪種模式繼續進行
  • 例項化主程式ImageLoaderMachO,加入到allImages
  • 到此共享快取載入完畢,設定版本化的dylib覆蓋
  • Now that shared cache is loaded, setup an versioned dylib overrides
  • 載入插入的庫
  • 越獄外掛在這裡加入
  • 記錄插入的庫的數量,以便統一搜索將先檢視插入的庫,然後是main,然後是其他。
  • record count of inserted libraries so that a flat search will look at inserted libraries, then main, then others.
  • 連結主程式
  • 繫結符號(非懶載入、弱符號)等
  • 連結所有插入的庫
  • 連結主可執行檔案後執行此操作,以使插入的dylib(例如libSystem)不在程式使用的dylib的前面
  • 只有插入的庫可以插入。繫結所有插入的庫後,註冊插入資訊,以便連結工作
  • 繫結並通知主程式可執行檔案,現在插入的已被註冊
  • 繫結並通知現在已插入的庫已插入的Image已被註冊
  • 執行所有初始化方法:ImageLoader
  • 初始化所有插入的庫、執行主要可執行檔案的初始化程式及其依賴
  • ImageLoader:
    • runInitializers:
      • processInitializers:
      • ecursiveInitialization:
        • notifySingle:
        • 此函式執行一個回撥,此回撥是_objc_init初始化時服飾的一個函式load_images
          • load_images裡執行class_load_metgods函式
          • class_load_metgods裡呼叫call_class_loads函式:迴圈呼叫各個類的load函式
        • doModInitFunction
        • 內部會呼叫全域性C++物件的建構函式__attribute__((constructor))的C函式
  • 通知任何監視過程此過程即將進入main()
  • 得到main()指標並return