【iOS】dyld加載流程——應用程序的加載

目錄

前言

編譯過程與動靜態庫

編譯過程

動靜態庫

dyld

📌 什么是 dyld?

dyld_shared_cache:

dyld加載流程

_dyld_start

dyldbootstrap::start

dyld::main()

配置環境變量

共享緩存

主程序的初始化

插入動態庫

link主程序

link動態庫

弱符號綁定

執行初始化方法

程序啟動時(加載鏡像)

類首次被訪問時

程序退出時(卸載鏡像)

動態庫的卸載(例如插件機制)

尋找主程序入口


前言

我們平時編寫的程序的入口函數都是main.m文件里面的main函數,但在重新load方法后,會發現load方法是在main函數之前執行的,那么在main函數之前到底發生了哪些事呢?這篇文章我們就來探究一下。

編譯過程與動靜態庫

編譯過程

當我們在編譯器上按下按鈕進行開發調試時,編譯器其實幫我們做了許多事,整個過程可以拆分成四個步驟:預處理、編譯、匯編和鏈接

這四個步驟會完成這些事:

  • 預處理:處理#開頭的預處理指令,替換宏、展開頭文件、刪除注釋,輸出中間文件:.i

  • 編譯:對.i文件進行詞法、語法和語義分析,執行代碼優化,生成匯編代碼,輸出中間文件.s

  • 匯編:將.s匯編文件翻譯成機器碼,輸出目標文件.o

  • 鏈接:將多個.o文件與系統庫、框架等一起鏈接成可執行文件,解決函數/變量引用、地址重定位等,輸出最終文件:可執行程序。在這個過程中,鏈接器將不同的目標文件鏈接起來,因為不同的目標文件之間可能有相互引用的變量或調用的函數,如我們經常調用Foundation框架和UIKit 框架中的方法和變量,但是這些框架跟我們的代碼并不在一個目標文件中,這就需要鏈接器將它們與我們自己的代碼鏈接起來

動靜態庫

Foundation`和`UIKit`這種可以共享代碼、實現代碼的復用統稱為`庫`——它是可執行代碼的二進制文件,可以被操作系統寫入內存,它又分為`靜態庫`和`動態庫

靜態庫靜態庫是一種將代碼編譯后封裝起來的二進制文件,在程序編譯鏈接階段被打包進最終的可執行文件中,運行時不再依賴外部庫文件。但同時由于需要將庫復制進最終程序,會使最終可執行文件體積變大。如.a、.lib都是靜態庫

動態庫動態庫(Dynamic Library),也稱為 共享庫,是指在程序運行時動態加載的代碼模塊,不會在編譯時被打包進可執行文件中,而是以共享形式存在,運行時由操作系統加載。多個程序可以共享同一個動態庫的實例,系統只需加載一次動態庫,可以節省內存。如.dylib、.framework都是動態庫

dyld

📌 什么是 dyld?

dyld(Dynamic Link Editor) 是蘋果操作系統中的 動態鏈接器,負責在程序啟動時將程序依賴的 動態庫(dynamic libraries)加載到內存,并完成 符號解析與重定位,以確保程序能夠正常運行。在應用被編譯打包成可執行文件格式的Mach-O文件之后 ,交由dyld負責鏈接,加載程序。

所以應用程序啟動應該是如下流程:

dyld_shared_cache:

為了優化程序啟動速度和利用動態庫緩存,蘋果從iOS3.1之后,將所有系統庫(私有與公有)編譯成一個大的緩存文件,這就是dyld_shared_cache,該緩存文件存在iOS系統下的/System/Library/Caches/com.apple.dyld/目錄下

dyld加載流程

我們在load方法和main方法處加一個斷點,使用LLDB——bt指令打印,可以看到最初的起點。在最新的xcode中,這個函數是start

而筆者在閱讀博客時,最初的起點是_dyld_start,而dyldbootstrap這個命名空間作用域里存在著這個start函數,_dyld_start會調用這個函數,整個啟動邏輯應該差別不大,這里先按照博客中閱讀的來講解

_dyld_start

在dyld源碼中可以搜索到_dyld_start這個函數,發現它是匯編實現的,并且它調用了dyldbootstrap::start方法。

截屏2025-05-11 03.38.02

dyldbootstrap::start

dyldbootstrap::start是指dyldbootstrap這個命名空間作用域里的 start函數。通過命名空間找到這個方法,方法的核心是返回值調用dyld的main函數,第一個參數是一個Mach-O(可執行文件)的頭部。Mach-O類型分為四個部分:Mach-O頭部Load CommandsectionOther Data。

這個函數主要進行了一些 dyld 自身狀態的初始化,進行重定位、棧溢出保護、參數解析等等,最重要的一步是調用dyld::_main()(真正開始啟動app)。

dyld::main()

dyld::main的主要流程為:

  • 配置環境變量:創建一個 RuntimeState 或類似結構體,保存當前 App 路徑、argc/argv/env/apple、主程序的 header、slide 等啟動信息。為接下來 image 加載和綁定做準備。(根據環境變量設置相應的值以及獲取當前運行架構)

  • 加載共享緩存:優先從預編譯的 dyld shared cache(系統框架緩存)中加載常用系統庫(如 libSystem、Foundation 等),提高性能。 如果無法使用 shared cache,就退回到逐個加載(fallback 邏輯)。

  • 主程序初始化:通過 instantiateFromLoadedImage()實例化主程序 image(Mach-O 文件),構造一個 ImageLoader 實例(封裝 image 加載行為)

  • 插入動態庫:解析環境變量 DYLD_INSERT_LIBRARIES,加載用戶或調試器插入的動態庫。

  • Link 主程序:遞歸解析主程序的依賴庫(LC_LOAD_DYLIB),完成主程序的符號綁定與鏈接(如解析外部函數地址)

  • Link 動態庫:將主程序依賴的所有 dylib 遞歸 link 完成,

  • 弱符號綁定

  • 執行初始化方法:執行所有 image 的 mod_init_funcs(C++ 構造函數、ObjC +load 等)

  • 尋找并跳轉到主程序入口 main()

配置環境變量

平臺,版本,路徑,主機信息的確定

共享緩存

checkSharedRegionDisable檢查是否開啟共享緩存(在iOS中必須開啟)

mapSharedCache加載共享緩存庫,其中調用loadDyldCache函數有這么幾種情況:

  • 僅加載到當前進程mapCachePrivate(模擬器僅支持加載到當前進程)

  • 共享緩存是第一次被加載,就去做加載操作mapCacheSystemWide

  • 共享緩存不是第一次被加載,那么就不做任何處理

主程序的初始化

調用instantiateFromLoadedImage函數實例化了一個imageLoader對象

進入instantiateFromLoadedImage方法,其中創建了一個ImageLoader實例對象,通過instantiateMainExecutable方法創建。

進入instantiateMainExecutable源碼,其作用是為主可執行文件創建映像,返回一個ImageLoader類型的image對象,即主程序。其中sniffLoadCommands函數會獲取Mach-O類型文件的Load Command的相關信息,并對其進行各種校驗

插入動態庫

遍歷DYLD_INSERT_LIBRARIES環境變量,調用loadInsertedDylib加載,通過該環境變量我們可以注入自定義的一些動態庫代碼從而完成安全攻防,loadInsertedDylib內部會從DYLD_ROOT_PATH、LD_LIBRARY_PATH、DYLD_FRAMEWORK_PATH等路徑查找dylib并且檢查代碼簽名,無效則直接拋出異常

link主程序

這里rebase函數執行的是重定位,即將原本指向自己的段內部地址,修正為加載到內存中的真實地址。

link動態庫

弱符號綁定

這里符號綁定(bind)是將原本指向自己的段內部地址,修正為加載到內存中的真實地址。

執行初始化方法

從函數調用棧里可以發現進入dyld::main()函數后,初始化的起點是dyld::initializeMainExecutable,進入initializeMainExecutable源碼,主要是循環遍歷去執行runInitializers

sImageRoots 是一個鏡像根列表,第 0 個是主程序,其它的是插入的動態庫;

遍歷這些庫,調用 runInitializers() 執行構造函數(構造器、initializer);

sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);這一行初始化主程序及其依賴的動態庫

找到runInitializers的源碼,核心代碼是 processInitializers,為初始化做準備

進入processInitializers函數的源碼實現,其中核心部分是對鏡像列表調用recursiveInitialization函數進行遞歸實例化。

這里的鏡像列表包含的是還未執行初始化的鏡像對象,主要包括:主程序本身、插入的動態庫、主程序依賴的系統或用戶動態庫、間接依賴庫。

找到recursiveInitialization函數,作用是獲取到鏡像的初始化

這里我們分成兩個部分來看,一部分是notifySingle函數,另一部分是負責初始化鏡像的doInitialization函數,首先探索notifySingle函數

源碼中與啟動流程相關的重點是一句通過靜態全局函數指針調用的回調,函數指針sNotifyObjCInit在別的地方賦值(即注冊回調),dyld在合適的時機進行調用(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());

現在我們來探索在哪里對這個函數指針進行了賦值,全局搜索sNotifyObjcInit,可以找到賦值操作

搜索registerObjCNotifiers在哪里調用,發現只在_dyld_objc_notify_register里調用了,這個函數只在運行時提供給objc使用。

這時,_dyld_objc_notify_register的源碼需要在libobjc源碼中搜索,libobjc 是 Objective-C(OC)語言的核心庫之一,負責處理運行時的動態行為。為什么要去libobjc中搜索,因為dyld 本身不負責 Objective-C 的類注冊、+load 調用等邏輯,這些是 libobjc 的職責

dyld 只知道:

  • 某個鏡像(dylib)被加載了

  • 某個鏡像初始化完成了

所以 dyld 會說:

“我通知你(libobjc),該你干活了。”

libobjc 就會注冊回調進來,讓 dyld 通過這些回調「把控制權還給它」。

在objc源碼中搜索_dyld_objc_notify_register,發現在_objc_init的是實現中調用了該方法,并傳入了參數,所以sNotifyObjcInit中賦值的就是objc中的_load_images

這里的map_images、load_images、unmap_image三個函數都是在libobjc實現的,賦值給dyld中源碼,由dyld在合適的時機調用。load_images會調用所有的+load方法,可以繼續往里探索。

這里所謂合適的時機:具體的時機包括:

程序啟動時(加載鏡像)
  • dyld 初始化:在程序啟動時,dyld 會被加載并開始執行,負責加載主可執行文件以及其依賴的動態庫。在加載過程中,dyld 會調用 map_images 來將這些庫的內容映射到內存中。

  • 加載動態庫時調用 load_images:一旦鏡像被映射到內存,dyld 會調用 load_images 以確保庫中的初始化代碼得以執行,包括處理 Objective-C 類的元數據。此時,如果庫中包含 Objective-C 類,libobjc 會接管對類元數據的管理,確保它們被正確初始化。

類首次被訪問時
  • 延遲初始化:當程序第一次訪問某個類時,libobjc 會通過 load_images 完成該類的初始化,包括注冊類、方法解析、執行 +initialize 等操作。這意味著,雖然類的元數據在程序啟動時已經被加載到內存中,但類的完整初始化通常會延遲到第一次使用時。

程序退出時(卸載鏡像)
  • unmap_image 的調用時機:在程序退出時,dyld 會清理和卸載不再需要的動態庫,這時 unmap_image 會被調用,卸載那些已經加載的鏡像,并釋放相關資源。

動態庫的卸載(例如插件機制)
  • 如果程序在運行過程中動態加載和卸載插件或其他動態庫,dyld 會在合適的時機調用 unmap_image 來卸載鏡像。這通常發生在使用 dlclose 等系統調用卸載共享庫時。

進入cal_load_methods()的源碼

這里的核心就是do-while循環調用+load方法,上圖紅框中是類調用load方法,下面是類別調用load方法

進入call_class_loads函數

可以看到這里的實現其實就是直接通過SEL找到了load對應的IMP進行load方法的調用

注意:這里的類即所有類,鏡像列表中會包含所有類的鏡像——包括所謂的“懶加載類”,因為從底層實現來看,所有編譯時就存在的類,其信息都會打包進對應的 Mach-O 鏡像,并由 dyld 在啟動或動態加載庫時統一處理。

??懶加載 ≠ 沒被加載到內存懶加載類的“懶”只是指 +initialize 的執行不是立即的,而不是類的元數據未被加載。懶加載類的元數據同樣在dyld啟動時加載到內存中。

關于類別:

① 編譯時定義的分類(在 .m 文件中寫的 @interface MyClass (CategoryName)):

  • ? 會被編譯器生成元數據,并寫入到 Mach-O 文件的:

    • __objc_catlist(分類列表)

    • __objc_const(分類的方法、屬性等結構)

  • ? 加載過程:

    1. 程序啟動或動態庫加載時,dyld 加載鏡像。

    2. 調用 libobjcload_images()

    3. 遍歷 __objc_catlist,找到所有分類。

    4. 將分類合并到其主類(category -> class)上。

    5. 如果分類實現了 +load 方法,進入 call_category_loads() 執行。

  • ? 所以分類雖然不是類自身的一部分,但它們是隨著鏡像加載一起加載的

② 運行時動態注冊的分類:

  • 🧩 通過 API 手動添加,比如:

    extern void objc_addCategory(Class cls, Category *cat);

    注:這類函數不是公開 API,可能是私有或內部機制。

  • 🚫 不依賴 dyld,也不會寫在 Mach-O 的 __objc_catlist 中。

  • ? 它們注冊后也能像普通分類一樣擴展類的功能(方法、屬性等),但不是通過鏡像加載的,而是手動注冊進 runtime 的哈希表中。

然而,實現了+load的類,也會非懶的效果,因為從語義上,人們把“懶加載類”理解為:

等到我真正用它的時候,它才在內存中變得活躍。

+load 徹底破壞了這種“懶”:

  • 類注冊后立即調用 +load

  • 這個類的所有元數據都要準備好

  • 如果它還有分類有 +load,也會一并調用

因此,在行為上,這些類無法再被延遲加載使用,所以說它是“非懶加載類”。

類的元數據包括:objc_class、class_rw_t、class_ro_t與方法、屬性、變量、協議等列表結構(這些都來源于class_ro_t)

這時就又有了一個問題,_objc_init是什么時候調用的呢?還記得剛開始我們把recursiveInitialization分成了兩部分來看嗎?還有一部分就是doInitialization函數的源碼實現,現在我們來看看這個函數

這里也分成兩部分:doImageInit函數和doModInitFunctions函數

doImageInit函數:

這個函數主要是在鏡像加載完畢后,檢查當前 Mach-O 文件中是否存在 LC_ROUTINES_COMMAND(舊版 Mach-O 中用于指定初始化函數),如果有,就提取出 init_address,偏移 slide 后計算出真實地址,并立即調用它。

這個-init的作用是在 鏡像(動態庫或可執行文件)被 dyld 加載后立即執行某段初始化邏輯,目的是:

? 在程序或庫使用前,執行初始化代碼,配置運行環境、注冊資源、設置全局狀態等。

進入doModInitFunctions源碼,可以發現這個函數的重點就是加載了所有Cxx文件。

到現在還是不知道_objc_init何時調用,我們為它添加斷點查看堆棧信息可以發現在完成所有加載工作后,最先調用的函數是libSystem_initializer

在libsystem中查找libSystem_initializer,查看其中的實現:

可以發現會調用libdispatch_init函數,這個函數的源碼是在libdispatch開源庫中的,在libdispatch中搜索libdispatch_init

調用了_os_object_init函數

可以發現在這里調用了_objc_init

尋找主程序入口

最后,關于尋找主程序入口,底層通過匯編實現的,需要注意的是main是寫定的函數,寫入內存,讀取到dyld。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/89492.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/89492.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/89492.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

從零開始,手把手教你本地部署Stable Diffusion AI繪畫(Win最新版)

本號之前有發過一篇win平臺的教程,由于是去年10月發布的,而Al繪畫技術發展很快,那篇教程已經有些不適用了,有些同學執行到第二步就出錯了。 應廣大同學的期望,我更新一版新版詳細教程。 一、前言 1.為什么要本地部署…

day21 力扣669. 修剪二叉搜索樹 力扣108.將有序數組轉換為二叉搜索樹 力扣538.把二叉搜索樹轉換為累加樹

修剪二叉搜索樹 給你二叉搜索樹的根節點 root ,同時給定最小邊界low 和最大邊界 high。通過修剪二叉搜索樹,使得所有節點的值在[low, high]中。修剪樹 不應該 改變保留在樹中的元素的相對結構 (即,如果沒有被移除,原有的父代子代關…

《設計模式之禪》筆記摘錄 - 7.中介者模式

中介者模式的定義中介者模式的定義為:Define an object that encapsulates how a set of objects interact.Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently…

Flutter:上傳圖片,選擇相機或相冊:wechat_assets_picker

圖片選擇功能:可選單張,或多張。 1、showModalBottomSheet(選擇相冊/相機) 2、WechatImagePicker(選取圖片) 3、CompressMediaFile(圖片壓縮)1、ActionSheetUtilimport package:duca…

pytest--0

1 pytest 使用方式 pytest測試框架-- 基本功能使用詳解 2 pytest-mock常用方式 pytest–1–pytest-mock常用的方法 3

multiprocessing.Pool 中的 pickle 詳解

前言: 在 Python 的 multiprocessing.Pool 中,任務和數據需要通過序列化(pickle)傳遞給子進程。pickle 是 Python 的內置序列化模塊,用于將 Python 對象轉換為字節流,以便在進程間通信時傳遞。然而&#xf…

Java集合框架體系詳解:List/Set/Map接口對比與核心實現原理

一、集合框架核心接口對比 1.1 List/Set/Map接口特性接口類型特性描述典型實現List有序可重復,支持索引訪問ArrayList/LinkedListSet無序不可重復,基于哈希表或樹實現HashSet/TreeSetMap鍵值對存儲,鍵唯一值可重復HashMap/TreeMap核心差異&am…

LeafletJS 進階:GeoJSON 與動態數據可視化

引言 LeafletJS 作為一個輕量、靈活的 JavaScript 地圖庫,以其對 GeoJSON 數據格式的強大支持而聞名。GeoJSON 是一種基于 JSON 的地理數據格式,能夠表示點(Point)、線(LineString)、多邊形(Po…

【STM32實踐篇】:F407 時鐘系統

文章目錄1. 時鐘與啟動2. CubeMX 時鐘樹2.1 時鐘源2.2 PLL 鎖相環2.3 時鐘分發與選擇2.4 頻率限制1. 時鐘與啟動 復位默認時鐘:系統復位后,CPU 時鐘默認由 16MHz 內部 RC 振蕩器(HSI)提供,該 RC 振蕩器經工廠校準&…

純前端html實現圖片坐標與尺寸(XY坐標及寬高)獲取

純前端html實現圖片坐標與尺寸&#xff08;XY坐標及寬高&#xff09;獲取。用于證書圖片或pdf打印的坐標測定。 <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <title>純html前端實現圖片坐標與尺寸&am…

飛睿UWB超寬帶定位測距技術,數字鑰匙重塑智能生活,高精度厘米級定位無感解鎖

最近&#xff0c;數字鑰匙領域動作頻頻&#xff0c;科技巨頭與車企正掀起一波創新浪潮。小米15S Pro搭載恩智浦UWB芯片&#xff0c;用戶靠近閘機即可無感通行深圳云巴一號線&#xff0c;輕觸小米YU7車門自動解鎖&#xff0c;實現手機-汽車-公共交通的無縫數字鑰匙生態。在智能家…

基于springboot+vue+mysql平臺的醫療病歷交互系統(源碼+論文)

一、開發環境 相關技術介紹 B/S模式分析 C/S模式&#xff1a;主要由客戶應用程序(Client)、服務器管理程序(Server)和中間件(middleware)三個部件組成。客戶應用程序是系統中用戶與數據組件交互。服務器程序負責系統資源&#xff0c;如管理信息數據庫的有效管理。中間件負責連…

arm架構,arm內核,處理器之間的關系

一、情景分析 我們經常說&#xff0c;stm32f103是采用cotex-M3內核&#xff0c;基于armv7架構設計的。 那么&#xff0c;stm32f103、cotex-M3、armv7之間有什么關系呢&#xff1f; 二、層次分析 1. 架構&#xff08;Architecture&#xff09; 定義&#xff1a;架構是處理器…

基于PHP的招投標系統_603gk

目錄具體實現截圖課程項目技術路線開發技術介紹PHP核心代碼部分展示系統測試詳細視頻演示/源碼獲取具體實現截圖 課程項目技術路線 招投標系統后端采用 PHP 語言搭配Thinkphp或者 Laravel 框架&#xff0c;PHP 語法簡潔且功能強大&#xff0c;Laravel 或者Thinkphp框架能優化代…

深入解析 JavaScript 中的 `$.ajax()`:專業指南與實戰示例

文章目錄一、為什么需要 $.ajax()&#xff1f;二、核心語法解析三、關鍵參數深度剖析四、實戰示例&#xff1a;從基礎到進階五、錯誤處理最佳實踐六、性能與安全優化七、現代替代方案對比八、總結作為網站編輯&#xff0c;我將帶您深入剖析 jQuery 的 $.ajax() 方法。本文不僅涵…

Flutter 前端開發中的常見問題全面解析

Flutter 開發中的常見問題全面解析一篇給 Flutter 開發者「靈兒」里里外外都能看的問題項。從基礎開發到打包上線&#xff0c;每一步都充滿坑&#xff0c;我們詳細列出「環環盜光」的那些場景和解決思路&#xff01;【基礎系統】開發環境問題 1. flutter doctor 報錯 常見錯誤:…

STM32 單片機的停車場管理系統設計與實現

基于 STM32 的停車場管理系統設計與實現摘要隨著城市汽車保有量的快速增長&#xff0c;停車場管理的效率與智能化水平愈發重要。本文設計并實現了一套基于 STM32 單片機的停車場管理系統&#xff0c;整合車輛檢測、車位引導、計費管理及信息交互等功能。系統以 STM32 為控制核心…

STM32 寫選項字 關鍵要加載HAL_FLASH_OB_Launch

AI亂寫&#xff0c;還是得自己來&#xff01;void Write_OptionBytes_IWDG_STDBY(void) {FLASH_OBProgramInitTypeDef OBInit;HAL_FLASHEx_OBGetConfig(&OBInit); // 獲取當前選項字節配置[6,7](ref)// 檢查當前nRST_STDBY位&#xff08;IWDG_STDBY相關位&#xff09;是否…

153.在 Vue 3 中使用 OpenLayers + Cesium 實現 2D/3D 地圖切換效果

&#x1f3ac; 效果演示截圖 ? 前言 在實際項目開發中&#xff0c;我們經常需要提供「二維地圖 三維地形」的可視化效果切換&#xff0c;例如&#xff1a; 智慧農業展示耕地分布 三維地形起伏&#xff1b; 智慧城市展示建筑物點位 三維城市&#xff1b; 數字孿生場景中&…

純C++11實現!零依賴貝葉斯情感分析系統,掌握機器學習系統工程化的秘密!

本文深度剖析了一個完全基于C++11標準庫實現的貝葉斯情感分析系統。該系統采用模塊化設計,實現了從文本預處理、特征提取到樸素貝葉斯分類的完整機器學習流水線。 1. 系統架構概覽 1.1 技術棧選擇與設計哲學 該系統完全采用C++11標準庫實現,無任何外部依賴,體現了"純…