iOS 知识点 - 编译与构建体系

一、从源码到 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. 展开宏: #define
2. 条件编译: #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 拷贝进 .app
1. 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. 权限控制:限制应用智能在授权的设备上运行。

签名体系详解篇:https://juejin.cn/post/7586518130760908840

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 进入运行态

符号是什么?

  • 每个函数、全局变量在编译后,都有一个唯一符号(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)
posted @ 2025-12-22 20:18  齐生  阅读(1)  评论(0)    收藏  举报