探索 iOS 可执行文件的内部构造。理解 Mach-O 是进行启动优化、逆向工程和深入理解 iOS 系统的基础。
1. Mach-O 概述
Mach-O (Mach Object) 是 macOS/iOS 系统的可执行文件格式,类似于 Linux 的 ELF 和 Windows 的 PE。
文件类型
| 类型 | 说明 | 示例 |
|---|---|---|
MH_EXECUTE |
可执行文件 | App 主程序 |
MH_DYLIB |
动态库 | .dylib, .framework |
MH_BUNDLE |
可加载 Bundle | .bundle 插件 |
MH_OBJECT |
目标文件 | .o 编译中间产物 |
MH_DSYM |
调试符号文件 | .dSYM |
查看 Mach-O 信息
# 查看文件类型
file MyApp
# 查看 Mach-O 头信息
otool -h MyApp
# 查看 Load Commands
otool -l MyApp
# 查看依赖的动态库
otool -L MyApp
# 查看符号表
nm MyApp
2. 文件结构
Mach-O 文件由三部分组成:
┌─────────────────────────────────┐
│ Header │ ← 文件头信息
├─────────────────────────────────┤
│ Load Commands │ ← 加载指令
├─────────────────────────────────┤
│ Data │ ← 实际数据
│ ┌───────────────────────────┐ │
│ │ __TEXT Segment │ │ ← 代码段
│ │ ┌─────────────────────┐ │ │
│ │ │ __text section │ │ │ ← 机器码
│ │ │ __stubs section │ │ │ ← 桩代码
│ │ │ __cstring section │ │ │ ← C字符串
│ │ └─────────────────────┘ │ │
│ ├───────────────────────────┤ │
│ │ __DATA Segment │ │ ← 数据段
│ │ ┌─────────────────────┐ │ │
│ │ │ __data section │ │ │ ← 初始化数据
│ │ │ __bss section │ │ │ ← 未初始化数据
│ │ │ __objc_classlist │ │ │ ← OC 类列表
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
3. Header 详解
struct mach_header_64 {
uint32_t magic; // 魔数:0xFEEDFACF (64位)
cpu_type_t cputype; // CPU 类型:ARM64
cpu_subtype_t cpusubtype; // CPU 子类型
uint32_t filetype; // 文件类型:MH_EXECUTE 等
uint32_t ncmds; // Load Commands 数量
uint32_t sizeofcmds; // Load Commands 总大小
uint32_t flags; // 标志位
uint32_t reserved; // 保留字段
};
常见标志位
| Flag | 说明 |
|---|---|
MH_PIE |
地址空间随机化 (ASLR) |
MH_TWOLEVEL |
两级命名空间 |
MH_DYLDLINK |
需要 dyld 加载 |
4. Load Commands
Load Commands 告诉系统如何加载这个文件。
常见 Load Commands
# 使用 otool 查看
otool -l MyApp
| Command | 说明 |
|---|---|
LC_SEGMENT_64 |
定义内存段 |
LC_DYLD_INFO_ONLY |
动态链接信息 |
LC_SYMTAB |
符号表 |
LC_DYSYMTAB |
动态符号表 |
LC_LOAD_DYLIB |
加载动态库 |
LC_MAIN |
程序入口点 |
LC_CODE_SIGNATURE |
代码签名 |
LC_ENCRYPTION_INFO |
加密信息 |
Segment 示例
struct segment_command_64 {
uint32_t cmd; // LC_SEGMENT_64
uint32_t cmdsize; // command 大小
char segname[16]; // 段名:__TEXT, __DATA
uint64_t vmaddr; // 虚拟内存地址
uint64_t vmsize; // 虚拟内存大小
uint64_t fileoff; // 文件偏移
uint64_t filesize; // 文件中的大小
vm_prot_t maxprot; // 最大内存保护
vm_prot_t initprot; // 初始内存保护
uint32_t nsects; // section 数量
uint32_t flags; // 标志
};
5. 主要 Segment
__TEXT (代码段)
只读、可执行,包含程序代码。
| Section | 内容 |
|---|---|
__text |
编译后的机器码 |
__stubs |
动态库调用的桩代码 |
__stub_helper |
桩代码辅助函数 |
__cstring |
C 语言字符串常量 |
__objc_methname |
OC 方法名 |
__objc_classname |
OC 类名 |
__DATA (数据段)
可读写,包含全局变量。
| Section | 内容 |
|---|---|
__data |
已初始化的全局变量 |
__bss |
未初始化的全局变量 |
__common |
公共符号 |
__objc_classlist |
OC 类列表 |
__objc_protolist |
OC 协议列表 |
__objc_selrefs |
OC 选择器引用 |
__LINKEDIT
链接信息,包含符号表、字符串表、代码签名等。
6. dyld 加载流程
dyld (Dynamic Loader) 是 Apple 的动态链接器,负责加载 Mach-O。
冷启动流程
exec() 系统调用
│
▼
┌─────────────────┐
│ 内核加载 Mach-O │
│ 解析 Header │
│ 映射 Segments │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 加载 dyld │
│ (动态链接器) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ dyld 初始化 │
│ 1. 加载动态库 │
│ 2. Rebase │
│ 3. Bind │
│ 4. ObjC setup │
│ 5. Initializers│
└────────┬────────┘
│
▼
┌─────────────────┐
│ main() │
└─────────────────┘
详细步骤
- Load dylibs: 递归加载所有依赖的动态库
- Rebase: 修正内部指针(因为 ASLR)
- Bind: 绑定外部符号
- ObjC Setup: 注册 OC 类、Category
- Initializers: 执行
+load和__attribute__((constructor))
7. 符号表
查看符号
# 查看所有符号
nm -m MyApp
# 查看未定义符号(需要动态链接)
nm -u MyApp
# 查看外部符号
nm -g MyApp
符号类型
| 类型 | 说明 |
|---|---|
T |
代码段符号 |
D |
数据段符号 |
U |
未定义(需要链接) |
S |
其他段符号 |
符号混淆 (Name Mangling)
// Swift 函数
func greet(name: String) -> String { }
// 混淆后的符号名
_$s7MyClass5greet4nameS2S_tF
8. 启动优化
测量启动时间
# 环境变量启用 dyld 日志
DYLD_PRINT_STATISTICS=1 ./MyApp
输出示例:
Total pre-main time: 1.2 seconds (100.0%)
dylib loading time: 350ms (29.1%)
rebase/binding time: 120ms (10.0%)
ObjC setup time: 280ms (23.3%)
initializer time: 450ms (37.5%)
优化建议
-
减少动态库数量
- 合并小型动态库
- 使用静态库替代
-
减少 OC 类数量
- 移除未使用的类
- 延迟加载不必要的类
-
优化 Initializers
- 避免
+load方法 - 使用
+initialize替代 - 减少
__attribute__((constructor))
- 避免
-
减少符号数量
- 使用
-Wl,-dead_strip移除无用代码 - 减少导出符号
- 使用
代码示例
// ❌ 避免使用 +load
class MyClass: NSObject {
override class func load() {
// 在 main() 之前执行,影响启动
}
}
// ✅ 使用 +initialize 替代
class MyClass: NSObject {
override class func initialize() {
// 首次使用类时执行
}
}
// ✅ 或使用 dispatch_once
private static let setupOnce: Void = {
// 初始化代码
}()
9. 常用工具
| 工具 | 用途 |
|---|---|
otool |
查看 Mach-O 信息 |
nm |
查看符号表 |
lipo |
管理 Fat Binary |
install_name_tool |
修改动态库路径 |
codesign |
代码签名 |
dwarfdump |
查看 DWARF 调试信息 |
MachOView |
GUI 查看工具 |
Hopper |
反汇编工具 |
常用命令
# 查看架构
lipo -info MyApp
# 提取单一架构
lipo -thin arm64 -output MyApp_arm64 MyApp
# 合并架构
lipo -create MyApp_arm64 MyApp_x86 -output MyApp_Universal
# 查看代码签名
codesign -dv --verbose=4 MyApp.app
10. Universal Binary (Fat Binary)
包含多个架构的二进制文件。
┌─────────────────────────────────┐
│ Fat Header │
├─────────────────────────────────┤
│ arm64 Mach-O │
├─────────────────────────────────┤
│ x86_64 Mach-O │
└─────────────────────────────────┘
# 查看 Fat Binary 信息
lipo -detailed_info MyApp.framework/MyApp
# 瘦身(移除不需要的架构)
lipo -remove x86_64 -output MyApp MyApp