一、從源碼到 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.展開宏: 3. 頭文件展開: |
純代碼文件 |
|
2. 編譯 |
編譯器體系:Clang(前端) + LLVM(後端) 1. Clang 解析語法樹(AST)-> LLVM IR(中間表示)
2. LLVM 中間層優化
3. LLVM 後端生成彙編代碼
|
彙編文件 |
|
3. 彙編 |
彙編代碼 -> 機器碼
|
目標文件 |
|
4. 鏈接 |
合併 1. 符號解析: 確定函數/變量地址 2. 重定位: 修正地址引用 3. 合併段信息: 生成完整 Mach-O |
可執行文件(Mach-O)
|
|
5. Bundle 組裝 |
“Mach-O + 資源 + Info.plist” 組裝成 |
|
|
6. 嵌入依賴 Embed |
將 Frameworks/PlugIns 拷貝進 2. PlugIns: App Extensions ( |
|
|
7. 代碼簽名 Codesign |
對 |
|
|
8. 打包 Packaging |
壓縮為 |
|
最終的 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 的可執行文件( |
|
存放在 |
|
動態庫/動態 Framework
|
|
動態加載的共享庫
|
|
插件 Bundle
|
|
運行時可通過 |
|
靜態庫
|
|
.o 文件的歸檔集合(非可執行文件)
|
⚙️ 歸檔靜態庫的工具時
libtool或ar,不是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 |
節: 段的 子區域,例如 |
|
Symbol Table |
符號與地址的映射表
|
|
Load Command |
加載配置(如依賴庫 |
1.1.3 常見 Section
|
Section
|
所屬 Segment
|
存儲內容
|
|
|
|
機器碼指令(編譯後的代碼)
|
|
|
|
動態庫樁代碼(跳轉到 |
|
|
|
C 字符串字面量 ("Hello\0")
|
|
|
|
const 常量
|
|
Section
|
所屬 Segment
|
存儲內容
|
|
---
|
---
|
---
|
|
|
|
Objc 類列表指針
|
|
|
|
@selector() 引用
|
|
|
|
Objc 類元數據( |
|
|
|
已初始化的全局/靜態變量
|
|
|
|
未初始化的全局/靜態變量
|
|
|
|
懶綁定函數指針(printf, malloc, ...)
|
|
Section
|
所屬 Segment
|
存儲內容
|
|
---
|
---
|
---
|
|
(無section,數據連續)
|
|
符號表 + 字符串表 + 綁定信息
|
1.2 代碼簽名體系:簽名、證書與描述文件
蘋果設計代碼簽名體系的三個核心目的:
- 身份驗證:確認應用來自可信的開發者。
- 完整性保護:確保應用沒有被篡改。
- 權限控制:限制應用智能在授權的設備上運行。
簽名體系詳解篇:
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 無法符號化?
- dSYM UUID 與崩潰日誌的 UUID 不匹配;
- BitCode 導致需要重新下載 dSYM;
- 代碼被編譯器內聯優化,符號丟失;
- 地址 ASLR 偏移計算錯誤(需要 load address)
那麼,如何保證符號化的準確性?
- 每次 Archive 保存對應版本的 dSYM;
- 用 UUID 匹配 dSYM 和 crash Log;
- 關閉 BitCode 或者從 App Store Connect 下載;
- 使用 Firebase 等服務自動上傳符號表。
1.4 靜態庫 vs 動態庫
|
特性
|
靜態庫 |
動態庫 |
|
鏈接時機 |
編譯期(鏈接到可執行文件)
|
運行期(由 dyld 動態加載)
|
|
文件格式 |
archive 歸檔(多個 |
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. 解包: 2. 簽名與權限校驗: install 服務校驗 App 的 “簽名哈希/權限聲明(entitlements)/描述文件(profile)” 3. 沙盒創建: 為 App 建立獨立的沙盒目錄結構 4. 通過校驗後,系統允許加載可執行文件 |
|
2. 裝載
|
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 - 運行 Swift static 初始化函數;
性能機制: - dyld shared cache: 系統預加載常用的 Framework, 提高加載速度; - dyld3 closure cache: 緩存符號綁定結果,加速下次啓動。 |
|
3. runtime 初始化 |
OC runtime ( 1. 註冊類、分類、協議;
2. 解析方法列表,建立 selector <-> IMP 的映射表;
3. 構建 4. 執行 5. 啓動消息發送機制 ( Swift runtime( 1. 註冊 Swift 類型元數據;
2. 處理泛型、協議一致性(conformance);
3. 與 Objc runtime 協同,實現 互操作性(混編)。 |
|
4. 進入業務邏輯 |
1. dyld 完成初始化後,程序跳轉到 2. - 初始化 - 創建主線程 & 主RunLoop;
- 加載 Info.plist 指定的 Scene 配置;
- 初始化 - 啓動生命週期: - 加載主 storyboard, 繪製首幀。
3. App 進入運行態 |
3.1 dyld 符號解析(Dynamic Link Editor)
符號是什麼?
- 每個函數、全局變量在編譯後,都有一個唯一符號(Symbol),鏈接器在 Mach-O 文件中維護它的“符號表”。
靜態符號 vs 動態符號
|
類型
|
來源
|
解析時間
|
|
靜態符號
|
|
編譯期已合併到目標文件
|
|
動態符號
|
|
運行時由 dyld 解析和綁定
|
3.1.1 符號解析(符號綁定)機制
- 讀取 Mach-O 的 LC_LOAD_DYLIB 命令;
- 加載對應動態庫;
- 掃描 Import Table;
- 懶綁定(Lazy Binding);
- 解析後緩存在 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)