Mach-O
Mach-O是Mach Object文件格式的缩写。它是用于可执行文件,动态库,目标代码的文件格式。作为a.out格式的替代,Mach-O格式提供了更强的扩展性,以及更快的符号表信息访问速度。
Mach-O格式为大部分基于Mach内核的操作系统所使用的,包括NeXTSTEP, Mac OS X和iOS,它们都以Mach-O格式作为其可执行文件,动态库,目标代码的文件格式。
具体到我们的iOS程序,当用XCode打包后,会生成一个.app为扩展名的文件(位于工程目录/Products文件夹下),其实.app是一个文件夹,我们用鼠标右键选择‘Show Package contents’,就可以查看文件夹的内容,其中会发现有一个和我们工程同名的unix 可执行文件,这个就是iOS可执行文件,它是符合Mach-O格式的。
Mach-O文件结构
关于Mach-O的文件格式,在苹果官网已经找不到相关说明了,但是你可以通过下面链接获取PDF版说明:
Mach-O File Format Reference
Mach-O格式如下图所示,它被分为header, load commands, data三大部分:
header:对Mach-O文件的一个概要说明,包括Magic Number, 支持的CPU类型等。
load commands: 当系统加载Mach-O文件时,load command会指导苹果的动态加载器(dyld)h或内核,该如何加载文件的Data数据。
data: Mach-O文件的数据区,包含代码和数据。其中包含若干Segment块(注意,除了Segment块之外,还有别的内容,包括code signature,符号表之类,不要被苹果的图所误导!),每个Segment块中包含0个或多个seciton。Segment根据对应的load command被dyld加载入内存中。
我们可以使用MachOView(一个查看MachO 格式文件信息的开源工具)工具来查看一个具体的文件的Mach-O格式。
header
我们以一个普通的iOS APP为例,看看Mach-O文件header部分的具体内容。通过MachOView打开可执行文件,可以看到header的结构:
是不是有些懵?下面我们就结合Darwin内核源码,来了解下Mach header的定义。
Mach header的定义位于Darwin源码中的 EXTERNAL_HEADERS/mach-o/loader.h 中:
32位:
struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};
64位:
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
可以看到,32位和64位的Mach header基本相同,只不过64位header中多了一个保留参数reserved。
- magic:魔数,用来标识这是一个Mach-O文件,有32位和64位两个版本:
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
- cputype:支持的CPU架构类型,如arm。
- cpusubtype:在支持的CPU架构类型下,所支持的具体机器型号。在我们的例子中,APP是支持所有arm64的机型的:CUP_SUBTYPE_ARM64_ALL
- filetype: Mach-O的文件类型。包括:
#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 内核扩展 */
- ncmds:load command的数量
- sizeofcmds: 所有load command的大小
- flags: Mach-O文件的标志位。主要作用是告诉系统该如何加载这个Mach-O文件以及该文件的一些特性。有很多值,我们取常见的几种:
#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 /* 加载程序在随机的地址空间,只在 MH_EXECUTE中使用 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
MH_PIE 随机地址空间
每次系统加载进程后,都会为其随机分配一个虚拟内存空间。
在传统系统中,进程每次加载的虚拟内存是相同的。这就让黑客有可能篡改内存来破解软件。
dyld
dyld是苹果公司的动态链接库,用来把Mach-O文件加载入内存。
二级命名空间
表示其符号空间中还会包含所在库的信息。这样可以使得不同的库导出通用的符号。与其相对的是扁平命名空间。
Load commands
load commands 紧跟在header之后,用来告诉内核和dyld,如何将各个Segment加载入内存中。
load command被源码表示为struct,有若干种load command,但是共同的特点是,在其结构的开头处,必须是如下两个属性:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
苹果为cmd定义了若干的宏,用来表示cmd的类型,下面列举出几种:
// 描述该如何将32或64位的segment 加载入内存,对应segment command类型
#define LC_SEGMENT 0x1
#define LC_SEGMENT_64 0x19
// UUID, 2进制文件的唯一标识符
#define LC_UUID 0x1b
// 启动动态加载器dyld
#define LC_LOAD_DYLINKER 0xe
Segment load command
在这么多的load command中,需要我们重点关注的是segment load command。segment command解释了该如何将Data中的各个Segment加载入内存中,而和我们APP相关的逻辑及数据,则大部分位于各个Segment中。
而和我们的Run time相关的Segment,则位于__DATA类型Segment下。
Segment load command分为32位和64位:
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
32位和64位的Segment load command基本类似,只不过在64位的结构中,把和寻址相关的数据类型,由32位的uint32_t改为了64位的uint64_t类型。
结构体的定义,看注释基本能够看懂,就是maxprot, initprot不太明白啥意思。
这里介绍一个特殊的‘Segment’,叫做__PAGEZERO Segment。 这里说它特殊,是因为这个Segment其实是苹果虚拟出来的,只是一个逻辑上的段,而在Data中,根本没有对应的内容,也没有占用任何硬盘空间。
__PAGEZERO Segment在VM中被置为Read only,逻辑上占用APP最开始的4GB空间,用来处理空指针。
我们用MachOV点开__PAGEZERO Segment所对应的Segment load command,LC_SEGMENT_64(__PAGEZERO):
可以看到其vm size是4GB,但其真正的物理地址File size和offset都是0。
Section header
在Data中,程序的逻辑和数据是按照Segment(段)存储,在Segment中,又分为0或多个section,每个section中在存储实际的内容。而之所以这么做的原因在于,在section中,可以不用内存对齐达到节约内存的作用,而所有的section作为整体的Segment,又可以整体的内存对齐。
在Mach-O文件中,每一个Segment load command下面,都会包含对应Segment 下所有section的header。
section header的定义如下:
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
这样,关于load commonds部分,其真正的结构其实和苹果提供的图片有些许的差异:
Data
Mach-O的Data部分,其实是真正存储APP 二进制数据的地方,前面的header和load command,仅是提供文件的说明以及加载信息的功能。
Data部分也被分为若干的部分,除了我们前面提到的Segment外,还包括符号表,代码签名,动态加载器信息等。
而程序的逻辑和数据,则是放在以Segment分割的Data部分中的。我们在这里,仅关心Data中的Segment的部分。
Segment根据内容的不同,分为若干类型,类型名称均是以“双下划线+大写英文”表示,有的Segment下面还会包含若干的section,section的命名是以”双下划线+小写英文”表示。
先来看Segment,Mach-O中有如下几种Segment:
#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" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */
这里面注意到到SEG_OBJC,是和OC的runtime相关的。但是根据这篇文章
中说所,在OC 2.0中已经废弃掉__OBJC段,而是将其放入到了__DATA段中以__objc开头的section中。这些和runtime相关的sections是本文的终点,我们稍后再分析。我们先看看其他的段。
__TEXT段
__TEXT是程序的只读段,用于保存我们所写的代码和字符串常量,const修饰常量等。
下面是__TEXT段下常见的section:
Section | 用途 |
---|---|
__TEXT.__text | 主程序代码 |
__TEXT.__cstring | C 语言字符串 |
__TEXT.__const | const 关键字修饰的常量 |
__TEXT.__stubs | 用于 Stub 的占位代码,很多地方称之为桩代码。 |
__TEXT.__stubs_helper | 当 Stub 无法找到真正的符号地址后的最终指向 |
__TEXT.__objc_methname | Objective-C 方法名称 |
__TEXT.__objc_methtype | Objective-C 方法类型 |
__TEXT.__objc_classname | Objective-C 类名称 |
例如,我们点击__TEXT.__objc_classname, 会看到我们程序中所使用到的类的名称:
而在__TEXT.__cstring section中,则看到我们定义的字符串常量(如@”I’m a cat!! miao miao”):
值得注意的是,这些都是以明文形式展现的。如果我们将加密key用字符串常量或宏定义的形式存储在程序中,可以想象其安全性是得不到保障的。
__DATA段
__DATA段用于存储程序中所定义的数据,可读写。__DATA段下常见的sectin有:
Section | 用途 |
---|---|
__DATA.__data | 初始化过的可变数据 |
__DATA.__la_symbol_ptr | lazy binding 的指针表,表中的指针一开始都指向 __stub_helper |
__DATA.nl_symbol_ptr | 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 |
__DATA.__const | 没有初始化过的常量 |
__DATA.__cfstring | 程序中使用的 Core Foundation 字符串(CFStringRefs) |
__DATA.__bss | BSS,存放为初始化的全局变量,即常说的静态内存分配 |
__DATA.__common | 没有初始化过的符号声明 |
__DATA.__objc_classlist | Objective-C 类列表 |
__DATA.__objc_protolist | Objective-C 原型 |
__DATA.__objc_imginfo | Objective-C 镜像信息 |
__DATA.__objc_selfrefs | Objective-C self 引用 |
__DATA.__objc_protorefs | Objective-C 原型引用 |
__DATA.__objc_superrefs | Objective-C 超类引用 |
我们将在后续的文章中,继续探讨这些section和runtime的关系。
总结
这次我们一起了解了XNU内核下的二进制文件格式Mach-O。它由header,load command以及data三部分组成:
我们重点应该了解的应该是data部分,因为这里存储着我们程序真正的数据和代码。
在data部分中,又区分为以Segment划分的部分以及代码签名等其他部分。
在Segment下,有区分有若干的section。
常用的Segment有__PAGE_ZERO, __TEXT, __DATA(注意区分Mach-O的data和这里的__DATA段名称)。
参考资料
趣探 Mach-O:文件格式分析
深入理解Macho文件(二)- 消失的__OBJC段与新生的__DATA段
mach-o格式分析
Mach-O 文件格式探索
Mach-O 维基百科