在进入 App 的过程中,需要加载各种文件,这种文件必然是需要符合某种格式来让系统高效的进行阅读。

在 iOS 系统中,需要先进行加载的是通用二进制 (Universal Binary / Fat Binary) 文件。

通用二进制文件是一种“容器”格式,旨在解决跨架构兼容性问题(如同时支持 IntelApple Silicon)。它的格式如下:

struct fat_header {
    uint32_t	magic;		/* FAT_MAGIC (0xcafebabe) */
    uint32_t	nfat_arch;	/* 包含的架构数量 */
};
struct fat_arch {
    int32_t		cputype;	/* CPU 类型 (如 ARM64, x86_64) */
    int32_t		cpusubtype;	/* CPU 子类型 */
    uint32_t	offset;		/* 该架构 Mach-O 在文件中的偏移量 */
    uint32_t	size;		/* 该架构 Mach-O 的大小 */
    uint32_t	align;		/* 内存对齐 */
};

内核一开始会先读取 读取 fat_header : 发现 0xcafebabe ,确认为 Fat Binary;然后遍历 Arch 列表 : 读取随后的 fat_arch 数组;接着通过 cputypecpusubtype ,与现在正在运行的 CPU 进行比对;找到了对应 CPU ,内核则通过 offsetsize ,讲对应架构的 Mach-O 数据映射到内存中。

这样的好处在于:内核只需要找到对应的索引,就能定位到正确的位置,加载到正确架构的代码,无关代码则完全不会干涉,节省了大量的时间和 RAM

这是典型的通过以磁盘空间换取兼容性和运行时效率的设计。

Mach-O 格式

Mach-O (Mach Object) 是 macOSiOS 上使用的可执行文件格式。它主要由三大部分组成: HeaderLoad CommandsData

  1. Header 包含 Magic NumberCPU 架构、文件类型(如 MH_EXECUTE )、Load Commands 的数量和总大小。Runtime 使用 headerType (即 mach_header_64 ) 来解析它。

  2. Load Commands (加载命令) 告诉内核和 Dyld 如何布局内存。 LC_SEGMENT_64 : 最关键的命令,划分内存区域(Segment)。 LC_LOAD_DYLIB : 声明依赖的动态库(如 Foundation )。 Data (数据区 - Segments & Sections)

  3. Data: 内容仓库(Segments & Sections)。

    • __TEXT Segment : 只读 。存放代码指令 ( __text ) 和常量 ( __cstring )。
    • __DATA Segment : 可读写 。存放全局变量和 objC Runtime 元数据 。
      • Runtime 在初始化时 ( _objc_init -> map_images ),会通过 getSectionData 扫描这里的特定 Section
    • __objc_classlist : 读取所有类。
    • __objc_selrefs : 读取方法选择器。
    • __objc_protolist : 读取协议。 - __LINKEDIT Segment : 只读 。存放符号表、字符串表和代码签名,供 dyld 链接和验签使用。

Header 主要是提供了一些用于总览的元信息,并不十分重要.下面着重分析 Load Commands 和 Data

Load Commands 和 Data

Mach-O 里,最容易混淆但也最关键的一对概念,就是 Load CommandsData。简单说,Load Commands 负责“描述”,Data 负责“承载”。系统真正加载一个 Mach-O 时,并不是先去读全部内容,而是先读取 HeaderLoad Commands,理解这个文件应该怎样被装入内存:哪些 segment 存在、每个 segment 在文件中的偏移是多少、映射到虚拟内存的地址范围是什么、权限该怎么设置、依赖哪些动态库、链接信息放在哪里。dyld 也是通过遍历这些 load command 来决定后续处理路径,这一点在 Header.cppforEachLoadCommand 实现里体现得很直接。

当我们说“每出现一次 LC_SEGMENT / LC_SEGMENT_64 就定义一个 segment”时,本质上是在说:Load Commands 把整个文件拆成了多个可装载单元。常见的 __TEXT__DATA__LINKEDIT__DATA_CONST 等,并不是拍脑袋命名,而是为了把不同访问模式和权限需求的数据分开管理。比如代码通常在 __TEXT ,可写数据在 __DATA* ,链接编辑相关字节集中在 __LINKEDIT 。这些 segment 的描述信息会被抽象成统一结构(如 vmaddr/vmsize/fileOffset/fileSize/initProt/maxProt ),可以参考 Header.h

接下来就到 DataData 不是“定义”,而是“真实字节”。机器码、字符串常量、全局变量、符号字符串表、重定位相关内容等,都属于 Data 这一层。Load Commands 告诉系统“去哪里拿这些字节、用什么方式映射”,Data 才是最终被 CPU 执行、被程序访问的内容本体。也因此,section 的元数据虽然在命令区里(dyld 会从 load command 解析 section,见 Header.cpp ),但 section 真正的内容在 data 区域中。

这里还有一个很重要的细节: fileSizevmSize 不一定相等。 fileSize 是文件里实际存在的字节数, vmSize 是运行时要占用的虚拟内存范围。很多情况下会出现 fileSize < vmSize ,这通常意味着有一段空间在文件中不需要存储具体内容,运行时按零填充扩展。也就是说,Mach-O 既考虑了磁盘存储效率,也考虑了内存映射语义。这种约束和检查在 dyld 里也有明确实现,比如 MachOFile.cppvalidSegments

总结的讲: Load Commands装载协议Data实际货物;前者决定“如何加载”,后者决定“加载什么”。 这两层分离,是 Mach-O 在工程上非常优雅也非常实用的设计。

从链接视角看,Mach-O 由多个 section 组成并被归入不同 segment;从装载视角看,内核/dyldsegment 为映射与权限控制单位。segment 同时有 filesizevmsize (用于表示文件内容与内存映射范围差异,如 zerofill),而 section 主要用 addr/size 描述其在 segment 内的位置与大小。

静态链接 & 动态链接

跳出 Mach-O 的信息解析,从链接的角度重新审视一下.

很多时候我们会把静态链接动态链接分开学,但真正落到 Mach-O 上,它们其实是同一条链路上的两个阶段。编译器先把每个源文件编译成目标文件,目标文件里已经有代码和数据,但符号地址通常还不完整;链接器接下来做的事情,一方面是把多个目标文件中同类型的内容合并到统一布局里,另一方面是根据符号表和重定位信息修正引用关系,让输出文件在结构上变得“可装载”。这一步完成后,我们拿到的是一个可以被系统装载的 Mach-O,但并不意味着运行时工作已经结束。

真正启动进程时,动态链接器才会接手后半段。可执行文件在 Load Commands 里声明了它依赖哪些动态库、由哪个动态链接器来处理这些依赖。dyld 会读取这些命令,按规则加载所需镜像,并在运行期完成剩余的符号绑定和地址修正。也就是说,静态链接解决的是“把文件组织好”,动态链接解决的是“把它跑起来”。这两者分工明确,但又紧密衔接。

从文件结构上看,Mach-O 的设计非常工程化:Load Commands 负责描述,Data 负责承载。前者告诉系统“内容在哪里、如何映射、需要什么权限、依赖关系是什么”;后者保存真正会被执行和访问的字节,比如代码、常量、可写数据以及链接编辑相关内容

理解这一点后,很多字段就不再抽象: fileSize 代表磁盘里实际存在的字节数, vmSize 代表运行时要占用的虚拟内存范围,它们不一定相等,出现差值通常意味着运行期还会有零填充扩展。这也是为什么我更愿意把 Mach-O 看成“描述层 + 内容层”的组合,而不是单纯的二进制打包格式。

再回到 sectionsegment 的关系:section 更像内容组织单元,segment 更像装载与权限管理单元。链接阶段大量操作发生在 section 级别,例如合并、重定位;运行时的映射和保护策略则更多落在 segment 级别。把这个边界理顺之后,很多常见疑惑会自然消失,比如为什么 section 更关注 offset/reloc 这类字段,而 segment 同时强调 file/vm 两套尺寸和权限位。

从实践角度看,这套机制的价值非常现实。静态链接让模块化开发成为可能:不同源码单元独立编译、最终统一拼装。动态链接让系统库共享成为可能:同一份库可被多个进程复用,升级修复也集中在系统侧完成,不需要每个应用都重新打包一份。二者叠加后,既保证了构建阶段的确定性,也保留了运行阶段的灵活性,这就是现代平台在体积、性能、可维护性之间的平衡点。

所以我现在理解 Mach-O,不再把它当成“文件格式细节集合”,而是把它看作一套跨阶段协议:编译器、链接器、动态链接器都围绕这套协议协作

链接器负责把碎片整理成结构化映像,dyld 负责把映像转换为可执行进程状态。Load CommandsData 只是表面上的两块区域,背后对应的是一整条从离线构建到在线运行的系统路径。