B416fda36ba65b0373a4574a7516186a
核心知识篇:Mach-O

1.什么是 Mach-O 文件?

Mach-O 是 Mach Object 的缩写,它是Mac/iOS 中用于存储程序、库的标准格式。作为 a.out 格式的替代,Mach-O 提供了更强的扩展性,并提升了符号表中信息的访问速度。

2.Mach-O 文件的格式

新建一个工程,打开如下路径,可以看到在工程配置中,Mach-O 文件具有多种格式:

下面我们来看看常见的 Mach-O 格式文件。

2.1 可执行文件

我们生成的 ipa 安装包中,就是这种可执行文件。这种太常见了,这里就不做详细的解说,后面我们还会经常和 ipa 包中的可执行文件打交道。

2.2 object 文件

这种是目标文件,主要有两大类。

2.2.1 .o 文件(目标文件)

小示例:.o 文件操作示例

  • 第一步:创建一个 GofOFileTest.c 文件。文件内容如下:
#include <stdio.h>

int main() {
    printf("GofOFileTest\n");
    return 0;
}
  • 第二步:使用指令编译。
clang -c GofOFileTest.c

会生成一个 GofOFileTest.o 文件。使用 file 指令查看该文件的格式:

> file GofOFileTest.o

//指令输出
> GofOFileTest.o: Mach-O 64-bit object x86_64

可以看到 .o 文件是一个 Mach-O 文件,但这个文件并不是一个可执行文件。

  • 第三步:生成可执行文件。
clang GofOFileTest.o

上面的指令会生成一个 a.out 的可执行文件。使用 file 指令查看该文件的格式:

> file a.out
// 输出
> a.out: Mach-O 64-bit executable x86_64

当然,我们也可以通过如下指令来生成一个指定名称为 GofOFileTest 的可执行文件:

clang -o GofOFileTest GofOFileTest.o
  • 第四步:执行可执行文件。
./a.out

可以看到对应的打印结果。
实际上,上面的编译和最终生成可执行文件的操作,也可以通过下面的一个命令来快速生成:

clang -o GofOFileTest GofOFileTest.c

补充示例:两个 .o 文件的链接

  • 第一步:在前面示例的基础上,新创建一个 GofOFileTest2.c 文件,并修改 GofOFileTest.c。内容如下:
// GofOFileTest2.c
void gofTest() {
    printf("GofTest\n");
    return;
}

// GofOFileTest.c
void gofTest();

int main() {
    printf("GofOFileTest\n");
    gofTest();  // 第二个示例添加
    return 0;
}
  • 第二步:直接编译链接两个文件,生成可执行文件:
clang -o GofOFileTest GofOFileTest2.c GofOFileTest.c
  • 第三步:执行可执行文件。
./GofOFileTest

当然,我们也可以拆解上面的操作,先生成目标文件。

// 编译
 clang -c GofOFileTest2.c GofOFileTest.c
 // 生成可执行文件
 clang -o GofOFileTest2 GofOFileTest2.o GofOFileTest.o
 // 执行可执行文件
 ./GofOFileTest2

2.2.2 .a 文件(静态库文件,多个 .o 文件的集合)

我们可以使用如下指令,来查看某个文件夹下的所【 .a】文件:

find /usr/local/lib -name "*.a"

也可以通过如下指令,查看【.a】文件的格式:

file /usr/local/lib/libpcrecpp.a

2.3 动态库文件

动态库文件主要有三种格式:

这里有一个补充说明的概念:动态库共享缓存。为了提高性能,系统的动态库文件都存在了动态库共享缓存里面。Mac 的动态库共享缓存,可以查看目录:【/private/var/db/dyld/dyld_shared_cache_x86_64h】。这个库里的文件,通过动态链接器(动态加载器)进行加载。

2.4 动态链接器(dyld)

在 Mac/iOS 系统中,都有动态链接器。Mac 系统中,动态链接器的位置:【/usr/lib/dyld】。我们也可以通过指令来查看 dyld 的格式:

> file /usr/lib/dyld
// 结果
> /usr/lib/dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
/usr/lib/dyld (for architecture x86_64):    Mach-O 64-bit dynamic linker x86_64
/usr/lib/dyld (for architecture i386):    Mach-O dynamic linker i386

3.通用二进制文件

在日常开发中,我们使用 DEBUG 模式进行程序的开发调试,生成的可执行文件只包含对应调试设备的架构。

// 指令
file GofMachO
// 输出
GofMachO: Mach-O 64-bit executable arm64

如果使用 Release 模式运行,可以看到生成的可执行文件包含多种架构:

// 指令
file GofMachO
// 输出
GofMachO: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64:Mach-O 64-bit executable arm64]
GofMachO (for architecture armv7):    Mach-O executable arm_v7
GofMachO (for architecture arm64):    Mach-O 64-bit executable arm64

这里要注意,系统最低版本要选择 11 以下,因为 iOS 11 之后,只会生成 arm 64 架构。

另外,release 模式在生成 ipa 包的同时,也生成了一个 dsym 文件。它也是一个 Mach-O 文件:

// 指令 
/Build/Products/Release-iphoneos/GofMachO.app.dSYM/Contents/Resources/DWARF > file GofMachO
// 输出
GofMachO: Mach-O 64-bit dSYM companion file arm64

从上面可以看到,Release 和 Debug 模式下,生成的是不同的,这个是在哪配置的呢?答案是在【Build Settings】进行配置。

上图中,有三个选项需要了解一下:

  • 【Build Active Architecture Only】:表示是否只编译当前设备的架构,Debug 模式下为 Yes,因此只生成了 Arm64 架构下的产物;Release 模式下为 No,就生成了多个架构。那么具体生成的架构会有哪些呢?结果是取【Valid Architectures】和【Architectures】配置项的交集。
  • 【Valid Architectures】:有效架构,可以看到有 4 种。
  • 【Architectures】:可以看到当前配置的是 $(ARCHS_STANDARD),这是一个环境变量,包含 armv7 和 arm64 两种架构。

所以上面的 release 模式下,生成的只有两个。如果这个时候,在 Architectures 中添加 arm64e,再次用 release 生成的时候,可以看到会包含 3 种架构:

// 指令
file GofMachO
// 结果
GofMachO: Mach-O universal binary with 3 architectures: [arm_v7:Mach-O executable arm_v7] [arm64:Mach-O 64-bit executable arm64] [arm64e:Mach-O 64-bit executable arm64e]
GofMachO (for architecture armv7):    Mach-O executable arm_v7
GofMachO (for architecture arm64):    Mach-O 64-bit executable arm64
GofMachO (for architecture arm64e):    Mach-O 64-bit executable arm64e

这种包含了多种架构的二进制文件,叫做 通用二进制文件。我们打包上传 Store 的时候,一般都会包含多种架构。这种通用二进制文件,我们在用 IDA 打开时,有一个专门的名称:Fat Mach-O file。

Apple 提出这个概念是为了解决一些历史原因,macOS(更确切的应该说是 OS X)最早是构建于 PPC 架构智商,后来才移植到 Intel 架构(从 Mac OS X Tiger 10.4.7 开始),通用二进制格式的二进制文件可以在 PPC 和 x86 两种处理器上执行。说到底,通用二进制格式只不过是对多架构的二进制文件的打包集合文件,而 macOS 中的多架构二进制文件也就是适配不同架构的 Mach-O 文件。

当然,我们也可以通过指令,对 通用二进制文件进行拆分,详见 组件库的二进制化,里面有 “lipo -remove” 和 “lipo -thin” 指令的使用示例。

4.MachO 文件结构

MachO 文件分为三大块:

  • Mach-O 头(Mach Header):这里描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息;
  • 加载命令(Load Commands):描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示。该部分用于告诉 loader 如何设置并加载二进制数据。
  • 数据区(Data):Data 中每一个段(Segment)的数据都保存在此,段的概念和 ELF 文件中段的概念类似,都拥有一个或多个 Section ,用来存放数据和代码。

除了上面的三大块,还有一部分叫 Loader Info (链接信息),不过一般我们把这部分内容看成 Data。它包含了动态加载器用来链接可执行文件或者依赖所需使用的符号表、字符串表等。

对于 通用二进制文件,它首先会有一个【Fat Header】,然后下面会包含多个上面的 「三大块」。

我们也可以通过下面的指令,来查看 Mach-O 文件的结构。

// 指令
otool -f GofMachO
/* 输出
Fat headers
fat_magic 0xcafebabe
nfat_arch 3
architecture 0
    cputype 12
    cpusubtype 9
    capabilities 0x0
    offset 16384
    size 75664
    align 2^14 (16384)
architecture 1
    cputype 16777228
    cpusubtype 0
    capabilities 0x0
    offset 98304
    size 76112
    align 2^14 (16384)
architecture 2
    cputype 16777228
    cpusubtype 2
    capabilities 0x0
    offset 180224
    size 75632
    align 2^14 (16384)
*/

4.1 Fat Header

Fat Header 的定义可以在【/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 8.2.simruntime/Contents/Resources/RuntimeRoot/usr/include/mach-o/fat.h】 中找到。

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 的幂 */
};

关于 magic 的取值,我们可以查看下表。加载器会通过这个符号来判断这是什么文件,通用二进制的 magic 为【0xcafebabe】。nfat_arch 字段指明当前的通用二进制文件中包含了多少个不同架构的 Mach-O 文件。fat_header 后会跟着多个 fat_arch,并与多个 Mach-O 文件及其描述信息(文件大小、CPU 架构、CPU 型号、内存对齐方式)相关联。

可执行格式 magic 用途
脚本 \x7FELF 主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 #! 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令
通用二进制格式 0xcafebabe、0xbebafeca 包含多种架构支持的二进制格式,只在 macOS 上支持
Mach-O 0xfeedface(32 位)、0xfeedfacf(64 位) macOS 的原生二进制格式

4.2 Mach Header

Header 包含整个二进制里的一般信息,它的定义可以在【/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 8.2.simruntime/Contents/Resources/RuntimeRoot/usr/include/mach-o/loader.h】 中找到。这里我们看一下 64 位下的定义:

struct mach_header_64 {
    uint32_t    magic;        /* mach magic 标识符,用来确定其属于 64位 还是 32位 */
    cpu_type_t    cputype;    /* CPU 类型标识符,同通用二进制格式中的定义 */
    cpu_subtype_t    cpusubtype;    /* CPU 子类型标识符,同通用二级制格式中的定义 */
    uint32_t    filetype;    /* 文件类型 */
    uint32_t    ncmds;        /* 加载器中加载命令(Load commands)的数量 */
    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        /* 静态链接文件(还不清楚是什么东西) */
                    /*  linking only, no section contents */
#define    MH_DSYM        0xa        /* 符号文件以及调试信息,在解析堆栈符号中常用 */
                    /*  sections */
#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 将会用在加载命令的过程中。

除了 MachOView 工具,我们也可以通过如下命令来查看 Mach-O 头部信息:

// 指令
otool -h GofMachO

/* 输出
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedface      12          9  0x00           2    22       2308 0x00200085
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777228          0  0x00           2    22       2720 0x00200085
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777228          2  0x00           2    22       2560 0x00200085
*/

4.3 Load Commands

先看两个数据定义:
```
struct segment_command_64 { /* for 64-bit architectures / uint32_t cmd; / LC_SEGMENT_64 */

top Created with Sketch.