1. Mach-O定义
Mach-O(Mach Object File Format)是macOS上的可执行文件格式,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。
2. Mach-O 文件格式
根据官网的描述,Mach-O文件的结构如下图:
主要分为三个部分:
Header
:记录了Mach-O文件的基本信息,包括CPU架构、文件类和Load Commands等信息。Load Commands
:描述了怎样加载每个 Segment 的信息,Data
:Data 中每一个Segment的数据都保存在此,每个Segment拥有一个或多个 Section ,用来存放数据和代码
这里我们借助MachOView来观察文件结构,先写一段简单的cpp代码:
#import <stdio.h>
int main() {
printf("Mach-O Test");
return 0;
}
使用 clang -g main.cpp -o main
生成执行文件,随后通过MachOView观察:
根据<mach-o/loader.h>中的源码,我们可以一起看看这三个部分的结构体
2.1 Header
Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且 ncmds 和 sizeofcmds 来能个字段将会用在加载命令的过程中。
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
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 */
}
magic
:魔术,根据宏定义,标识当前Mach-O位32位(0xfeedface)/ 64位 (0xfeedfacf)
#define MH_MAGIC 0xfeedface /* the mach magic number */`
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
cputype
/ cpusubtype:CPU类型/CPU子类型filetype
:文件类型,常用的如下
* Constants for the filetype field of the mach_header
*/
#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 Commands数量sizeofcmds
:Load Commands的总大小flag
:标识位,记录文件的详细信息
#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 攻击 */
reserved
:64位文件特有的保留字段
对于刚才生成的可执行文件,其Header信息如下:
2.2. Load Commands
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
一起来看看Load Commands在MachOView中的构成,以LC_SEGMENT_64这个cmd为例(cmd类型为:LC_SEGMENT_64,size为72):
2.3 Data
从整体架构图中可以看到,Data又分为Segment
和Section
两个部分
2.3.1 Segment
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 */
};
cmd
:Load Commands部分中提到的cmd类型cmdsize
:同上segname[16]
:段名称vmaddr
:段虚拟地址(未偏移),真实虚拟地址要加上ASLR的偏移量vmsize
:段的虚拟地址大小fileoff
:段在文件内的地址偏移filesize
:段在文件内的大小
加载segment的过程,就是从文件偏移fileoff
处,将大小为filesize
的段,加载到虚拟机vmaddr
处。nsects
:段内section数量flags
:标志位,用于描述详细信息
大家看到后面几个地址和偏移肯定会头晕,其实这几个变量主要作用在加载segment的时候。
加载segment的过程,就是从文件偏移 fileoff
处,将大小为 filesize
的段,加载到虚拟机 vmaddr
处。
而segment[16]
其实我们刚才在MachOView中就有见到:
可以看到,LC_SEGMENT_64中包含了五种类型,分别是: __PAGEZERO, __TEXT, __DATA, __DATA_CONST, __LINKEDIT:
PAGEZERO
:可执行文件捕获空指针的段TEXT
:代码段和只读数据DATA_CONST
:常态变量DATA
:全局变量和静态变量LINKEDIT
:包含动态链接器所需的符号、字符串表等数据
而对于__TEXT和__DATA这两个Segment,则可以继续分解为Section,从而形成Segment->Section的结构。之所以要这样设计,是因为在同一个Segment下的Section可以拥有相同的控制权限,并且可以不完全按照Page的大小进行内存对齐,从而达到节约内存的效果。
2.3.2 Section
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 */
};
sectname
:section名称segname
:所属的segment名称addr
:section在内存中的地址size
:section大小offset
:section在文件中的偏移align
:内存对齐边界reloff
:重定位入口在文件中的偏移nreloc
:重定位入口数量
以LC_SEGMENT_64为例,其中的Section64 Header(__text)
,大写的 __TEXT
代表 segment
,小写的 __text
代表 section
,其中的不同的Section代表着不同的含义,列举一下常见的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 类名称 |
__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 超类引用 |
3. Mach-O实验
3.1 验证__TEXT.__text的加载
上文提到,__TEXT.__text
的含义是主程序代码,更值得一提的是这个section的加载过程可以观察得到。
首先通过MachOView来看Load Commonds中__TEXT.__text的数据,为什么要先看Load Comands呢,因为Load Commands记录了Data是如何加载的,即作为Data加载结果的预期值,所以当结果=预期时我们就达到了验证效果。
从图中可以看到,此时__TEXT.__text的address(section在内存中的地址)为0000000100003F60,这是预期值。
随即通过vtool
命令来查看汇编之后的代码起始地址:
otool -vt MachOTest
可见加载同样起始于0000000100003F60,验证完毕。
3.2 探索__DATA.__la_symbol_ptr和__TEXT.__stubs的关系
在上文中,我们提到了常见section的用途。其中
__DATA.__la_symbol_ptr
的用途为: lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
。因为这些用途都是从资料上搜集的,有些不知道其具体含义。但是对于这条尤其困惑,借此例子理解一下。
首先,我们从MachOView上点开__TEXT.__stubs,选择其中一个stub,取其Data。由于我们demo比较简单,只存在一个stub,所以选择这个就OK了。
接下来在Hopper Disassembler
中打开这个之前demo的Mach-O文件,在其中搜索刚才的地址FF2570400000,得到:
找到了对应代码的汇编表示,我们双击进入:
可以看出,这个stub的含义就是跳转到以__la_symbol_ptr
对应表项数据所指向地址的代码,我们再取地址100008000回到MachOView中看一下:
得到这个地址中的Data为0000000100003FA0,在通过Hopper查看这个Data:
果然落在了__stub_helper这个section!
回想一下我们刚才走过的链路
- 从
__TEXT.__stubs
中取出一个stub,取其Data(FF2570400000),在Hopper中打开 - 通过Hopper中找到Data并继续进入后,发现最终指向
__la_symbol_ptr
的某一项 - 取这一项的地址(100008000),在MachOView中找到对应的Data(0000000100003FA0)
- 发现这个Data最终落在
__TEXT.__stubs_helper
也就是说, __DATA.__la_symbol_ptr
里面的所有表项的数据在开始时都会被 binding 成 __stub_helper
。而一旦被首次调用,找到地址后,就会将 __DATA.__la_symbol_ptr
内的占位符binding为真实的地址,便可以直接执行函数,后续无需再走binding的流程。这就如其名一样完成了lazy binding的过程。