Mach-O文件格式字段简介

概览

本次来学习下Mach-O文件的格式,Mach-O(Mach Object File Format)是 macOS 上的可执行文件格式。

Mach-O 文件格式如下:

新建一个单页面工程 使用 MachOView 来查看.app包内的mach-o文件:

结合可知 Mach-O 文件包含了三部分内容:

  • Header(头部),指明了 cpu 架构、大小端序、文件类型、Load Commands 个数等一些基本信息
  • Load Commands(加载命令),正如官方的图所示,描述了怎样加载每个 Segment 的信息。在 Mach-O 文件中可以有多个 Segment,每个 Segment 可能包含一个或多个 Section。
  • Data(数据区),Segment 的具体数据,包含了代码和数据等。

Headers

Mach-O 文件的头部定义如下:

1
2
3
4
5
6
7
8
9
10
11
// user/include / mach-o / loader.h
struct mach_header_64 {
uint32_t magic; /* 标志符 0xfeedface 是 32 位, 0xfeedfacf 是 64 位。*/
cpu_type_t cputype; /* cpu 类型、平台 */
cpu_subtype_t cpusubtype; /* cpu 类型、平台 */
uint32_t filetype; /* 文件类型,可执行文件、符号文件(DSYM)、内核扩展等 */
uint32_t ncmds; /* 加载 Load Commands 的数量 */
uint32_t sizeofcmds; /* 加载 Load Commands 的大小 */
uint32_t flags; /* dyld 动态链接器 加载的标志*/
uint32_t reserved; /* 64 位的保留字段 */
};

从上图中看出asdasd这个文件header对应的是 MH_MAGIC_64CPU_TYPE_X86_64_ALLMH_EXECUTE…等

filetype的定义有:

1
2
3
4
5
6
7
8
9
10
11
12
// user/include / mach-o / 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 内核扩展 */

和部分flags定义:

1
2
3
4
5
6
7
8
9
10
#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 来能个字段将会用在加载命令的过程中。

Load Commands

Headers之后就是Load Commands,其占用的内存和加载命令总数已经在Header中 ncmds/sizeofcmds 中指出。

load commands的定义如下:

1
2
3
4
5
// user/include / mach-o / loader.h
struct load_command {
uint32_t cmd; /* load command 类型*/
uint32_t cmdsize; /* command大小 用于计算出到下一个 command 的偏移量*/
};

cmd 字段指出了 command 类型,主要有:

1
2
3
4
5
6
// user/include / mach-o / loader.h
LC_SEGMENT、LC_SEGMENT_64 将 segment 映射到进程的内存空间,
LC_UUID 二进制文件 id,与符号表 uuid 对应,可用作符号表匹配,
LC_LOAD_DYLINKER 启动动态加载器,
LC_SYMTAB 描述在 __LINKEDIT 段的哪找字符串表、符号表,
LC_CODE_SIGNATURE 代码签名等

LC_SEGMENT_64和LC_SEGMENT是加载的主要命令,它负责指导内核来设置进程的内存空间。一般Mach-O文件有多个段(Segement),段每个段有不同的功能,一般包括:

  1. __PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;
  2. __TEXT: 包含了执行代码以及其他只读数据。该段数据的保护级别为:VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),防止在内存中被修改;
  3. __DATA: 包含了程序数据,该段可写;
  4. __LINKEDIT: 链接器使用的符号以及其他表

Segment & Section

Segment定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// user/include / mach-o / loader.h
struct segment_command_64 {
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* section_64 结构体所需要的空间 */
char segname[16]; /* segment 名字,上述宏中的定义 */
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; /* 标识符 */
};

其中 segname 在源码中定义的宏,有:

1
2
3
4
5
#define    SEG_PAGEZERO    "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,捕获到空指针 */
#define SEG_TEXT "__TEXT" /* 代码/只读数据段 */
#define SEG_DATA "__DATA" /* 数据段 */
#define SEG_OBJC "__OBJC" /* Objective-C runtime 段 */
#define SEG_LINKEDIT "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */

部分的 Segment (主要指的 __TEXT__DATA)可以进一步分解为 Section。之所以按照 Segment -> Section 的结构组织方式,是因为在同一个 Segment 下的 Section,可以控制相同的权限,也可以不完全按照 Page 的大小进行内存对其,节省内存的空间。而 Segment 对外整体暴露,在程序载入阶段映射成一个完整的虚拟内存,更好的做到内存对齐(可以继续参考 OS X & iOS Kernel Programming 一书的第一章内容)

Section定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// user/include / mach-o / loader.h
struct section_64 {
char sectname[16]; /* Section 名字 */
char segname[16]; /* Section 所在的 Segment 名称 */
uint64_t addr; /* Section 所在的内存地址 */
uint64_t size; /* Section 的大小 */
uint32_t offset; /* Section 所在的文件偏移 */
uint32_t align; /* Section 的内存对齐边界 (2 的次幂) */
uint32_t reloff; /* 重定位信息的文件偏移 */
uint32_t nreloc; /* 重定位条目的数目 */
uint32_t flags; /* 标志属性 */
uint32_t reserved1; /* 保留字段1 (for offset or index) */
uint32_t reserved2; /* 保留字段2 (for count or sizeof) */
uint32_t reserved3; /* 保留字段3 */
};

下面列举一些常见的 Section。

Section用途
__TEXT.__text主程序代码
__TEXT.__cstringC 语言字符串
__TEXT.__constconst 关键字修饰的常量
__TEXT.__stubs用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methnameObjective-C 方法名称
__TEXT.__objc_methtypeObjective-C 方法类型
__TEXT.__objc_classnameObjective-C 类名称
__DATA.__data初始化过的可变数据
__DATA.__la_symbol_ptrlazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.__nl_symbol_ptr非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const没有初始化过的常量
__DATA.__cfstring程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bssBSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__common没有初始化过的符号声明
__DATA.__objc_classlistObjective-C 类列表
__DATA.__objc_protolistObjective-C 原型
__DATA.__objc_imginfoObjective-C 镜像信息
__DATA.__objc_selfrefsObjective-C self 引用
__DATA.__objc_protorefsObjective-C 原型引用
__DATA.__objc_superrefsObjective-C 超类引用

__TEXT.__text 这里存放的是汇编后的代码,当我们进行编译时,每个.m文件会经过预编译->编译->汇编形成.o文件,称之为目标文件。汇编后,所有的代码会形成汇编指令存储在.o文件的(TEXT,text)区((DATA,data)也是类似)。链接后,所有的.o文件会合并成一个文件,所有.o文件的(TEXT,text)数据都会按链接顺序存放到应用文件的(TEXT,text)中。

__DATA.__data 存储数据的section,static在进行非零赋值后会存储在这里,如果static 变量没有赋值或者赋值为0,那么它会存储在__DATA.__bss中。

另外还有 Symbol Table符号表,这个是重点中的重点,符号表是将地址和符号联系起来的桥梁。符号表并不能直接存储符号,而是存储符号位于字符串表的位置。

String Table字符串表所有的变量名、函数名等,都以字符串的形式存储在字符串表中。

实践:关联类的方法名

上面说了一大堆定义,来查看使用Mach-O文件呢。这里来看一下如何用 MachO 文件关联类的方法名。

先来看看 Load Commands里的表示类名的 __objc_classname :

根据 offset字段 0000425F 值可以找到 section中对应的 __TEXT,__objc_classname 信息:

同理可以找到对应方法名的 __objc_methname 和 表示类虚拟地址的 __objc_classlist :

其中我们需要查看类的方法名信息就在__objc_classlist中,根据上图中 _OBJC_CLASS_$_ViewController 对应的data : 00000001000060E8,可以在__DATA,__objc_data中找到类结构信息:

其中data 是我们感兴趣的,它指向 class_ro_t, class_ro_t 存储了类在编译器就确定的属性、方法、协议等。根据data的值 0x100005370就找到了__DATA,__objc_const中:

其中baseMethods 0x100005350 指向了第一个方法。这样我们就就找到了类的方法名。

使用命令查看Mach-O信息

上文是借助 MachOView 这个工具 和 loader.h 来了解Mach-O文件的大概定义,现在我们来使用sizeotool 查看一下Mach-O文件。

如下这样一个 hello.c文件:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello world\n");
return 0;
}

使用clang生成Mach-O文件:

1
2
#生成 hello.out 的 Mach-O 二进制文件
xcrun clang hello.c -o hello.out

可以通过 file 命令来查看简要的架构信息:

1
2
3
file hello.out 
#输出:
hello.out: Mach-O 64-bit executable x86_64

一、 Section

就像我们上面提到的一样,这里有些东西叫做 section。一个可执行文件包含多个段,也就是多个 section。可执行文件不同的部分将加载进不同的 section,并且每个 section 会转换进某个 segment 里。这个概念对于所有的可执行文件都是成立的。

我们来看看 hello.out 二进制中的 section。我们可以使用 size 工具来观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
size -x -l -m hello.out 
#输出
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x2a (addr 0x100000f60 offset 3936)
Section __stubs: 0x6 (addr 0x100000f8a offset 3978)
Section __stub_helper: 0x1a (addr 0x100000f90 offset 3984)
Section __cstring: 0xd (addr 0x100000faa offset 4010)
Section __unwind_info: 0x48 (addr 0x100000fb8 offset 4024)
total 0x9f
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000

如上代码所示,我们的 hello.out 文件有 4 个 segment。有些 segment 中有多个 section。

当运行一个可执行文件时,虚拟内存 (VM - virtual memory) 系统将 segment 映射到进程的地址空间上。映射完全不同于我们一般的认识,如果你对虚拟内存系统不熟悉,可以简单的想象虚拟内存系统将整个可执行文件加载进内存 – 虽然在实际上不是这样的。VM 使用了一些技巧来避免全部加载。

当虚拟内存系统进行映射时,segment 和 section 会以不同的参数和权限被映射。

上面的代码中,__TEXT segment 包含了被执行的代码。它被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。这些代码也不能对自己做出修改,因此这些被映射的页从来不会被改变。

__DATA segment 以可读写和不可执行的方式映射。它包含了将会被更改的数据。

第一个 segment 是 __PAGEZERO。它的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前 4GB 被映射为 不可执行、不可写和不可读。这就是为什么当读写一个 NULL 指针或更小的值时会得到一个 EXC_BAD_ACCESS 错误。这是操作系统在尝试防止引起系统崩溃。

在 segment中,一般都会有多个 section。它们包含了可执行文件的不同部分。在 TEXT segment 中,text section 包含了编译所得到的机器码。stubs 和 stub_helper 是给动态链接器 (dyld) 使用的。通过这两个 section,在动态链接代码中,可以允许延迟链接。const (在我们的代码中没有) 是常量,不可变的,就像 cstring (包含了可执行文件中的字符串常量 – 在源码中被双引号包含的字符串) 常量一样。

DATA segment 中包含了可读写数据。在我们的程序中只有 nl_symbol_ptr 和 __la_symbol_ptr,它们分别是 non-lazy 和 lazy 符号指针。延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。而针对非延迟符号指针,当可执行文件被加载同时,也会被加载。

在 _DATA segment 中的其它常见 section 包括 const,在这里面会包含一些需要重定向的常量数据。例如 char * const p = “foo”; – p 指针指向的数据是可变的。bss section 没有被初始化的静态变量,例如 static int a; – ANSI C 标准规定静态变量必须设置为 0。并且在运行时静态变量的值是可以修改的。common section 包含未初始化的外部全局变量,跟 static 变量类似。例如在函数外面定义的 int a;。最后,dyld 是一个 section 占位符,被用于动态链接器。

二、Section 中的内容

可以通过 otool 来了解section中的内容

1
2
3
4
5
6
7
otool -s  __TEXT __text  hello.out 
#输出
hello.out:
Contents of (__TEXT,__text) section
0000000100000f60 55 48 89 e5 48 83 ec 10 c7 45 fc 00 00 00 00 48
0000000100000f70 8d 3d 34 00 00 00 b0 00 e8 0d 00 00 00 31 c9 89
0000000100000f80 45 f8 89 c8 48 83 c4 10 5d c3

我们还可以通过添加 -v 来查看反汇编代码 , 由于 -s TEXT text 很常见,otool 对其设置了一个缩写 -t :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
otool -v -t  hello.out
#输出
hello.out:
(__TEXT,__text) section
_main:
0000000100000f60 pushq %rbp
0000000100000f61 movq %rsp, %rbp
0000000100000f64 subq $0x10, %rsp
0000000100000f68 movl $0x0, -0x4(%rbp)
0000000100000f6f leaq 0x34(%rip), %rdi
0000000100000f76 movb $0x0, %al
0000000100000f78 callq 0x100000f8a
0000000100000f7d xorl %ecx, %ecx
0000000100000f7f movl %eax, -0x8(%rbp)
0000000100000f82 movl %ecx, %eax
0000000100000f84 addq $0x10, %rsp
0000000100000f88 popq %rbp
0000000100000f89 retq

上面的内容是一样的,只不过以反汇编形式显示出来。你应该感觉很熟悉,这就是我们在前面编译时候的代码。唯一的不同就是,在这里我们没有任何的汇编指令在里面。这是纯粹的二进制执行文件。