Mach-O 底层探索

解剖 iOS 二进制可执行文件结构。理解 Segment/Section 映射原理以及 dyld 动态加载流程。

探索 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()      │
└─────────────────┘

详细步骤

  1. Load dylibs: 递归加载所有依赖的动态库
  2. Rebase: 修正内部指针(因为 ASLR)
  3. Bind: 绑定外部符号
  4. ObjC Setup: 注册 OC 类、Category
  5. 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%)

优化建议

  1. 减少动态库数量

    • 合并小型动态库
    • 使用静态库替代
  2. 减少 OC 类数量

    • 移除未使用的类
    • 延迟加载不必要的类
  3. 优化 Initializers

    • 避免 +load 方法
    • 使用 +initialize 替代
    • 减少 __attribute__((constructor))
  4. 减少符号数量

    • 使用 -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