dyld 加載流程

前言

小測驗:

  • 透過以下三種函數請問加載順序為何?
  • +load方法,main()函數,C++方法
  • 通過運行程序可以知道,打印順序為load → C++ → main函數

  • 為了瞭解為何運行的程序是這樣的順序,我們進行以下分析

  • 在分析程序前,我們先補充一些底層知識

編譯過程

  • 在我們將運行程序前,會進行編譯的動作,主要有以下幾步
    • 源文件:載入.h .m .cpp文件
    • 預編譯:替換宏,刪除註釋,展開頭文件,產生.i文件
    • 編譯 :將.i文件轉換為匯編語言,產生.s文件
    • 匯編 : 將匯編文件轉換為其他機器碼文件,產生.o文件
    • 鏈接 :對.o文件中引用其他庫的地方進行引用,生成最後的可執行文件
  • 編譯過程流程圖

靜態庫與動態庫

  • 靜態庫:在鏈接時會將編譯後的彙編代碼生成出來的目標程序與引用的庫文件一起鏈接打包為可執行文件,此時靜態庫就不會改變,而是拷貝了一份到目標程序裡。
    • 優勢:編譯後,對於目標程序沒有多餘的依賴,可直接運行。
    • 劣勢:由於靜態庫會有兩份,所以會導致目標程序體積增大,對於內存,性能,速度消耗很大
  • 動態庫:程序編譯時並不會鏈接到目標程序中,目標程序只會存儲指向動態庫的引用,在程序運行時才被載入
    • 優勢
      • 減少打包後的app的大小:因為不需要拷貝到目標程序中,所以不會影響到目標程序的體積,與靜態庫相比,減少了app的體積大小。
      • 共享內存:同一份庫可以被多個程序使用。
      • 方便更新程序:由於運行時才載入的特性,可以隨時對庫進行替換,不需要重新編譯代碼。
    • 劣勢:動態載入會帶來一部分性能損失,使用動態庫也會是目標程序對於外部依賴,如果缺少了動態庫則回造成程序無法運行。

dyld加載流程分析

  • 以下將根據dyld源碼,搭配libobjclibSystemlibdispatch 源碼分析

  • dyld(dynamic link editor) 是蘋果動態鏈接器,在app被編譯打包成Mach-O文件時,交由dyld負責鏈接,加載程序

  • APP啟動流程圖如下

app啟動的起始點

  • 在上述的測驗中,我們想知道app的起始點,可以從我們已知的入手,也就是剛剛的第一個調用的函數+load(),進行分析。
  • 通過bt打印堆棧信息,查看app啟動是從哪裡開始的
  • 可以看到是從dyld中的_dyld_start開始的,所以需要去下列網址下載原始碼進行分析

Source Browser

  • 另外也可以透過_dyld_start

dyld::_main源碼分析

  • 如下為版本dyld750.6 源碼查找_dyld_start 可以在匯編代碼的arm64架構下看到註釋 也就是程式會運行到dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue) 這個函數為C++函數
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)

  • 源碼中搜索dyldbootstrap 找到命名作用空間,在這個文件中查找start方法,其核心就是返回值就是調用dyld::main函數,其中macho_header是Mach-O的頭部,而dyld加載的文件就是Mach-O類型的,即Mach-O類型是可執行文件類型,Mach-O header,Load command ,section,Other Data,可以通過MachOViewt查看。
  • 可透過MachOview查看文件各個區域

【第一步】配置環境變量

  • 根據環境變量設置相應的值以獲取當前運行的架構。

【第二步】共享緩存

  • 檢查是否開啟共享緩存,以及共享緩存是否緩存映射到共享區域,例如UIKit,coreFoundation

【第三步】主程序初始化

  • 調用instantiateFromLoadedImage函數實例化一個ImageLoader對象

【第四步】插入動態庫

  • 遍歷sEnv.DYLD_INSERT_LIBRARIES環境變量,調用加載loadInsertedDylib方法

【第五步】鏈接主程序

  • 鏈接主程序

【第六步】鏈接動態庫

【第七步】弱符號綁定

【第八步】執行初始化方法

【第九步】尋找主程序入口main函數:

  • 從Load Command讀取LC_MAIN入口,如果沒有就讀取LC_UNIXTHREAD,這樣就來到了日常開發的main函數了。
  • 以下主要將分析第三步及第八步
  • 【第三步】主程序初始化
  • sMainExecutable表示主程序變量,查看其賦值,是通過instantiateFromLoadedImage 方法初始化
  • 進入instantiateFromLoadedImage 源碼,其中創建一個ImageLoader 實例對象,通過instantiateMainExecutable 方法創建
  • 進入instantiateMainExecutable 源碼,其作用是為主可執行文件創建映像,返回一個ImageLoader 類型的image對象,即主程序。其中sniffLoadCommands函數時獲得Mach-O類型文件的Load Command的相關信息,並對其進行各種校驗。
  • 【第八步】執行初始化方法
  • 進入initializeMainExecutable 源碼,主要是循環遍歷,都會執行runInitializers 方法
  • 全局搜索runInitializers(cons ,找到如下源碼,其核心代碼是processInitializers 函數的調用
  • 進入processInitializers 函數的源碼實現,其中對鏡像列表調用recursiveInitialization 函數進行遞歸實例化。
  • 全局搜索recursiveInititalization(cons 函數,其源碼實現如下
  • 在這裡,需要分成兩個部分探索,一部分是notifySingle 函數,一部分是doInitialization 函數首先探索notifySingle 函數

notifySingle函數

  • 全局搜索notifySingle( 函數,其重點是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader()); 這句
  • 全局搜索sNotifyObjCInit 發現沒有找到實現,有賦值操作
  • 搜索registerObjCNotifiers在哪裡調用了,發現在_dyld_objc_notify_register 進行了調用

注意:_dyld_objc_notify_register 的函數需要在libobjc源碼中搜索

  • objc4-781 源碼中搜索_dyld_objc_notify_register ,發現在_objc_init 源碼中調用了該方法,並傳入了參數,所以sNotifyObjCInit 的賦值就是objc中的load_images,而load_images會調用所有的+load方法,綜上所述,notifySingle是一個回調函數

load函數加載

下面我們進入load_images 的源碼看看其實現,以此來證明load_images 中調用了所有的load函數

  • 通過objc源碼中_objc_init源碼實現,進入load_images的源碼實現
  • 進入call_load_methods 源碼實現,可以發現其核心是通過do-while 循環調用+load 方法
  • 進入call_class_loads源碼實現,了解到這裏調用的load方法證實我們前文提及的類load方法
  • 所以,load_images調用了所有的load函數,以上的源碼分析過程正好對應堆棧的打印信息
  • 【總結】load的源碼鏈結為:

_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle (回調函數) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)

  • 那麼_objc_init 是什麼時候調用的呢?

doInitialization 函數

  • 走到objc_objc_init函數,發現不通了,我們退回到recursiveInitialization 遞歸函數的源碼實現,發現我們忽略一個函數doInitialization
  • 進入doInitialization 函數的源碼實現
  • 這裏也需要分成兩部份,一部分是doImageInit 函數,一部分是doModInitFunctions 函數。
  • 進入doImageInit 源碼實現,其核心主要是for循環加載方法的調用,這裏需要注意的一點是,libSystem的初始化必須先運行
  • 進入doModInitFunctions 源碼實現,這個方法中加載了所有Cxx文件
  • 可以通過測試程序的堆棧信息來驗證,在C++方法處加一個斷點
  • 走到這裡,還是沒有找到_objc_init的調用,我們通過_objc_init 加一個符號斷點來查看調用_objc_init前的堆棧信息。
  • _objc_init 加一個符號斷點,運行程序,查看_objc_init 斷住後的堆棧信息
  • libsystem中查找libSystem_initializer ,查看其中實現
  • 根據前面的堆棧信息,我們發現走的是libSystem_initializer 中會調用libdispatch_init 函數,而這個函數的源碼是在libdsipatch開源庫中的,在libdispatch中搜尋libdispatch_init
  • 進入_os_object_init 源碼實現,其源碼實現調用了_objc_init 函數
  • 結合上面的分析,從初始化_objc_init 註冊的_dyld_objc_notify_register 的參數2,即load_images ,到sNotifySingle —>sNotifyObjCInie=參數2sNotifiyObjcInit()調用,形程了一個閉環

  • 所以可以簡單的理解sNotifySingle 這裡是添加通知即addObsever_objc_init中調用_dyld_objc_notify_register 相當於發送通知,即push ,而sNotifyObjcInit 相當於通知的處理函數,即selector

【總結】:_objc_init的源碼鏈:

_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

第九步:尋找主入口函數

  • 匯編調適,可以看到顯示來到+[ViewController load] 方法
  • 繼續執行,來到kcFuna的C++函數
  • 點擊stepover繼續往下,讀取寄存器rax 可以看到會進入main函數
  • dyld彙編源碼實現

注意:main是寫死的函數,寫入內存,讀取到dyld,如果修改了main函數的名稱,會報錯如下

所以,最終dyld加載流程,如下圖所示

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 前言 在上一篇中我們了解了dyld加載的流程,此篇我們將介紹dyld與objc的關聯。 dyld 加載流程[htt...
    黑足小貓咪阅读 231评论 0 0
  • 前言 在我們知道了cache寫入sel-imp流程後,接下來我們探討寫入之前的消息發送的流程 cache原理分析[...
    黑足小貓咪阅读 195评论 0 0
  • 問題一:類存在幾份 類的信息存在了類的isa的shiftcls區域中,而這樣的類信息只有一份,所以類對象只有一份 ...
    黑足小貓咪阅读 81评论 0 1
  • 前言 在上一篇快速查找提到,如果快速查找無法找到相對應的方法,則會進入慢速查找流程,此篇重點在於慢速查找。 obj...
    黑足小貓咪阅读 237评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,575评论 16 22