Mach-O 文件格式探索
最近开始研究 iOS 逆向的相关知识,并且使用 MonkeyDev 对 WeChat 进行了实战。这里我放出后期会持续更新的个人项目 WeCheat。在逆向专题真正开始之前,需要系统的学习一些软件内幕知识。这篇文章将从二进制格式开始讲起,并探秘 Mach-O 文件格式内容。
进程与二进制格式
进程在众多操作系统中都有提及,它是作为一个正在执行的程序的实例,这是 UNIX 的一个基本概念。而进程的出现是特殊文件在内从中加载得到的结果,这种文件必须使用操作系统可以认知的格式,这样才对该文件引入依赖库,初始化运行环境以及顺利地执行创造条件。
Mach-O(Mach Object File Format)是 macOS 上的可执行文件格式,类似于 Linux 和大部分 UNIX 的原生格式 ELF(Extensible Firmware Interface)。为了更加全面的了解这块内容,我们看一下 macOS 支持的三种可执行格式:解释器脚本格式、通用二进制格式和 Mach-O 格式。
可执行格式 |
magic |
用途 |
脚本 |
\x7FELF |
主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 #! 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令 |
通用二进制格式 |
0xcafebabe
0xbebafeca |
包含多种架构支持的二进制格式,只在 macOS 上支持 |
Mach-O |
0xfeedface (32 位) 0xfeedfacf (64 位) |
macOS 的原生二进制格式 |
通用二进制格式(Universal Binary)
这个格式在有些资料中也叫胖二进制格式(Fat Binary),Apple 提出这个概念是为了解决一些历史原因,macOS(更确切的应该说是 OS X)最早是构建于 PPC 架构智商,后来才移植到 Intel 架构(从 Mac OS X Tiger 10.4.7 开始),通用二进制格式的二进制文件可以在 PPC 和 x86 两种处理器上执行。
说到底,通用二进制格式只不过是对多架构的二进制文件的打包集合文件,而 macOS 中的多架构二进制文件也就是适配不同架构的 Mach-O 文件。
Fat Header 的数据结构在 <mach-o/fat.h>
头文件中有定义,可以参看 /usr/include/mach-o/fat.h
找到定义头:
#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
uint32_t magic; /* FAT_MAGIC 或 FAT_MAGIC_64 */
uint32_t nfat_arch; /* 结构体实例的个数 */
};
struct fat_arch {
cpu_type_t cputype; /* cpu 说明符 (int) */
cpu_subtype_t cpusubtype; /* 指定 cpu 确切型号的整数 (int) */
uint32_t offset; /* CPU 架构数据相对于当前文件开头的偏移值 */
uint32_t size; /* 数据大小 */
uint32_t align; /* 数据内润对其边界,取值为 2 的幂 */
};
对于 cputype
和 cpusubtype
两个字段这里不讲述,可以参看 /usr/include/mach/machine.h
头中对其的定义,另外 Apple 官方文档中也有简单的描述。
在 fat_header
中,magic
也就是我们之前在表中罗列的 magic 标识符,也可以类比成 UNIX 中 ELF 文件的 magic 标识。加载器会通过这个符号来判断这是什么文件,通用二进制的 magic 为 0xcafebabe
。nfat_arch
字段指明当前的通用二进制文件中包含了多少个不同架构的 Mach-O 文件。fat_header
后会跟着多个 fat_arch
,并与多个 Mach-O 文件及其描述信息(文件大小、CPU 架构、CPU 型号、内存对齐方式)相关联。
这里可以通过 file
命令来查看简要的架构信息,这里以 iOS 平台 WeChat 4.5.1 版本为例:
~ file Desktop/WeChat.app/WeChat
Desktop/WeChat.app/WeChat: Mach-O universal binary with 2 architectures: [arm_v7: Mach-O executable arm_v7] [arm64]
Desktop/WeChat.app/WeChat (for architecture armv7): Mach-O executable arm_v7
Desktop/WeChat.app/WeChat (for architecture arm64): Mach-O 64-bit executable arm64
进一步,也可以使用 otool
工具来打印其 fat_header
详细信息:
~ otool -f -V Desktop/WeChat.app/WeChat
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture armv7
cputype CPU_TYPE_ARM
cpusubtype CPU_SUBTYPE_ARM_V7
capabilities 0x0
offset 16384
size 56450224
align 2^14 (16384)
architecture arm64
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64_ALL
capabilities 0x0
offset 56475648
size 64571648
align 2^14 (16384)
之后我们用 Synalyze It! 来查看 WeChat 的 Mach64 Header 的效果:

- 从第一个段中得到
magic = 0xcafebabe
,说明是 FAT_MAGIC
。
- 第二段中所存储的字段为
nfat_arch = 0x00000002
,说明该 App 中包含了两种 CPU 架构。
- 后续的则是
fat_arch
结构体中的内容,cputype(0x0000000c)
、cpusubtype(0x00000009)
、offset(0x00004000)
、size(0x03505C00)
等等。需要臧帅闯是如果只含有一种 CPU 架构,是没有 fat 头定义的,这部分则可跳过,从而直接过去 arch
数据。
Mach-O 文件格式
由上所知一个通用二进制格式包含了很多个 Mach-O 文件格式,下面我们来具体说说这个格式。Mach-O 文件格式在官方文档中有一个描述图,是很多教程中都引用到的,我重新绘制了一版更清晰的:

可以看的出 Mach-O 主要由 3 部分组成:
- Mach-O 头(Mach Header):这里描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息;
- 加载命令(Load Command):描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示;
- 数据区(Data):Data 中每一个段(Segment)的数据都保存在此,段的概念和 ELF 文件中段的概念类似,都拥有一个或多个 Section ,用来存放数据和代码。
Mach-O 头
与 Mach-O 文件格式有关的结构体定义都可以从 /usr/include/mach-o/loader.h
中找到,也就是 <mach-o/loader.h>
头。以下只给出 64 位定义的代码,因为 32 位的区别是缺少了一个预留字段:
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
struct mach_header_64 {
uint32_t magic; /* mach magic 标识符 */
cpu_type_t cputype; /* CPU 类型标识符,同通用二进制格式中的定义 */
cpu_subtype_t cpusubtype; /* CPU 子类型标识符,同通用二级制格式中的定义 */
uint32_t filetype; /* 文件类型 */
uint32_t ncmds; /* 加载器中加载命令的条数 */
uint32_t sizeofcmds; /* 加载器中加载命令的总大小 */
uint32_t flags; /* dyld 的标志 */
uint32_t reserved; /* 64 位的保留字段 */
};
由于 Mach-O 支持多种类型文件,所以此处引入了 filetype
字段来标明,这些文件类型定义在 loader.h
文件中同样可以找到。
#define MH_OBJECT 0x1 /* Target 文件:编译器对源码编译后得到的中间结果 */
#define MH_EXECUTE 0x2 /* 可执行二进制文件 */
#define MH_FVMLIB 0x3 /* VM 共享库文件(还不清楚是什么东西) */
#define MH_CORE 0x4 /* Core 文件,一般在 App Crash 产生 */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* 动态库 */
#define MH_DYLINKER 0x7 /* 动态连接器 /usr/lib/dyld */
#define MH_BUNDLE 0x8 /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
#define MH_DYLIB_STUB 0x9 /* 静态链接文件(还不清楚是什么东西) */
#define MH_DSYM 0xa /* 符号文件以及调试信息,在解析堆栈符号中常用 */
#define MH_KEXT_BUNDLE 0xb /* x86_64 内核扩展 */
另外在 loader.h
中还可以找到 flags
中所取值的全部定义,这里只介绍常用的:
#define MH_NOUNDEFS 0x1 /* Target 文件中没有带未定义的符号,常为静态二进制文件 */
#define MH_SPLIT_SEGS 0x20 /* Target 文件中的只读 Segment 和可读写 Segment 分开 */
#define MH_TWOLEVEL 0x80 /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
#define MH_FORCE_FLAT 0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
#define MH_PIE 0x200000 /* 对可执行的文件类型启用地址空间 layout 随机化 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且 ncmds
和 sizeofcmds
来能个字段将会用在加载命令的过程中。
Mach-O Data
加载命令在 Mach-O 文件加载解析时,会被内核加载器或者动态链接器调用。这些指令都采用 Type-Size-Value
这种格式,即:32 位的 cmd
值(表示类型),32 位的 cmdsize
值(32 位二级制位 4 的倍数,64 位位 8 的倍数),以及命令本身(由 cmdsize
指定的长度)。内核加载器使用的命令可以参看 xnu 源码来学习,其他命令则是由动态链接器处理的。
在正式进入加载命令这一过程之前,先来学习一下 Mach-O 的 Data 区域,其中由 Segment 段和 Section 节组成。先来说 Segment 的组成,以下代码仍旧来自 loader.h
:
```c
define SEG_PAGEZERO "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,捕获到空指针 */
define SEG_TEXT "__TEXT" /* 代码/只读数据段 */
Mach-O 文件格式探索
最近开始研究 iOS 逆向的相关知识,并且使用 MonkeyDev 对 WeChat 进行了实战。这里我放出后期会持续更新的个人项目 WeCheat。在逆向专题真正开始之前,需要系统的学习一些软件内幕知识。这篇文章将从二进制格式开始讲起,并探秘 Mach-O 文件格式内容。
进程与二进制格式
进程在众多操作系统中都有提及,它是作为一个正在执行的程序的实例,这是 UNIX 的一个基本概念。而进程的出现是特殊文件在内从中加载得到的结果,这种文件必须使用操作系统可以认知的格式,这样才对该文件引入依赖库,初始化运行环境以及顺利地执行创造条件。
Mach-O(Mach Object File Format)是 macOS 上的可执行文件格式,类似于 Linux 和大部分 UNIX 的原生格式 ELF(Extensible Firmware Interface)。为了更加全面的了解这块内容,我们看一下 macOS 支持的三种可执行格式:解释器脚本格式、通用二进制格式和 Mach-O 格式。
可执行格式 |
magic |
用途 |
脚本 |
\x7FELF |
主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 #! 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令 |
通用二进制格式 |
0xcafebabe
0xbebafeca |
包含多种架构支持的二进制格式,只在 macOS 上支持 |
Mach-O |
0xfeedface (32 位) 0xfeedfacf (64 位) |
macOS 的原生二进制格式 |
通用二进制格式(Universal Binary)
这个格式在有些资料中也叫胖二进制格式(Fat Binary),Apple 提出这个概念是为了解决一些历史原因,macOS(更确切的应该说是 OS X)最早是构建于 PPC 架构智商,后来才移植到 Intel 架构(从 Mac OS X Tiger 10.4.7 开始),通用二进制格式的二进制文件可以在 PPC 和 x86 两种处理器上执行。
说到底,通用二进制格式只不过是对多架构的二进制文件的打包集合文件,而 macOS 中的多架构二进制文件也就是适配不同架构的 Mach-O 文件。
Fat Header 的数据结构在 <mach-o/fat.h>
头文件中有定义,可以参看 /usr/include/mach-o/fat.h
找到定义头:
#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
uint32_t magic; /* FAT_MAGIC 或 FAT_MAGIC_64 */
uint32_t nfat_arch; /* 结构体实例的个数 */
};
struct fat_arch {
cpu_type_t cputype; /* cpu 说明符 (int) */
cpu_subtype_t cpusubtype; /* 指定 cpu 确切型号的整数 (int) */
uint32_t offset; /* CPU 架构数据相对于当前文件开头的偏移值 */
uint32_t size; /* 数据大小 */
uint32_t align; /* 数据内润对其边界,取值为 2 的幂 */
};
对于 cputype
和 cpusubtype
两个字段这里不讲述,可以参看 /usr/include/mach/machine.h
头中对其的定义,另外 Apple 官方文档中也有简单的描述。
在 fat_header
中,magic
也就是我们之前在表中罗列的 magic 标识符,也可以类比成 UNIX 中 ELF 文件的 magic 标识。加载器会通过这个符号来判断这是什么文件,通用二进制的 magic 为 0xcafebabe
。nfat_arch
字段指明当前的通用二进制文件中包含了多少个不同架构的 Mach-O 文件。fat_header
后会跟着多个 fat_arch
,并与多个 Mach-O 文件及其描述信息(文件大小、CPU 架构、CPU 型号、内存对齐方式)相关联。
这里可以通过 file
命令来查看简要的架构信息,这里以 iOS 平台 WeChat 4.5.1 版本为例:
~ file Desktop/WeChat.app/WeChat
Desktop/WeChat.app/WeChat: Mach-O universal binary with 2 architectures: [arm_v7: Mach-O executable arm_v7] [arm64]
Desktop/WeChat.app/WeChat (for architecture armv7): Mach-O executable arm_v7
Desktop/WeChat.app/WeChat (for architecture arm64): Mach-O 64-bit executable arm64
进一步,也可以使用 otool
工具来打印其 fat_header
详细信息:
~ otool -f -V Desktop/WeChat.app/WeChat
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture armv7
cputype CPU_TYPE_ARM
cpusubtype CPU_SUBTYPE_ARM_V7
capabilities 0x0
offset 16384
size 56450224
align 2^14 (16384)
architecture arm64
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64_ALL
capabilities 0x0
offset 56475648
size 64571648
align 2^14 (16384)
之后我们用 Synalyze It! 来查看 WeChat 的 Mach64 Header 的效果:

- 从第一个段中得到
magic = 0xcafebabe
,说明是 FAT_MAGIC
。
- 第二段中所存储的字段为
nfat_arch = 0x00000002
,说明该 App 中包含了两种 CPU 架构。
- 后续的则是
fat_arch
结构体中的内容,cputype(0x0000000c)
、cpusubtype(0x00000009)
、offset(0x00004000)
、size(0x03505C00)
等等。需要臧帅闯是如果只含有一种 CPU 架构,是没有 fat 头定义的,这部分则可跳过,从而直接过去 arch
数据。
Mach-O 文件格式
由上所知一个通用二进制格式包含了很多个 Mach-O 文件格式,下面我们来具体说说这个格式。Mach-O 文件格式在官方文档中有一个描述图,是很多教程中都引用到的,我重新绘制了一版更清晰的:

可以看的出 Mach-O 主要由 3 部分组成:
- Mach-O 头(Mach Header):这里描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息;
- 加载命令(Load Command):描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示;
- 数据区(Data):Data 中每一个段(Segment)的数据都保存在此,段的概念和 ELF 文件中段的概念类似,都拥有一个或多个 Section ,用来存放数据和代码。
Mach-O 头
与 Mach-O 文件格式有关的结构体定义都可以从 /usr/include/mach-o/loader.h
中找到,也就是 <mach-o/loader.h>
头。以下只给出 64 位定义的代码,因为 32 位的区别是缺少了一个预留字段:
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
struct mach_header_64 {
uint32_t magic; /* mach magic 标识符 */
cpu_type_t cputype; /* CPU 类型标识符,同通用二进制格式中的定义 */
cpu_subtype_t cpusubtype; /* CPU 子类型标识符,同通用二级制格式中的定义 */
uint32_t filetype; /* 文件类型 */
uint32_t ncmds; /* 加载器中加载命令的条数 */
uint32_t sizeofcmds; /* 加载器中加载命令的总大小 */
uint32_t flags; /* dyld 的标志 */
uint32_t reserved; /* 64 位的保留字段 */
};
由于 Mach-O 支持多种类型文件,所以此处引入了 filetype
字段来标明,这些文件类型定义在 loader.h
文件中同样可以找到。
#define MH_OBJECT 0x1 /* Target 文件:编译器对源码编译后得到的中间结果 */
#define MH_EXECUTE 0x2 /* 可执行二进制文件 */
#define MH_FVMLIB 0x3 /* VM 共享库文件(还不清楚是什么东西) */
#define MH_CORE 0x4 /* Core 文件,一般在 App Crash 产生 */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* 动态库 */
#define MH_DYLINKER 0x7 /* 动态连接器 /usr/lib/dyld */
#define MH_BUNDLE 0x8 /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
#define MH_DYLIB_STUB 0x9 /* 静态链接文件(还不清楚是什么东西) */
#define MH_DSYM 0xa /* 符号文件以及调试信息,在解析堆栈符号中常用 */
#define MH_KEXT_BUNDLE 0xb /* x86_64 内核扩展 */
另外在 loader.h
中还可以找到 flags
中所取值的全部定义,这里只介绍常用的:
#define MH_NOUNDEFS 0x1 /* Target 文件中没有带未定义的符号,常为静态二进制文件 */
#define MH_SPLIT_SEGS 0x20 /* Target 文件中的只读 Segment 和可读写 Segment 分开 */
#define MH_TWOLEVEL 0x80 /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
#define MH_FORCE_FLAT 0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
#define MH_PIE 0x200000 /* 对可执行的文件类型启用地址空间 layout 随机化 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且 ncmds
和 sizeofcmds
来能个字段将会用在加载命令的过程中。
Mach-O Data
加载命令在 Mach-O 文件加载解析时,会被内核加载器或者动态链接器调用。这些指令都采用 Type-Size-Value
这种格式,即:32 位的 cmd
值(表示类型),32 位的 cmdsize
值(32 位二级制位 4 的倍数,64 位位 8 的倍数),以及命令本身(由 cmdsize
指定的长度)。内核加载器使用的命令可以参看 xnu 源码来学习,其他命令则是由动态链接器处理的。
在正式进入加载命令这一过程之前,先来学习一下 Mach-O 的 Data 区域,其中由 Segment 段和 Section 节组成。先来说 Segment 的组成,以下代码仍旧来自 loader.h
:
```c
define SEG_PAGEZERO "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,捕获到空指针 */
define SEG_TEXT "__TEXT" /* 代码/只读数据段 */
Mach-O 文件格式探索
最近开始研究 iOS 逆向的相关知识,并且使用 MonkeyDev 对 WeChat 进行了实战。这里我放出后期会持续更新的个人项目 WeCheat。在逆向专题真正开始之前,需要系统的学习一些软件内幕知识。这篇文章将从二进制格式开始讲起,并探秘 Mach-O 文件格式内容。
进程与二进制格式
进程在众多操作系统中都有提及,它是作为一个正在执行的程序的实例,这是 UNIX 的一个基本概念。而进程的出现是特殊文件在内从中加载得到的结果,这种文件必须使用操作系统可以认知的格式,这样才对该文件引入依赖库,初始化运行环境以及顺利地执行创造条件。
Mach-O(Mach Object File Format)是 macOS 上的可执行文件格式,类似于 Linux 和大部分 UNIX 的原生格式 ELF(Extensible Firmware Interface)。为了更加全面的了解这块内容,我们看一下 macOS 支持的三种可执行格式:解释器脚本格式、通用二进制格式和 Mach-O 格式。
可执行格式 |
magic |
用途 |
脚本 |
\x7FELF |
主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 #! 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令 |
通用二进制格式 |
0xcafebabe
0xbebafeca |
包含多种架构支持的二进制格式,只在 macOS 上支持 |
Mach-O |
0xfeedface (32 位) 0xfeedfacf (64 位) |
macOS 的原生二进制格式 |
通用二进制格式(Universal Binary)
这个格式在有些资料中也叫胖二进制格式(Fat Binary),Apple 提出这个概念是为了解决一些历史原因,macOS(更确切的应该说是 OS X)最早是构建于 PPC 架构智商,后来才移植到 Intel 架构(从 Mac OS X Tiger 10.4.7 开始),通用二进制格式的二进制文件可以在 PPC 和 x86 两种处理器上执行。
说到底,通用二进制格式只不过是对多架构的二进制文件的打包集合文件,而 macOS 中的多架构二进制文件也就是适配不同架构的 Mach-O 文件。
Fat Header 的数据结构在 <mach-o/fat.h>
头文件中有定义,可以参看 /usr/include/mach-o/fat.h
找到定义头:
#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
uint32_t magic; /* FAT_MAGIC 或 FAT_MAGIC_64 */
uint32_t nfat_arch; /* 结构体实例的个数 */
};
struct fat_arch {
cpu_type_t cputype; /* cpu 说明符 (int) */
cpu_subtype_t cpusubtype; /* 指定 cpu 确切型号的整数 (int) */
uint32_t offset; /* CPU 架构数据相对于当前文件开头的偏移值 */
uint32_t size; /* 数据大小 */
uint32_t align; /* 数据内润对其边界,取值为 2 的幂 */
};
对于 cputype
和 cpusubtype
两个字段这里不讲述,可以参看 /usr/include/mach/machine.h
头中对其的定义,另外 Apple 官方文档中也有简单的描述。
在 fat_header
中,magic
也就是我们之前在表中罗列的 magic 标识符,也可以类比成 UNIX 中 ELF 文件的 magic 标识。加载器会通过这个符号来判断这是什么文件,通用二进制的 magic 为 0xcafebabe
。nfat_arch
字段指明当前的通用二进制文件中包含了多少个不同架构的 Mach-O 文件。fat_header
后会跟着多个 fat_arch
,并与多个 Mach-O 文件及其描述信息(文件大小、CPU 架构、CPU 型号、内存对齐方式)相关联。
这里可以通过 file
命令来查看简要的架构信息,这里以 iOS 平台 WeChat 4.5.1 版本为例:
~ file Desktop/WeChat.app/WeChat
Desktop/WeChat.app/WeChat: Mach-O universal binary with 2 architectures: [arm_v7: Mach-O executable arm_v7] [arm64]
Desktop/WeChat.app/WeChat (for architecture armv7): Mach-O executable arm_v7
Desktop/WeChat.app/WeChat (for architecture arm64): Mach-O 64-bit executable arm64
进一步,也可以使用 otool
工具来打印其 fat_header
详细信息:
~ otool -f -V Desktop/WeChat.app/WeChat
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture armv7
cputype CPU_TYPE_ARM
cpusubtype CPU_SUBTYPE_ARM_V7
capabilities 0x0
offset 16384
size 56450224
align 2^14 (16384)
architecture arm64
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64_ALL
capabilities 0x0
offset 56475648
size 64571648
align 2^14 (16384)
之后我们用 Synalyze It! 来查看 WeChat 的 Mach64 Header 的效果:

- 从第一个段中得到
magic = 0xcafebabe
,说明是 FAT_MAGIC
。
- 第二段中所存储的字段为
nfat_arch = 0x00000002
,说明该 App 中包含了两种 CPU 架构。
- 后续的则是
fat_arch
结构体中的内容,cputype(0x0000000c)
、cpusubtype(0x00000009)
、offset(0x00004000)
、size(0x03505C00)
等等。需要臧帅闯是如果只含有一种 CPU 架构,是没有 fat 头定义的,这部分则可跳过,从而直接过去 arch
数据。
Mach-O 文件格式
由上所知一个通用二进制格式包含了很多个 Mach-O 文件格式,下面我们来具体说说这个格式。Mach-O 文件格式在官方文档中有一个描述图,是很多教程中都引用到的,我重新绘制了一版更清晰的:

可以看的出 Mach-O 主要由 3 部分组成:
- Mach-O 头(Mach Header):这里描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息;
- 加载命令(Load Command):描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示;
- 数据区(Data):Data 中每一个段(Segment)的数据都保存在此,段的概念和 ELF 文件中段的概念类似,都拥有一个或多个 Section ,用来存放数据和代码。
Mach-O 头
与 Mach-O 文件格式有关的结构体定义都可以从 /usr/include/mach-o/loader.h
中找到,也就是 <mach-o/loader.h>
头。以下只给出 64 位定义的代码,因为 32 位的区别是缺少了一个预留字段:
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
struct mach_header_64 {
uint32_t magic; /* mach magic 标识符 */
cpu_type_t cputype; /* CPU 类型标识符,同通用二进制格式中的定义 */
cpu_subtype_t cpusubtype; /* CPU 子类型标识符,同通用二级制格式中的定义 */
uint32_t filetype; /* 文件类型 */
uint32_t ncmds; /* 加载器中加载命令的条数 */
uint32_t sizeofcmds; /* 加载器中加载命令的总大小 */
uint32_t flags; /* dyld 的标志 */
uint32_t reserved; /* 64 位的保留字段 */
};
由于 Mach-O 支持多种类型文件,所以此处引入了 filetype
字段来标明,这些文件类型定义在 loader.h
文件中同样可以找到。
#define MH_OBJECT 0x1 /* Target 文件:编译器对源码编译后得到的中间结果 */
#define MH_EXECUTE 0x2 /* 可执行二进制文件 */
#define MH_FVMLIB 0x3 /* VM 共享库文件(还不清楚是什么东西) */
#define MH_CORE 0x4 /* Core 文件,一般在 App Crash 产生 */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* 动态库 */
#define MH_DYLINKER 0x7 /* 动态连接器 /usr/lib/dyld */
#define MH_BUNDLE 0x8 /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
#define MH_DYLIB_STUB 0x9 /* 静态链接文件(还不清楚是什么东西) */
#define MH_DSYM 0xa /* 符号文件以及调试信息,在解析堆栈符号中常用 */
#define MH_KEXT_BUNDLE 0xb /* x86_64 内核扩展 */
另外在 loader.h
中还可以找到 flags
中所取值的全部定义,这里只介绍常用的:
#define MH_NOUNDEFS 0x1 /* Target 文件中没有带未定义的符号,常为静态二进制文件 */
#define MH_SPLIT_SEGS 0x20 /* Target 文件中的只读 Segment 和可读写 Segment 分开 */
#define MH_TWOLEVEL 0x80 /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
#define MH_FORCE_FLAT 0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
#define MH_PIE 0x200000 /* 对可执行的文件类型启用地址空间 layout 随机化 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且 ncmds
和 sizeofcmds
来能个字段将会用在加载命令的过程中。
Mach-O Data
加载命令在 Mach-O 文件加载解析时,会被内核加载器或者动态链接器调用。这些指令都采用 Type-Size-Value
这种格式,即:32 位的 cmd
值(表示类型),32 位的 cmdsize
值(32 位二级制位 4 的倍数,64 位位 8 的倍数),以及命令本身(由 cmdsize
指定的长度)。内核加载器使用的命令可以参看 xnu 源码来学习,其他命令则是由动态链接器处理的。
在正式进入加载命令这一过程之前,先来学习一下 Mach-O 的 Data 区域,其中由 Segment 段和 Section 节组成。先来说 Segment 的组成,以下代码仍旧来自 loader.h
:
```c
define SEG_PAGEZERO "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,捕获到空指针 */
define SEG_TEXT "__TEXT" /* 代码/只读数据段 */