摘要
Mach-O(Mach Object)是 Apple 生态系统使用的二进位档案格式,主要用于表示目标档案、可执行档、动态程式库及其他类型的二进位映像。Mach-O 提供一套结构化机制,用于描述处理器架构、档案类型、机器指令与资料区域、符号资讯、重定位、动态连结,以及程式载入与执行所需的相关资讯。
与可执行与可连结格式(Executable and Linkable Format,ELF)不同,Mach-O 透过载入命令(Load Commands)组织大量与载入及连结相关的控制资讯。这些命令可以描述档案中的哪些区域需要映射至记忆体、程式依赖哪些动态程式库,以及符号表和其他连结资料的位置。
Mach-O 的历史与 Mach 系统、NeXT 作业系统以及 Mac OS X 的形成密切相关。随着 Apple 平台的发展,Mach-O 逐渐支援多种处理器架构,包括 PowerPC、x86-64 与 ARM64。
本文研究 Mach-O 的历史来源、二进位档案结构、标头、载入命令、区段与节区、符号表、重定位机制、动态连结、通用二进位档案、程式载入流程,以及 Apple 平台的程式码签署机制。
本文主要依据 Apple 公开的 XNU 原始码、Mach-O 结构定义及动态连结器 dyld 的相关文件进行分析。文中涉及位址、配置及工具输出的范例,若未注明实际来源,均属技术示意,不代表特定 Mach-O 档案的分析结果。
关键词: Mach-O、Mach Object、二进位格式、可执行档、Apple、macOS、XNU、dyld、载入命令、区段、节区、重定位、通用二进位档案、ABI。
一、什么是 Mach-O?
1.1. 定义
Mach-O 是一种目标档案及二进位执行映像格式,用于储存连结器、载入器及作业系统在建置或执行程式时所需要的资讯。
依照档案用途,Mach-O 可以包含:
- 已编译的机器指令。
- 静态资料与唯读资料。
- 需要初始化的记忆体区域。
- 符号表。
- 重定位资讯。
- 动态程式库相依资讯。
- 程式进入点。
- 程式码签署资料。
- 平台及执行环境相关中继资料。
Mach-O 是二进位档案格式,而不是程式语言、处理器指令集或独立的机器码种类。
Mach-O 所储存的机器码仍取决于目标处理器架构,例如 x86-64 或 ARM64。
1.2. Mach-O 的使用范围
Mach-O 广泛用于 Apple 平台的各类二进位产物。
| 类型 | 主要用途 |
|---|---|
目标档案 .o |
编译与连结过程中的中间产物 |
| 可执行档 | 由作业系统载入并执行的程式 |
动态程式库 .dylib |
提供其他程式使用的机器码与资料 |
| 二进位套件(Bundle) | 可依平台机制动态载入的程式组件 |
| dSYM 档案 | 可包含用于除错资讯的 Mach-O 映像 |
| 系统二进位映像 | 部分作业系统及执行环境组件 |
Mach-O 使用于 macOS、iOS、iPadOS、watchOS、tvOS 及其他相关 Apple 平台。
但不同平台对二进位档案的执行权限、程式码签署及载入方式,可能存在不同的技术与安全要求。
1.3. Mach-O 与 .app 应用程式套件
macOS 应用程式通常以副档名为 .app 的套件形式分发。
例如:
Example.app/
└── Contents/
├── Info.plist
├── MacOS/
│ └── Example
└── Resources/
其中:
Example.app是应用程式套件。Info.plist包含应用程式的描述资讯。Contents/MacOS/Example可以是一个 Mach-O 可执行档。
因此,**Mach-O 描述的是二进位档案本身,而非整个 .app 目录的格式。**
二、历史来源与发展
2.1. 与 Mach 系统的关系
Mach-O 的名称源自 Mach Object。
Mach 是一系列在二十世纪八○年代于卡内基美隆大学(Carnegie Mellon University)研究的作业系统核心设计。
Mach 涉及工作(Task)、执行绪(Thread)、虚拟记忆体及程序间通讯等系统机制。
Mach-O 则与使用 Mach 技术的作业系统环境有关,特别是 NeXT 系统,以及后来 Apple 所采用的 Darwin 和 Mac OS X 技术体系。
不过,Mach-O 不是 Mach 核心本身,也不是 CPU 指令集的一部分。
作业系统核心与可执行档格式属于不同的技术层次,两者透过程式载入和记忆体管理机制相互配合。
2.2. NeXTSTEP 与 Mac OS X
NeXTSTEP 是 NeXT 公司开发的作业系统,其基础包含 Mach 相关技术及 Unix 系统的多项特征。
Mach-O 与 NeXT 的软体环境具有历史关联,并随相关技术演进而成为 Apple 平台的重要二进位格式。
1997 年 Apple 收购 NeXT 后,NeXT 的多项技术成为新一代 Macintosh 作业系统的基础。
2001 年,Mac OS X 10.0 正式发行。
Mach-O 随之成为 Mac OS X 以及后续 macOS 发展中的重要二进位格式。
2.3. 处理器架构的演变
Mach-O 经历 Apple 多次重要的处理器架构转换。
| 历史阶段 | 代表架构 | 技术意义 |
|---|---|---|
| NeXT 系统时期 | Motorola 68k、x86 等 | Mach 与 NeXT 软体环境的早期发展 |
| Mac OS X 早期 | PowerPC | Mach-O 于新一代 Macintosh 系统中广泛使用 |
| Intel Mac 时期 | x86、x86-64 | 支援 Intel 处理器 |
| Apple silicon 时期 | ARM64 | 支援 Apple 自行设计的 ARM64 处理器 |
多架构支援需求,促进了通用二进位档案(Universal Binary)的使用。
2.4. 32 位元与 64 位元 Mach-O
Mach-O 定义了 32 位元与 64 位元两种主要的 Mach 标头格式。
Apple 在 mach-o/loader.h 中定义:
struct mach_header;
struct mach_header_64;
两者除了支援不同的档案结构需求外,也具有不同的标头大小。
对于 64 位元 Mach-O,其区段与节区结构使用 64 位元栏位描述虚拟位址及部分大小资讯。
三、Mach-O 的整体档案结构
3.1. 结构概观
典型 Mach-O 的逻辑构成可以表示如下:
┌─────────────────────────────────┐
│ Mach Header │
│ - Magic │
│ - CPU Type │
│ - File Type │
│ - Number of Load Commands │
├─────────────────────────────────┤
│ Load Commands │
│ - Segment Commands │
│ - Symbol Table Information │
│ - Dynamic Linking Information │
│ - Entry Point Information │
├─────────────────────────────────┤
│ Segment Contents │
│ - Machine Code │
│ - Read-only Data │
│ - Writable Data │
├─────────────────────────────────┤
│ Link-edit Data │
│ - Symbols │
│ - Fixups / Bindings │
│ - Other Metadata │
├─────────────────────────────────┤
│ Code Signature, if present │
└─────────────────────────────────┘
此图仅用于说明概念上的资料分类。
实际内容未必依照图示顺序连续排列。各个资料区域的位置,应依照相关结构所记录的偏移量及大小判定。
3.2. 三项主要结构
Mach-O 可以从三个核心部分开始理解。
Mach 标头(Mach Header)
用于描述档案格式、处理器架构、档案类型及载入命令的基本资讯。
载入命令(Load Commands)
描述载入器、连结器及分析工具需要理解的二进位结构及相关中继资料。
区段与节区(Segments and Sections)
负责组织机器码、资料及其他需要在连结或载入阶段处理的内容。
符号表、重定位与动态连结资料则透过相关结构进行定位和解读。
四、Mach 标头(Mach Header)
4.1. 64 位元标头结构
Apple 于 mach-o/loader.h 中定义:
struct mach_header_64 {
uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t sizeofcmds;
uint32_t flags;
uint32_t reserved;
};
其中 cpu_type_t 与 cpu_subtype_t 在相关定义中使用 32 位元整数表示。
mach_header_64 的大小为 32 位元组。
4.2. 栏位配置
| 偏移量 | 栏位 | 大小 | 说明 |
|---|---|---|---|
0x00 |
magic |
4 位元组 | 格式识别值 |
0x04 |
cputype |
4 位元组 | CPU 架构 |
0x08 |
cpusubtype |
4 位元组 | CPU 子类型 |
0x0C |
filetype |
4 位元组 | Mach-O 档案类型 |
0x10 |
ncmds |
4 位元组 | 载入命令数量 |
0x14 |
sizeofcmds |
4 位元组 | 载入命令总大小 |
0x18 |
flags |
4 位元组 | 档案属性旗标 |
0x1C |
reserved |
4 位元组 | 保留栏位 |
总大小:
32 位元 Mach-O 标头包含前七个栏位,不含 reserved,因此大小为 28 位元组。
4.3. 魔术数字(Magic Number)
Mach-O 使用特定数值辨识标头格式及位元组顺序。
| 常数 | 数值 |
|---|---|
MH_MAGIC |
0xFEEDFACE |
MH_CIGAM |
0xCEFAEDFE |
MH_MAGIC_64 |
0xFEEDFACF |
MH_CIGAM_64 |
0xCFFAEDFE |
CIGAM 是对应 magic 值的位元组顺序反转形式。
例如,在小端序(Little-endian)系统中,32 位元数值 0xFEEDFACF 会依序储存为:
CF FA ED FE
因此,解析 Mach-O 时,必须正确处理实际位元组顺序,而不能只比较以文字表示的十六进位数值。
4.4. 档案类型(File Type)
filetype 栏位用于识别 Mach-O 的用途。
| 常数 | 数值 | 说明 |
|---|---|---|
MH_OBJECT |
0x1 |
可重定位目标档案 |
MH_EXECUTE |
0x2 |
可执行档 |
MH_CORE |
0x4 |
核心倾印映像 |
MH_DYLIB |
0x6 |
动态程式库 |
MH_DYLINKER |
0x7 |
动态连结器 |
MH_BUNDLE |
0x8 |
动态载入套件 |
MH_DSYM |
0xA |
除错资讯映像 |
MH_KEXT_BUNDLE |
0xB |
具有历史及平台限制的核心延伸套件类型 |
MH_FILESET |
0xC |
包含多个 Mach-O 组件的档案集合映像 |
这些常数定义于 Apple 的 Mach-O 标头档。
并非所有档案类型都可以像一般应用程式一样直接执行。
五、载入命令(Load Commands)
5.1. 定义
载入命令是 Mach-O 最重要的结构特征之一。
Mach 标头后方包含一系列载入命令,用于描述档案中的相关区域,以及载入、连结或其他处理所需的资讯。
Apple 定义的基本结构为:
struct load_command {
uint32_t cmd;
uint32_t cmdsize;
};
两个栏位总共占用 8 位元组。
cmd:载入命令的类型。cmdsize:整个命令的大小,包括相关附加资料。
解析器通常透过 cmdsize 计算下一个载入命令的位置。
5.2. 对齐规则
依照 Apple 的 Mach-O 结构定义:
- 32 位元 Mach-O 的载入命令大小须为 4 位元组的整数倍。
- 64 位元 Mach-O 的载入命令大小须为 8 位元组的整数倍。
必要时使用值为零的填充位元组。
因此,若某个 64 位元载入命令的原始资料长度为 27 位元组,其最终大小应依对齐要求调整。
若 cmdsize 不合法,可能导致解析器读取超出档案范围,甚至在错误的实作中出现无限回圈。
5.3. 常见载入命令
| 载入命令 | 主要功能 |
|---|---|
LC_SEGMENT |
描述 32 位元区段 |
LC_SEGMENT_64 |
描述 64 位元区段 |
LC_SYMTAB |
符号表与字串表的位置 |
LC_DYSYMTAB |
传统动态连结相关符号资讯 |
LC_LOAD_DYLIB |
宣告动态程式库相依性 |
LC_ID_DYLIB |
宣告动态程式库识别资讯 |
LC_LOAD_DYLINKER |
指定动态连结器 |
LC_MAIN |
提供程式进入点资讯 |
LC_RPATH |
补充程式库搜寻路径 |
LC_UUID |
二进位映像的唯一识别资讯 |
LC_CODE_SIGNATURE |
程式码签署资料的位置 |
LC_BUILD_VERSION |
建置平台、最低系统版本与 SDK 资讯 |
LC_DYLD_EXPORTS_TRIE |
汇出符号 Trie 结构的位置 |
LC_DYLD_CHAINED_FIXUPS |
链式修正资讯 |
LC_FILESET_ENTRY |
档案集合中的组件资讯 |
不是每一个 Mach-O 都包含上述所有命令。
不同档案类型,例如 MH_OBJECT 与 MH_EXECUTE,具有不同的技术需求。
5.4. 动态连结器必须理解的命令
Mach-O 定义:
#define LC_REQ_DYLD 0x80000000
此旗标用来标示动态连结器必须理解的载入命令类型。
依照 Apple 的定义,如果 dyld 遇到带有必要性旗标、但自身无法辨识的命令,可能拒绝载入该映像。
这种机制使格式可以加入具有必要语义的新命令,而不让旧版载入器错误地忽略它们。
六、区段(Segment)与节区(Section)
6.1. 区段的概念
区段用于描述二进位档案与程序虚拟记忆体之间的映射关系。
64 位元 Mach-O 使用以下结构:
struct segment_command_64 {
uint32_t cmd;
uint32_t cmdsize;
char segname[16];
uint64_t vmaddr;
uint64_t vmsize;
uint64_t fileoff;
uint64_t filesize;
vm_prot_t maxprot;
vm_prot_t initprot;
uint32_t nsects;
uint32_t flags;
};
此结构的固定部分为 72 位元组。
如果区段包含节区,其 section_64 纪录会接续在该区段命令之后。
6.2. 区段命令栏位
| 栏位 | 大小 | 说明 |
|---|---|---|
cmd |
4 位元组 | 命令类型 |
cmdsize |
4 位元组 | 命令及节区资料的总大小 |
segname |
16 位元组 | 区段名称 |
vmaddr |
8 位元组 | 虚拟记忆体位址 |
vmsize |
8 位元组 | 记忆体配置大小 |
fileoff |
8 位元组 | 档案偏移量 |
filesize |
8 位元组 | 档案中储存的资料大小 |
maxprot |
4 位元组 | 允许的最大记忆体保护权限 |
initprot |
4 位元组 | 初始记忆体保护权限 |
nsects |
4 位元组 | 节区数量 |
flags |
4 位元组 | 区段旗标 |
在许多情况下:
当记忆体大小大于档案中实际储存的内容时,额外区域可能依照载入规则以零值初始化。
6.3. 节区的概念
节区用于组织区段内的机器码、资料或中继资讯。
Mach-O 64 位元节区定义如下:
struct section_64 {
char sectname[16];
char segname[16];
uint64_t addr;
uint64_t size;
uint32_t offset;
uint32_t align;
uint32_t reloff;
uint32_t nreloc;
uint32_t flags;
uint32_t reserved1;
uint32_t reserved2;
uint32_t reserved3;
};
固定结构大小为 80 位元组。
| 栏位 | 功能 |
|---|---|
sectname |
节区名称 |
segname |
所属区段名称 |
addr |
节区的虚拟位址 |
size |
节区大小 |
offset |
实际档案资料的偏移量,适用于具有档案内容的节区 |
align |
对齐需求的指数 |
reloff |
重定位纪录的偏移量 |
nreloc |
重定位纪录数量 |
flags |
节区类型与属性 |
reserved1–3 |
依节区类型而有不同用途的附加栏位 |
6.4. 常见区段
| 区段名称 | 典型内容 |
|---|---|
__PAGEZERO |
不映射的低位址范围,用于降低空指标存取风险 |
__TEXT |
可执行程式码与相关唯读资料 |
__DATA |
可写入资料及相关结构 |
__DATA_CONST |
载入或连结处理后可施加保护的资料 |
__LINKEDIT |
符号、修正及其他连结资讯 |
上述名称常见于 Apple 平台,但并不代表 Mach-O 只能使用这些区段名称。
实际配置可能随作业系统、连结器版本及处理器架构而改变。
6.5. 常见节区
在 __TEXT 中:
| 节区 | 典型用途 |
|---|---|
__text |
机器指令 |
__cstring |
C 字串 |
__const |
部分常数资料 |
__stubs |
特定连结机制使用的呼叫存根 |
在资料区段中:
| 节区 | 典型用途 |
|---|---|
__data |
已初始化资料 |
__bss |
未初始化资料 |
__common |
传统共用资料物件 |
__la_symbol_ptr |
传统延迟符号指标区域 |
节区的名称、存在与位置均可能因工具链和 ABI 而不同。
七、符号表与重定位机制
7.1. 符号表(Symbol Table)
Mach-O 可以透过符号表描述函式、资料及其他可被识别的程式位置。
LC_SYMTAB 使用以下结构:
struct symtab_command {
uint32_t cmd;
uint32_t cmdsize;
uint32_t symoff;
uint32_t nsyms;
uint32_t stroff;
uint32_t strsize;
};
其中:
symoff:符号表在档案中的偏移量。nsyms:符号数量。stroff:字串表的偏移量。strsize:字串表大小。
64 位元 Mach-O 传统符号纪录使用 nlist_64 结构。
主要栏位包括:
n_strxn_typen_sectn_descn_value
nlist_64 的大小为 16 位元组。
7.2. 符号解析
假设程式需要呼叫外部程式库提供的 calculate 函式。
当编译器产生目前模组的目标档案时,可能尚无法得知该函式在最终映像中的位置。
连结器负责根据符号名称、可见性、系结规则及其他条件,建立对应关系。
若符号由动态程式库提供,部分资讯可能保留至 dyld 载入或连结阶段处理。
7.3. 重定位(Relocation)
重定位是指根据最终位址或符号资讯,调整尚未确定的位址参照。
在 MH_OBJECT 档案中,节区可以透过:
reloff
nreloc
描述重定位纪录的位置与数量。
具体重定位种类取决于 CPU 架构。
例如,x86-64 与 ARM64 对函式呼叫和相对位址的指令编码不同,因此其重定位类型和运算规则也不同。
7.4. 重定位与修正(Fixup)
重定位与修正具有密切关系,但不应在所有情况下视为完全相同的术语。
重定位(Relocation)
通常用于描述在连结或载入过程中,需要根据位址与符号资讯调整参照的纪录或运算。
修正(Fixup)
是更广泛的用语,表示需要被调整的二进位位置或相关操作。
在现代 Mach-O 中,Fixup 亦可指 dyld 使用的特定资料结构,例如链式修正(Chained Fixups)。
因此,传统 Mach-O 目标档案的重定位纪录,与现代执行映像中的链式修正资讯,不应被视为相同的资料结构。
八、Mach-O 的动态连结机制
8.1. 动态连结器 dyld
dyld 是 Apple 平台动态连结机制的重要组件。
当可执行程式需要使用外部动态程式库时,Mach-O 会记录相关资讯,以协助系统定位程式库并处理二进位参照。
常见命令包括:
LC_LOAD_DYLIB
LC_LOAD_DYLINKER
LC_RPATH
这些命令分别与动态程式库相依性、动态连结器及程式库搜寻路径有关。
8.2. 位址重新基准化(Rebase)
重新基准化用于处理映像实际载入位置与原先假定位址之间的差异。
若映像中的某些指标指向同一映像内的资料,当载入基准发生变化时,这些指标可能需要相应调整。
这类操作通常称为 Rebase。
8.3. 符号系结(Bind)
符号系结是将某个参照与相应的符号定义连接起来的过程。
在动态连结环境中,符号可能由另一个程式库提供。
dyld 必须根据二进位中记录的资讯及平台规则,处理相应的符号参照。
因此,可以概括为:
- Rebase: 主要处理映像位置变化造成的位址调整。
- Bind: 主要处理对其他映像或程式库所提供符号的参照。
8.4. 链式修正(Chained Fixups)
现代 Mach-O 支援:
LC_DYLD_CHAINED_FIXUPS
该命令指向以链式结构编码的修正资讯。
相较於单纯依赖传统重定位清单,链式修正可以透过特定资料结构描述需要处理的指标及其关联资讯。
相关内容可能包括:
- 链式修正的起始资讯。
- 指标格式。
- 汇入符号资料。
- Rebase 与 Bind 资讯。
- 不同处理器架构使用的指标表示方式。
链式修正并非所有历史版本 Mach-O 都必须具有的结构。
8.5. 汇出符号 Trie
Mach-O 亦可以使用:
LC_DYLD_EXPORTS_TRIE
指定汇出符号资料的 Trie 结构。
Trie 是一种树状资料结构,可利用字串共同前缀组织资讯。
在汇出符号的情境中,这有助于以结构化方式储存符号名称及相关资料。
8.6. dyld 共享快取
Apple 还使用 dyld 共享快取(dyld Shared Cache)来组织部分系统动态程式库。
共享快取并不等同於单一 Mach-O 档案。
它可以包含或组织多个 Mach-O 映像,并搭配额外的结构与中继资料,以支援系统程式库的载入和相关最佳化。
部分位址修正与连结处理可在快取建置阶段事先完成,减少执行阶段需要进行的工作。
因此,dyld Shared Cache 应被视为与 Mach-O 密切相关、但具有自身格式及管理机制的系统组件。
九、通用二进位档案(Universal Binary)
9.1. 定义
通用二进位档案是指在同一个容器档案中包含多种处理器架构版本的二进位程式。
例如,某个 macOS 应用程式可能同时支援:
- x86-64:Intel Mac。
- ARM64:Apple silicon Mac。
开发者可以分别编译两种架构的程式,再将它们整合至同一档案中。
9.2. 通用二进位档案的组成
Universal Binary 亦常称为 Fat Binary。
其外层结构提供架构清单与各个二进位映像的位置。
┌───────────────────────────────┐
│ Fat Header │
├───────────────────────────────┤
│ Architecture Table │
│ ├─ x86_64 │
│ └─ arm64 │
├───────────────────────────────┤
│ Mach-O Slice: x86_64 │
├───────────────────────────────┤
│ Mach-O Slice: arm64 │
└───────────────────────────────┘
每个架构切片(Architecture Slice)均包含其对应架构的 Mach-O 映像。
外层架构表不会取代每个切片内部的 Mach 标头。
9.3. Fat Header
传统 Fat Header 定义如下:
struct fat_header {
uint32_t magic;
uint32_t nfat_arch;
};
其中:
magic:Fat 格式识别值。nfat_arch:架构纪录数量。
传统 32 位元架构纪录为:
struct fat_arch {
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t offset;
uint32_t size;
uint32_t align;
};
此结构占用 20 位元组。
此外,Mach-O 生态系统也具有使用较大偏移量及大小栏位的 Fat 64 位元格式。
9.4. 使用 lipo 工具
Apple 提供 lipo 工具,用于检查与操作通用二进位档案。
查看架构:
lipo -archs program
查看资讯:
lipo -info program
撷取 ARM64 切片:
lipo -thin arm64 program -output program-arm64
合并两个架构版本:
lipo -create program-x86_64 program-arm64 \
-output program-universal
最后一项操作要求输入档案具备相容的格式与架构资讯。
9.5. 在 Apple 架构转换中的作用
通用二进位档案在 Apple 转换处理器架构的历史中具有重要作用。
无论是从 PowerPC 转移至 Intel,或从 Intel 转移至 Apple silicon,多架构封装都能让同一套软体支援不同世代的硬体。
但 Universal Binary 并不会自动将 x86-64 机器码转换成 ARM64 机器码。
每个架构切片仍必须由相应的编译工具产生。
十、程式进入点与载入流程
10.1. 程式进入点(Entry Point)
可执行档必须提供必要资讯,使系统能够确定程式的启动位置或启动方式。
Mach-O 的一种现代机制是:
LC_MAIN
其结构定义如下:
struct entry_point_command {
uint32_t cmd;
uint32_t cmdsize;
uint64_t entryoff;
uint64_t stacksize;
};
| 栏位 | 意义 |
|---|---|
cmd |
载入命令类型 |
cmdsize |
命令大小 |
entryoff |
依命令规定表示的进入点偏移量 |
stacksize |
要求的初始堆叠大小,若为零则依平台预设机制处理 |
在使用 LC_MAIN 的模式中,entryoff 依照与 __TEXT 区段相关的偏移规则表示。
因此,entryoff 不应直接被视为 C 语言函式 main 的绝对虚拟位址。
10.2. 载入流程概观
Mach-O 的载入与执行过程可概括如下:
Mach-O 档案
│
▼
验证格式与处理器架构
│
▼
读取 Header 与 Load Commands
│
▼
建立记忆体映射
│
▼
处理动态连结资讯
│
▼
准备执行环境
│
▼
移交控制权至启动程式码
│
▼
程式开始执行
此流程为概念上的简化。
实际工作可能由作业系统核心、dyld、语言执行阶段及其他启动组件共同完成,并非完全按照单一线性顺序运作。
10.3. Mach-O 与位址空间配置随机化
位址空间配置随机化(Address Space Layout Randomization,ASLR)是一种透过改变程式记忆体配置位置来提升安全性的机制。
当 Mach-O 映像被载入至不同于原始假定位址的位置时,系统可能需要调整相关指标。
这些操作可透过相应的修正资讯与载入机制完成。
但是,ASLR 并非 Mach-O 格式本身独立提供的功能。
其实际支援程度仍受到作业系统、架构、二进位属性及安全政策影响。
十一、程式码签署与安全机制
11.1. 程式码签署(Code Signing)
Mach-O 可以透过以下载入命令记录程式码签署资料的位置:
LC_CODE_SIGNATURE
此命令包含定位签署资料所需的资讯。
它并不表示整份签章资料直接储存在载入命令结构之内。
11.2. 程式码签署的用途
程式码签署可配合平台机制,用于:
- 验证程式码完整性。
- 确认签署身分资讯。
- 支援作业系统安全政策。
- 配合软体发布与安装程序。
不同 Apple 平台对程式码签署的要求并不完全相同。
因此,不能仅因 Mach-O 包含 LC_CODE_SIGNATURE,就推断作业系统一定允许执行该档案。
11.3. 二进位格式与执行权限
Mach-O 负责储存程式码及相关中继资料。
真正的验证、执行授权与安全政策,则由作业系统及相关安全机制决定。
换言之,格式结构正确的 Mach-O,不一定是符合平台执行政策的有效程式。
十二、Mach-O、ELF 与 PE/COFF 比较
12.1. 主要差异
| 比较项目 | Mach-O | ELF | PE/COFF |
|---|---|---|---|
| 主要平台 | Apple | Linux 与多种 Unix 系统 | Windows |
| 格式识别 | Magic Number | ELF Magic | MZ/PE 与 COFF |
| 主要标头 | Mach Header | ELF Header | DOS/PE/COFF Header |
| 记忆体映射描述 | Segment Load Commands | Program Headers | PE 节区表及相关映像机制 |
| 程式码与资料组织 | Segments、Sections | Segments、Sections | Sections |
| 符号资讯 | Mach-O 符号与连结资讯 | ELF 符号表 | COFF 符号及 PE 汇入/汇出资料 |
| 动态连结 | dyld | ELF 动态连结机制 | Windows Loader |
| 多架构封装 | Fat/Universal Binary | 一般情况下一个映像对应单一架构 | 一般情况下一个映像对应单一架构 |
| 平台版本资讯 | 相关 Load Commands | ABI、Notes 与平台机制 | Header、Subsystem 与中继资料 |
12.2. 结构设计的差异
Mach-O 的特色在于透过不同类型的载入命令,描述多种载入及连结资料。
ELF 则明确提供两种重要视角:
- 以节区为核心的连结视角。
- 以程式标头为核心的执行视角。
PE 则透过多层标头、节区表及资料目录,组织可执行映像与动态连结资讯。
三者具有相似的基本目的,但内部格式不能直接互换。
12.3. 修改标头能否将 Mach-O 转为 ELF?
一般而言,不能只透过修改 Magic Number 或标头就完成转换。
原因在于三者还存在以下差异:
- 节区与区段的资料结构。
- 重定位类型。
- 符号解析规则。
- 动态程式库相依资讯。
- ABI 与函式呼叫惯例。
- 进入点规范。
- 作业系统载入机制。
若要将 macOS 程式移植至 Linux,通常需要为目标平台重新编译及连结,并处理相关作业系统 API 的相容性问题。
十三、使用工具分析 Mach-O
13.1. 建立测试程式
建立 hello.c:
#include <stdio.h>
int mainvoid {
puts("Hello, Mach-O!");
return 0;
}
在已安装适当开发工具的 macOS 环境中:
clang hello.c -o hello
输出通常是符合目前目标架构的 Mach-O 可执行档。
13.2. 辨识档案类型
file hello
可以查看格式、架构及部分二进位属性。
具体输出会依作业系统版本、架构及编译选项而不同。
13.3. 检查 Mach Header
otool -hv hello
可以观察:
- Magic Number。
- CPU Type。
- CPU Subtype。
- File Type。
- Load Commands 数量。
- Load Commands 总大小。
- Flags。
13.4. 检查载入命令
otool -l hello
此命令是分析 Mach-O 结构的重要工具。
输出可能包含:
LC_SEGMENT_64
LC_LOAD_DYLIB
LC_MAIN
LC_BUILD_VERSION
LC_CODE_SIGNATURE
实际命令清单取决于二进位档案的建置方式。
13.5. 检查动态程式库相依性
otool -L hello
此命令可列出 Mach-O 中描述的相关动态程式库。
对于分析动态连结错误与部署问题特别有用。
13.6. 检查符号
nm hello
可使用适当选项进一步筛选符号。
如果二进位档案已经移除符号资讯(Stripped Binary),其传统符号表可能不完整,或无法提供原先全部的除错与分析资讯。
13.7. 检查架构
lipo -info hello
或:
lipo -archs hello
可以确认档案具有哪些处理器架构切片。
13.8. 使用 LLVM 工具
LLVM 亦提供可分析 Mach-O 的工具。
例如:
llvm-objdump --macho --private-headers hello
查看反组译:
llvm-objdump --disassemble hello
若分析 Universal Binary,必须确认工具正在处理预期的架构切片。
十四、Mach-O 解析器与产生器的设计要求
14.1. Mach-O 解析器
一个基本 Mach-O 解析器至少需要完成:
- 读取 Magic Number。
- 判断是单架构 Mach-O 还是 Fat Binary。
- 确定位元组顺序及标头类型。
- 检查最小档案大小。
- 解析 Mach Header。
- 验证
ncmds与sizeofcmds。 - 逐项解析 Load Commands。
- 检查区段与节区的偏移量及大小。
- 视需要读取符号表与其他资料。
- 拒绝超出档案范围或违反格式规则的结构。
解析器应特别防范整数溢位。
例如,检查资料范围时可使用条件:
但实作时不应在未检查的情况下直接相加。
较安全的检查方式是:
offset <= fileSize
size <= fileSize - offset
这样可以避免在无号整数运算中因加法溢位而误判资料范围合法。
14.2. Mach-O 产生器
如果编译器或连结器需要自行产生 Mach-O,至少必须正确建立相关的二进位结构。
对目标档案而言,主要工作可能包括:
- 建立 Mach Header。
- 指定 CPU 架构。
- 建立相关区段与节区。
- 配置机器码和资料。
- 产生符号资讯。
- 建立重定位纪录。
- 计算档案偏移量与结构大小。
对可执行映像而言,还可能包括:
- 建立符合载入要求的区段配置。
- 提供程式进入点资讯。
- 记录动态程式库相依性。
- 建立 dyld 所需的中继资料。
- 设定目标平台及版本资讯。
- 在必要情况下产生或加入程式码签署资料。
然而,产生结构合法的 Mach-O,不代表已经产生可以在所有 Apple 平台成功执行及分发的应用程式。
后者还涉及 SDK、ABI、执行阶段环境与平台安全政策。
14.3. 相容性测试
| 测试案例 | 目的 |
|---|---|
| 合法 64 位元 Mach-O | 验证基本标头与载入命令 |
| 截断档案 | 防止超出资料范围 |
cmdsize 为 0 |
防止错误遍历及无限回圈 |
| 载入命令未正确对齐 | 侦测格式规范违反 |
| 偏移量超出档案大小 | 侦测不合法资料 |
| 多架构 Fat Binary | 验证架构切片选择 |
| 不具有实体档案资料的节区 | 正确处理 Zerofill 类型 |
| 不同架构的重定位 | 验证重定位格式 |
| 包含 Chained Fixups | 正确识别修正资料 |
| 包含程式码签署资讯 | 验证签署资料的位置与大小 |
十五、结论
Mach-O 是 Apple 生态系统的重要二进位格式,贯穿程式编译、连结、载入及执行等阶段。
此格式的历史与 Mach、NeXT 及 Mac OS X 的发展密切相关,并随 Apple 平台演进而支援不同的硬体架构和系统需求。
Mach-O 最鲜明的特征,在于以 Mach 标头、载入命令、区段及节区为基础的档案组织方式。
透过这些结构,Mach-O 不仅能够保存机器码及资料,也能描述动态连结、程式进入点、目标平台、记忆体配置及部分安全相关资讯。
Universal Binary 使不同架构的 Mach-O 映像可以被封装于同一档案,对 Apple 多次处理器架构转换具有重要作用。
随着 dyld 技术的发展,链式修正与汇出符号 Trie 等结构,也反映 Mach-O 在现代连结与载入需求下的持续演进。
对于编译器、连结器及作业系统研究而言,理解 Mach-O 有助于掌握机器码如何被组织为可供系统辨识、映射及执行的二进位映像。
不过,Mach-O 仍只是程式与作业系统之间二进位介面的一部分。程式能否成功执行,还取决于 ABI、CPU 架构、执行阶段程式库及平台政策。
附录一:技术术语
| 中文术语 | 英文术语 | 说明 |
|---|---|---|
| Mach-O 格式 | Mach Object | Apple 生态系统使用的目标档案与执行映像格式 |
| Mach 标头 | Mach Header | 描述档案基本资讯的结构 |
| 载入命令 | Load Command | 描述载入、连结及档案处理资讯的纪录 |
| 区段 | Segment | 用于记忆体映射及资料组织的区域 |
| 节区 | Section | 区段内部的程式码或资料单位 |
| 符号表 | Symbol Table | 储存符号名称及相关资讯的结构 |
| 重定位 | Relocation | 根据位址与符号资讯调整参照 |
| 修正 | Fixup | 需要调整的二进位参照或相关操作 |
| 动态连结 | Dynamic Linking | 透过载入及执行阶段机制连结程式库 |
| 动态连结器 | Dynamic Linker | 负责执行阶段动态连结的组件 |
| 通用二进位档案 | Universal Binary | 在同一容器中储存多种架构映像的档案 |
| 架构切片 | Architecture Slice | Fat Binary 内特定架构的 Mach-O 映像 |
| 程式进入点 | Entry Point | 程式启动执行流程的位置 |
| 位址空间配置随机化 | Address Space Layout Randomization | 透过变动记忆体配置位置提高安全性 |
| 程式码签署 | Code Signing | 用于验证程式码完整性及支援平台安全政策的机制 |
| 应用程式二进位介面 | Application Binary Interface | 规范二进位组件互动方式的技术介面 |
附录二:参考文献
A. Apple 官方规格与原始码
[1] Apple, mach-o/loader.h, XNU Source Code.
Mach-O 标头、载入命令、区段、节区及相关常数的主要结构定义。
https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h
[2] Apple, mach-o/fat.h.
定义 Fat Binary 的标头及架构纪录。
https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/fat.h
[3] Apple, mach-o/nlist.h.
定义 Mach-O 符号纪录及相关属性。
https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/nlist.h
[4] Apple, Mach-O Documentation.
Apple 提供的 Mach-O 格式及二进位载入相关技术文件。
https://developer.apple.com/documentation/kernel/mach-o
B. 动态连结与修正机制
[5] Apple, dyld, Open Source Repository.
Apple 动态连结器及相关二进位处理组件的公开原始码。
https://github.com/apple-oss-distributions/dyld
[6] Apple, MachOFile.cpp.
与 Mach-O 结构分析、验证及相关处理有关的原始码。
https://github.com/apple-oss-distributions/dyld/blob/main/common/MachOFile.cpp
[7] Apple, fixup-chains.h.
链式修正资料结构及相关常数的定义。
https://github.com/apple-oss-distributions/dyld/blob/main/include/mach-o/fixup-chains.h
[8] Apple, Cache Builder.
描述 dyld 共享快取建置,以及 Rebase、Bind 和其他处理机制的技术资料。
https://github.com/apple-oss-distributions/dyld/blob/main/doc/CacheBuilder.md
C. 通用二进位档案
[9] Apple Developer Documentation, Building a Universal macOS Binary.
介绍如何建立同时支援 Intel 与 Apple silicon 的 macOS 二进位档案。
https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary
D. 工具与延伸研究
[10] LLVM Project, LLVM Object File Formats.
LLVM 对多种二进位档案格式的读取、产生与分析工具及相关原始码。
[11] LLVM Project, Mach-O LLD Port.
LLVM LLD 中 Mach-O 连结器的相关技术文件。
https://lld.llvm.org/MachO/index.html
[12] John R. Levine, Linkers and Loaders, Morgan Kaufmann, 1999.
研究连结器、载入器、符号解析、重定位与目标档案格式的专门著作。
注释
- Apple,
mach-o/loader.h,包括mach_header、mach_header_64、load_command、segment_command_64、section_64及相关 Mach-O 常数。https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h ↩ - Apple,
mach-o/fat.h,Fat Binary 的标头及架构纪录。https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/fat.h ↩ - Apple,
mach-o/nlist.h,Mach-O 符号纪录与相关定义。https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/nlist.h ↩ - Apple, dyld,动态连结与 Mach-O 相关处理的公开原始码。https://github.com/apple-oss-distributions/dyld ↩
- Apple, Building a Universal macOS Binary,关于 x86-64、ARM64 与
lipo的官方说明。https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary ↩ - Apple,
fixup-chains.h,链式修正资料结构定义。https://github.com/apple-oss-distributions/dyld/blob/main/include/mach-o/fixup-chains.h ↩ - Apple, Cache Builder,共享快取、Rebase、Bind 及相关建置机制。https://github.com/apple-oss-distributions/dyld/blob/main/doc/CacheBuilder.md ↩
规格适用范围说明
Mach-O 格式会随 Apple SDK、作业系统版本及工具链持续发展。Apple 公开原始码中的结构定义,是理解该格式的重要技术依据,但不代表所有内部实作细节都属于长期稳定且对第三方软体提供相容性保证的介面。
特别是自行产生 Mach-O 档案的编译器或连结器,应针对目标 SDK、处理器架构及作业系统版本进行相容性验证,而不能假设某个在特定 macOS 版本中有效的二进位布局,必然能被所有 Apple 平台接受。