一、從源碼到 App 啓動都經歷了什麼?

主要分為 2 個階段:、

  • 構建階段(Build): 在 mac 上,將源碼與資源編譯為「可安裝產物」
  • 運行階段(Run): 在 iphone 上,將「產物」加載到內存並啓動。

可以簡單理解為:

Build: 源碼 -> 可執行二進制

Run: 二進制 -> 進程 -> App 啓動


二、 構建階段(Build)

將源碼變成「可安裝產物」

輸入:

  • 源碼: .m / .mm / .swift / .h
  • 資源: .xcassets.storyboard.xib、音頻、圖片
  • 配置: Build Settings、Info.plist、entitlements(權限)

輸出(按場景):

  • Build / Run: .app (可執行文件 + 資源)
  • Archive(歸檔): .xcarchive (內含: .app + dSYM + Info.plist)
  • Export(發佈): .ipa (.app 的 zip 包,可分發的安裝包)

主要步驟:

步驟

描述

產物

1. 預處理

1.展開宏: #define2. 條件編譯: #ifdef/#if

3. 頭文件展開: #import/#include

純代碼文件.i

2. 編譯

編譯器體系:Clang(前端) + LLVM(後端)

1. Clang 解析語法樹(AST)-> LLVM IR(中間表示)

2. LLVM 中間層優化

3. LLVM 後端生成彙編代碼

彙編文件.s

3. 彙編

彙編代碼 -> 機器碼

目標文件.o(不可執行,含符號信息)

4. 鏈接

合併 .o 與依賴 (靜態庫.a / 動態庫.dylib / .framework / 系統.tbd 等),做:

1. 符號解析: 確定函數/變量地址

2. 重定位: 修正地址引用

3. 合併段信息: 生成完整 Mach-O

可執行文件(Mach-O)

5. Bundle 組裝

“Mach-O + 資源 + Info.plist” 組裝成 .app

.app

6. 嵌入依賴 Embed

將 Frameworks/PlugIns 拷貝進 .app1. Frameworks: 動態.framework / 動態.dylib

2. PlugIns: App Extensions (.appex)

.app/Frameworks.app/PlugIns

7. 代碼簽名 Codesign

.app 及內部 Mach-O 文件簽名,驗證 App 的來源和完整性

_Codesignature/

8. 打包 Packaging

壓縮為 .ipa

.ipa

最終的 Bundle 結構組成:

MyApp.app/
 ├── _CodeSignature/          # 簽名信息
 ├── MyApp                    # Mach-O 可執行文件
 ├── Info.plist               # 應用元數據
 ├── Frameworks/              # 動態 Frameworks
 ├── PlugIns/                 # App Extensions (.appex)
 ├── Assets.car               # xcassets 編譯結果
 └── Base.lproj/              # 本地化資源

1.1 Mach-O

Mach-O 是 iOS/macOS 的 二進制可執行格式,包含了代碼、數據、符號表、依賴信息等,是構建結果的核心。

1.1.1 Mach-O 文件類型

Mach-O 文件(有不同的類型):

文件類型

枚舉常量

用途

App 主可執行文件

App Extension 的可執行文件(.appex)

MH_EXECUTE

存放在 App.app/AppNameApp.app/PlugIns

動態庫/動態 Framework

MH_DYLIB

動態加載的共享庫

插件 Bundle

MH_BUNDLE

運行時可通過 NSBundle 加載的模塊 (少見)

靜態庫

.a

.o 文件的歸檔集合(非可執行文件)

⚙️ 歸檔靜態庫的工具時 libtoolar,不是 ld(鏈接器)。

1.1.2 Mach-O 文件結構

文件偏移                    Mach-O File Structure
═══════════════════════════════════════════════════════════════

0x0000          ┌───────────────────────────────────────────┐
                │ Mach Header (32 bytes)                    │
                │ ──────────────────────────────────────────│
                │  magic: 0xFEEDFACF                        │
                │  cputype: ARM64         # 架構             │
                │  filetype: MH_EXECUTE   # 文件類型          │
                │  ncmds: 28              # 加載的命令數量     │
                │  sizeofcmds: 3456                         │
                │  flags: PIE | DYLDLINK                    │
                └───────────────────────────────────────────┘
                
0x0020          ┌───────────────────────────────────────────┐
                │ Load Commands (3456 bytes)                │
                ├───────────────────────────────────────────┤
                │ LC_SEGMENT_64 (__PAGEZERO)     # 空頁保護   │
                │   vmaddr:  0x0                            │
                │   vmsize:  0x100000000 (4GB)              │
                │   fileoff: 0                              │
                │   filesize: 0                             │ ← 不佔文件空間
                ├───────────────────────────────────────────┤
                │ LC_SEGMENT_64 (__TEXT)         # 代碼段    │
                │   vmaddr:  0x100000000                    │
                │   vmsize:  0x4000 (16KB)                  │
                │   fileoff: 0x0000                         │
                │   filesize: 0x4000                        │
                │   maxprot: r-x                            │
                │   nsects: 6                               │
                │   Sections:             ← Section 描述符   │
                │     __text      (機器碼)                   │
                │     __stubs     (樁代碼)                   │
                │     __cstring   (字符串)                   │
                ├───────────────────────────────────────────┤
                │ LC_SEGMENT_64 (__DATA)         # 數據段    │
                │   vmaddr:  0x100004000                    │
                │   vmsize:  0x4000                         │
                │   fileoff: 0x4000                         │
                │   filesize: 0x4000                        │
                │   maxprot: rw-                            │
                │   nsects: 8                               │
                │   Sections:             ← Section 描述符   │
                │     __data            (全局變量)          │
                │     __objc_classlist   (類列表)            │  
                │     __la_symbol_ptr    (懶綁定指針表)       │
                ├───────────────────────────────────────────┤
                │ LC_SEGMENT_64 (__LINKEDIT)     # 鏈接信息   │
                │   vmaddr:  0x100008000                    │
                │   vmsize:  0x4000                         │
                │   fileoff: 0x8000                         │
                │   filesize: 0x4000                        │
                │   maxprot: r--                            │
                │   nsects: 0             ← 沒有 Sections!  │
                │                           數據是連續數據塊   │
                ├───────────────────────────────────────────┤
                │ LC_LOAD_DYLIB                  # 動態庫    │
                │   /usr/lib/libobjc.A.dylib                │
                │   UIKit.framework                         │
                ├───────────────────────────────────────────┤
                │ LC_MAIN                    # Entry Point  │
                │   entryoff: 0x3A20                        │
                ├───────────────────────────────────────────┤
                │ LC_CODE_SIGNATURE              # 簽名位置   │
                │   dataoff: 0xC000                         │
                │   datasize: 0x1000                        │
                └───────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════════════════
                                      説明:
                數據位置:Load Commands 通過 fileoff 指向實際的 Data 位置
                加載位置:vmaddr 指定加載到內存的位置                   
═══════════════════════════════════════════════════════════════════════════════════

0x0D80          ┌───────────────────────────────────────────┐
(約 3456字節後)  │ ↓↓↓ 以下是實際的 Data 區域 ↓↓↓               │
                └───────────────────────────────────────────┘

0x0000          ┌───────────────────────────────────────────┐
(相對偏移)       │ __TEXT Segment Data (16KB)                │
                ├───────────────────────────────────────────┤
                │ __text Section                            │
                │   0x100000000: 55 48 89 E5 ...  (機器碼)   │
                │   0x100003A20: <_main> ...                │
                ├───────────────────────────────────────────┤
                │ __stubs Section                           │
                │   0x100003800: FF 25 ...          (樁代碼) │
                ├───────────────────────────────────────────┤
                │ __cstring Section                         │
                │   0x100003900: "Hello, World\0"  (字符串)│
                └───────────────────────────────────────────┘

0x4000          ┌───────────────────────────────────────────┐
                │ __DATA Segment Data (16KB)                │
                ├───────────────────────────────────────────┤
                │ __data Section               (全局變量)   │
                │   0x100004000: 2A 00 00 00 (globalVar=42) │
                ├───────────────────────────────────────────┤
                │ __objc_classlist Section      (類列表)     │
                │   0x100005000: [MyClass*, UIView*, ...]   │
                ├───────────────────────────────────────────┤
                │ __la_symbol_ptr Section       (懶綁定指針表)│
                │   0x100007000: [printf地址, malloc地址, ...]│
                └───────────────────────────────────────────┘

0x8000          ┌───────────────────────────────────────────┐
                │ __LINKEDIT Segment Data (16KB)            │
                │ ▲ 注意:沒有 Section 結構,是連續數據塊        │
                ├───────────────────────────────────────────┤
                │ Dyld Info (Rebase/Bind 信息)              │
                ├───────────────────────────────────────────┤
                │ Symbol Table     (符號表)                  │
                │   struct nlist_64 {                       │
                │     n_strx: 10,  n_value: 0x100003A20     │
                │     ...                                   │
                │   }                                       │
                ├───────────────────────────────────────────┤
                │ String Table      (字符串表)               │
                │   "\0_main\0_printf\0_objc_msgSend\0"     │
                └───────────────────────────────────────────┘

0xC000          ┌───────────────────────────────────────────┐
                │ Code Signature Data (4KB)                 │
                ├───────────────────────────────────────────┤
                │ Code Directory (每頁的 Hash)               │
                │   Page 0: 3A2F...                         │
                │   Page 1: 8B1C...                         │
                ├───────────────────────────────────────────┤
                │ Entitlements                              │
                │ CMS Signature (證書鏈)                     │
                └───────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════════════════

關鍵概念

名稱

説明

Segment

段: 按用途劃分的內存區,存放不同的數據(代碼/變量/常量)

Section

節: 段的 子區域,例如 __text/__cstring

Symbol Table

符號與地址的映射表

Load Command

加載配置(如依賴庫 LC_LOAD_DYLIBLC_MAIN

1.1.3 常見 Section

Section

所屬 Segment

存儲內容

__text

__TEXT(只讀)

機器碼指令(編譯後的代碼)

__stubs

__TEXT

動態庫樁代碼(跳轉到 __la_symbol_ptr)

__cstring

__TEXT

C 字符串字面量 ("Hello\0")

__const

__TEXT

const 常量

Section

所屬 Segment

存儲內容

---

---

---

__objc_classlist

__DATA(可寫)

Objc 類列表指針

__objc_selrefs

__DATA

@selector() 引用

__objc_data

__DATA

Objc 類元數據(class_t + 它引用的所有子結構體)

__data

__DATA

已初始化的全局/靜態變量

__bbs

__DATA

未初始化的全局/靜態變量

__la_symbol_ptr

__DATA

懶綁定函數指針(printf, malloc, ...)

Section

所屬 Segment

存儲內容

---

---

---

(無section,數據連續)

__LINKEDIT(只讀)

符號表 + 字符串表 + 綁定信息

1.2 代碼簽名體系:簽名、證書與描述文件

蘋果設計代碼簽名體系的三個核心目的:

  1. 身份驗證:確認應用來自可信的開發者。
  2. 完整性保護:確保應用沒有被篡改。
  3. 權限控制:限制應用智能在授權的設備上運行。

簽名體系詳解篇:

1.3 dSYM 與符號化

  • 背景:
  • App 編譯成 Mach-O 二進制後,出於性能和安全考慮,符號(函數名、變量名) 會被剝離和混淆。
  • 因此,需要一個文件來記錄符號與實際地址的關係,這個文件就是 .app.dSYM
  • 概念: dSYM (Debug Symbols) 是 App 的符號表存檔文件,它存儲了【源文件路徑、symbol table、行號信息】。
  • 實際上是一個 bundle:
# dSYM 文件結構
YourApp.app.dSYM/
    └── Contents/
        └── Resources/
            └── DWARF/
                └── YourApp # 實際符號表文件
  • 生成方式:
  • Debug 構建: 默認包含符號;
  • Release 構建: 若開啓「Debug Information Format = DWARF with dSYM」,會生成單獨 .dSYM 文件。

1.3.1 符號化

  • 編譯階段由 linker 生成符號表;
  • .dSYM 相當於符號信息的離線副本;
  • UUID 匹配機制 保證符號與 crash 日誌對應。

查看 UUID:

# 查看 dSYM 的 UUID
dwarfdump --uuid YourApp.app.dSYM

# 查看 App 的 UUID
dwarfdump --uuid YourApp.app/YourApp

# 必須匹配才能符號化
UUID: 12345678-1234-1234-1234-123456789ABC (arm64)

Crash 日誌原始格式:

Thread 0 Crashed:
0   YourApp    0x0000000100004a2c 0x100000000 + 18988
1   YourApp    0x0000000100005b3d 0x100000000 + 23357

符號化後:

Thread 0 Crashed:
0   YourApp    0x100004a2c -[MyViewController viewDidLoad] + 124 (MyViewController.swift:42)
1   YourApp    0x100005b3d -[AppDelegate application:didFinishLaunchingWithOptions:] + 89

手動符號化:

# 方法1:使用 atos
atos -arch arm64 -o YourApp.app.dSYM/Contents/Resources/DWARF/YourApp -l 0x100000000 0x100004a2c

# 方法2:使用 symbolicatecrash
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
./symbolicatecrash crash.log YourApp.app.dSYM > symbolicated.log

BitCode 與符號化:

開啓 BitCode 後:

  • App Store 重新編譯,生成新的二進制;
  • 原 dSYM 的 UUID 與線上包不匹配;
  • 必須從 App Store Connect 下載對應的 dSYM。
# Xcode → Organizer → Download dSYMs
# 或命令行
xcrun altool --download-dsyms \
             --username "your@email.com" \
             --password "@keychain:AC_PASSWORD" \
             --app-identifier "com.yourapp"

提問: 為什麼線上 crash 無法符號化?

  1. dSYM UUID 與崩潰日誌的 UUID 不匹配;
  2. BitCode 導致需要重新下載 dSYM;
  3. 代碼被編譯器內聯優化,符號丟失;
  4. 地址 ASLR 偏移計算錯誤(需要 load address)

那麼,如何保證符號化的準確性?

  1. 每次 Archive 保存對應版本的 dSYM;
  2. 用 UUID 匹配 dSYM 和 crash Log;
  3. 關閉 BitCode 或者從 App Store Connect 下載;
  4. 使用 Firebase 等服務自動上傳符號表。

1.4 靜態庫 vs 動態庫

特性

靜態庫.a

動態庫.dylib/.framework

鏈接時機

編譯期(鏈接到可執行文件)

運行期(由 dyld 動態加載)

文件格式

archive 歸檔(多個.o文件打包)

Mach-O 可執行文件

文件大小

會增大 App 主可執行文件體積

單獨的文件

內存佔用

每個進程獨立拷貝

多進程共享文件

更新方式

重新編譯 App

替換 dylib 即可

啓動速度


啓動時需要 dyld 加載,略慢

使用限制

iOS App 可隨意使用

App Store 對自定義動態庫有限制

1.4.1 靜態庫

  • 靜態庫在 編譯期被鏈接到可執行文件中,本質是多個 .o 文件的集合(使用 ar 打包)。

常見問題:

  • duplicate symbol 錯誤:如果兩個 .o 文件裏定義了相同的符號(Symbol),鏈接器無法判斷該保留哪一個,就會報錯 duplicate symbol
  • 也就是:多個文件實現了同名全局符號
  • 命令:

命令

方式

-ObjC

加載所有 Objc 類和 Category

-all_load

強制加載所有靜態庫的所有符號

`-force_load libALib.a

只強制加載特定靜態庫

  • Q:為什麼 Category 需要 -ObjC
  • A:靜態庫是多個 .o 文件的集合,鏈接器的默認策略是:

只有當某個目標文件中,存在「被引用符號」時,才會將該 .o 鏈接到最終的可執行文件。

  • 也就是説:
  • 類的實現會被用到 -> 其符號 _OBJC_CLASS_$_MyClass 被引用 → 對應 .o 被加載;
  • 但是,Category 不會生成新的類符號,它只是對已有類添加方法;
  • 因此,沒有任何符號能 “引用” 到 Category 所在 .o 文件;
  • 鏈接器認為它 “沒用”,就不會把它打包進最終 Mach-O。
  • Q:為什麼動態庫無此問題?
  • A:動態庫的符號在 運行時由 dyld 加載,所有符號表已打包到 Mach-O 中,不受鏈接器「按需加載」的策略影響。

1.4.2 動態庫

  • 動態庫在 運行時由 dyld 加載,本質是 Mach-O 文件,有自己的:
  • Segment / Section
  • 符號表
  • 導出表
  • 依賴信息(LC_LOAD_DYLIB 等 Load Command)
  • 動態庫在 運行時被 dyld 動態加載,不直接打入可執行文件。
  • iOS 常見形式:
  • .dylib: 純動態庫
  • .framework: 動態 Framework, 包括 .dylib + heades + Resources

三、 運行階段(Run)

將產物「運行起來」

輸入:

  • 安裝包: .ipa

主要步驟:

步驟

描述

1. 安裝與校驗

1. 解包: .ipa 解壓

2. 簽名與權限校驗: install 服務校驗 App 的 “簽名哈希/權限聲明(entitlements)/描述文件(profile)”

3. 沙盒創建: 為 App 建立獨立的沙盒目錄結構

4. 通過校驗後,系統允許加載可執行文件

2. 裝載
【dyld(Dynamic Link Editor)】

1. mmap(內存映射): 將 Mach-O 內容映射到虛擬內存

2. 依賴解析:dyld 遞歸加載當前 Mach-O 的依賴(LC_LOAD_DYLIB 指令),包括系統 Framework 和 App 自帶動態庫等

3. rebase(地址重定基): 根據 ASLR(地址空間隨機化)修正指針偏移

4. bind(符號綁定): 為符號引用(函數/變量等)綁定實際地址

- Lazy binding: 第一次調用時解析;

- Eager binding: 啓動時立即解析所有符號;

5. 初始化:

- 調用 C++ static 構造函數;

- 調用 OC +load 方法;(由 dyld 調用)

- 運行 Swift static 初始化函數;

性能機制:

- dyld shared cache: 系統預加載常用的 Framework, 提高加載速度;

- dyld3 closure cache: 緩存符號綁定結果,加速下次啓動。

3. runtime 初始化

OC runtime (libobjc.A.dylib)

1. 註冊類、分類、協議;

2. 解析方法列表,建立 selector <-> IMP 的映射表;

3. 構建 Class / Meta-Class 層級結構;

4. 執行 +load 方法;(由 runtime 執行)

5. 啓動消息發送機制 (objc_msgSend)。

Swift runtime(libswiftCore.dylib

1. 註冊 Swift 類型元數據;

2. 處理泛型、協議一致性(conformance);

3. 與 Objc runtime 協同,實現 互操作性(混編)

4. 進入業務邏輯

1. dyld 完成初始化後,程序跳轉到 main()

2. UIApplicationMain 工作:

- 初始化 UIKit 環境;

- 創建主線程 & 主RunLoop;

- 加載 Info.plist 指定的 Scene 配置;

- 初始化 AppDelegate / SceneDelegate

- 啓動生命週期: didFinishLaunching: -> sceneWillEnterForeground:

- 加載主 storyboard, 繪製首幀。

3. App 進入運行態

3.1 dyld 符號解析(Dynamic Link Editor)

符號是什麼?

  • 每個函數、全局變量在編譯後,都有一個唯一符號(Symbol),鏈接器在 Mach-O 文件中維護它的“符號表”。

靜態符號 vs 動態符號

類型

來源

解析時間

靜態符號

.a 靜態庫

編譯期已合併到目標文件

動態符號

.dylib/.framework 動態庫

運行時由 dyld 解析和綁定

3.1.1 符號解析(符號綁定)機制

  1. 讀取 Mach-O 的 LC_LOAD_DYLIB 命令;
  2. 加載對應動態庫;
  3. 掃描 Import Table;
  4. 懶綁定(Lazy Binding);
  5. 解析後緩存在 GOT(全局偏移表)或指針表中。

3.1.2 符號衝突

同名符號在動態鏈接場景中,以「加載順序」為準。

因此 Apple 框架採用命名空間(如 _OBJC_CLASS_$_UIView)避免衝突。


3.2 ASLR 與 PIE

PIE(位置無關可執行文件, Position Independent Executable)  是 iOS / macOS 可執行文件的一種編譯方式,使程序可以在 任意內存地址加載運行,從而增強安全性。

  • 它是 ASLR(地址空間隨機化)的基礎支撐機制。

PIE: 是一種 可以在任意地址被加載的可執行程序,編譯器會生成相對地址訪問代碼,而不是固定絕對地址。

與之對應的是:

  • Non-PIE(非位置無關可執行文件) :程序加載地址是固定的。

3.2.1 為什麼需要 PIE

PIE 的設計目的是配合 ASLR(Address Space Layout Randomization)

概念

作用

ASLR

每次運行時隨機化可執行文件、棧、堆、庫的加載地址

PIE

確保可執行文件本身能在任意地址運行(位置無關)

沒有 PIE 的程序,即使系統支持 ASLR,也無法隨機化主程序基地址。


3.2.2 PIE 的原理

普通可執行文件(Non-PIE)

  • 代碼中大量使用絕對地址。
  • 鏈接器在編譯期寫死這些地址。
  • 程序啓動時只能加載到固定位置(如 0x100000000)。
mov rax, [0x10002000]   // 絕對地址

PIE 可執行文件

  • 編譯器使用 相對地址訪問(PC-relative addressing)。
  • 程序運行時基址可隨機化,指令使用偏移量計算目標地址。
  • 運行時只需做一次 rebase(地址重定基)。
adrp x0, _GLOBAL_OFFSET_TABLE_
add  x1, x0, #offset     // 相對尋址

這也是為什麼 dyld 在啓動階段需要執行 rebase 步驟


四、還記得 Xcode 兩個常用命令不?

4.1 Build(⌘B)

  • 執行 “構建” 流程,生成 .app 等產物。
  • Build = compile + link + bundle + embed + sign (+ package)

4.2 Run(⌘R)

  • 先增量 Build,然後 install + launch + attach debugger(調試)。
  • Run = Build + install + launch (+ attach debugger)

五、App 發佈與分發

5.1 TestFlight

  • 官方測試渠道
  • 內測 / 外測兩種模式

5.2 App Store 發佈

  • Archive → Validate → Upload → 審核 → 發佈

5.3 安裝驗證

系統會在安裝時驗證:

  • 簽名鏈;
  • Profile 匹配;
  • Entitlements 權限合法性。

六、總結

從 .m / .swift 到首幀渲染,iOS 應用經歷了:

源碼編譯 → 鏈接 → Mach-O 生成 → 簽名打包 → 系統加載 → runtime 註冊 → main 啓動。

這條鏈條貫穿:

  • 編譯器(Clang / LLVM)
  • 鏈接器(ld)
  • 動態加載器(dyld)
  • 語言運行時(Objective-C / Swift runtime)
  • 框架加載系統(UIKit)