之前在项目中使用 fishhook 来替换系统的 C 函数,其中涉及到很多和 iOS 系统相关的编译、链接等方面的知识,由于内容比较多,所以打算分几篇文章来进行讲解,本文主要是分析 Mach—O 文件。
在 macOS 以及 iOS 系统中,可执行文件的格式为 Mach-O,理解 Mach-O 文件格式对于我们探究操作系统的运作机制起着关键的作用。Mach-O 文件格式如下图所示,它由 Header、Load commands 以及 Data 三部分组成:
Header:记录 Mach-O 文件的基本信息,包括文件类型、支持的 CPU 类型以及加载命令的个数、大小等。
Load commands: 位于 Header 之后,向操作系统描述如何解析文件。
Data: 用于保存程序的 TEXT、DATA、LINKEDIT 等 segment 数据。
以下是人民群众喜闻乐见的一段代码:
#include <stdio.h>
int main(int argc, const char * argv[]) {
printf("Hello, World!\n");
return 0;
}
使用 clang main.c -o MachOExplore
命令编译上述代码,我们得到名为 MachOExplore 的目标文件,也就是本文将要探究的 Mach-O 文件。otool 是用来查看 Mach-O 文件的常用工具,但是本文会使用另一种工具 MachOVie�w 来完成任务。
Header
MachOView 查看 Header 的内容如下:
Header 中记录了 Mach-O 文件的属性信息,相关数据结构定义在 loader.h ����中,分为32位和64位两种:
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
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 */
};
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
/*
* 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 */
};
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
下表描述了各个字段的含义,mach_header_64 除了比 mach_header 多出一个 reserved 字段外,其他方面并无区别。
字段 | 说明 |
---|---|
magic | 表明文件适用于64位操作系统还是32位操作系统,因为大小端的存在,所以有 MH_MAGIC_64 和 MH_CIGAM_64 两种形式 。 |
cputype | 描述文件所支持的 CPU 架构,包括 ARM64、X86_64、i386等。 |
cpusubtype | 描述文件对应的具体 CPU 架构,例如 ARM_V7S、ARM64_V8、X86_ARCH1 等。 |
filetype | 描述文件的类型,常见的类型有可执行文件、可重定位文件、共享库文件等。 |
ncmds | 记录加载命令的个数。 |
sizeofcmds | 记录所有加载命令的大小。 |
flags | 描述文件在编译、链接等过程中的信息,示例中的 MH_NOUNDEFS 表示文件中不存在未定义的符号,MH_DYLDLINK 表示文件要交由 DYLD 进一步处理,MH_TWOLEVEL 表示文件使用两级命名空间,MH_PIE 表示启用地址空间布局随机化。 |
Load commands
Load commands 紧随在 Header 后,它包含了一系列的加载命令,目的是向操作系统描述如何处理 Mach-O 文件。示例中包含的加载命令如下:
LC_SEGMENT_64
命令表示将相应的 segment 映射到虚拟地址空间中,以 LC_SEGMENT_64(__PAGEZERO)
为例:
Command: 表示加载命令;
Command Size: 表示加载命令的大小;
Segment Name: 被加载的段的名字;
VM Address: 段所在的虚拟空间地址;
VM Size: 段所占用的虚拟空间的大小;
File Offset: 段在文件中的偏移量;
File Size: 段在文件中的大小;
Maximum VM protection: 表示与段相对应的最大操作权限;
Initial VM protection: 表示段的初始操作权限;
Number of Sections: 段包含多少个 Section;
Flags: 描述与段相关的加载信息,具体解释请参考 loader.h 文件;
所以上述命令是将 __PAGEZERO
段映射到虚拟地址 0x0 处,占用虚拟空间大小为 4GB,但是这4GB并不是真实的文件大小,它仅表明将虚拟地址空间的前4GB映射为不可读、不可写、不可执行,与 NULL 指针相对应。如果程序试图访问 __PAGEZERO
段,那么将会引起系统的崩溃。
接下来我们来看 LC_SEGMENT_64(__TEXT)
:
它将 __TEXT
段映射到虚拟地址空间 0x100000000 处,也就是紧随 __PAGEZERO
段,占用虚拟空间大小为 4096B,所对应的权限是可读、可执行、不可写入。我们的 __TEXT
段包含以下5个 Section:
__text
: 包含程序的机器码;__stubs
和__stub_helper
: 用来帮助 DYLD 绑定符号;__cstring
: 记录了文件中的常量字符串信息(包含在双引号中),我们可以依据此信息找到字符串的地址;__unwind_info
: 用于确定异常发生时栈所对应的信息,包括栈指针、返回地址、寄存器信息等,它同样包含相应的处理函数来支持像 catch、final 等特性;
LC_SEGMENT_64(__DATA)
的作用是将 __DATA
段映射到紧随 __TEXT
段的虚拟地址空间上,它包含两个 Section:
__nl_symbol_ptr Section
包含的符号指针需要在加载时绑定,而 __la_symbol_ptr Section
包含的符号指针则是在其第一次被程序使用时绑定。
LC_SEGMENT_64(__LINKEDIT)
则是将与动态链接相关的信息映射到虚拟地址空间,__LINKEDIT
段包括 rebase、bind、lazy bind 等信息。
LC_DYLD_INFO_ONLY
记录了有关链接的重要信息,它的数据结构如下:
struct dyld_info_command {
uint32_t cmd; /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
uint32_t cmdsize; /* sizeof(struct dyld_info_command) */
uint32_t rebase_off; /* file offset to rebase info */
uint32_t rebase_size; /* size of rebase info */
uint32_t bind_off; /* file offset to binding info */
uint32_t bind_size; /* size of binding info */
uint32_t weak_bind_off; /* file offset to weak binding info */
uint32_t weak_bind_size; /* size of weak binding info */
uint32_t lazy_bind_off; /* file offset to lazy binding info */
uint32_t lazy_bind_size; /* size of lazy binding infs */
uint32_t export_off; /* file offset to lazy binding info */
uint32_t export_size; /* size of lazy binding infs */
};
根据它所记录的偏移量,我们便可以找到在 Dynamic Loader Info 中的相关信息。它的 ONLY 后缀表明这是程序运行所必须的,如果链接器不支持,那么加载过程就会终止。
LC_SYMTAB
记录了程序的符号表以及字符串表的偏移量及大小,符号表中记录了程序用到的函数以及全局变量的信息,符号表条目的数据结构定义在 nlist.h 中:
/*
* Format of a symbol table entry of a Mach-O file for 32-bit architectures.
*/
struct nlist {
union {
#ifndef __LP64__
char *n_name; /* for use when in-core */
#endif
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
int16_t n_desc; /* see <mach-o/stab.h> */
uint32_t n_value; /* value of this symbol (or stab offset) */
};
/*
* This is the symbol table entry structure for 64-bit architectures.
*/
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
数据结构中相关字段的含义都可以在 nlist.h 中找到,这里值得一说的是 n_un 字段,它用来记录符号的名字,但为什么是 uint32_t 类型呢?又为什么在注释中标明是 string table 的 索引呢?
这是因为在程序中,字符串的长度是不固定的,所以会将其放在 string table 中,然后存储它在 string table 中的偏移。如果其他部分想要引用某个字符串,那么他首先需要找到 string table 的起始地址,然后根据偏移量找到相应字符串的起始位置并向后读取字符,直到遇见 \0
才会停止读取过程,最后返回读到的字符串。
这也是 LC_SYMTAB
额外记录 string table 地址的原因,string table 通常用于记录 section 名、符号名等信息。
其他的加载命令如下表所示:
加载命令 | 描述 |
---|---|
LC_DYSYMTAB | 包括动态链接过程中所需要的信息 |
LC_LOAD_DYLINKER | 指定动态链接器的地址 |
LC_LOAD_DYLIB | 记录了程序所需要的动态库的相关信息 |
LC_UUID | 静态链接器为其生成的文件所提供的唯一标识符 |
LC_MAIN | 指定 main 函数的地址 |
LC_FUNCTION_STARTS | 记录文件中每个函数的起始地址 |
LC_DATA_IN_CODE | 记录那些写在程序二进制执行指令中的数据 |
LC_CODE_SIGNATURE | 代码签名 |
Data
Data 包括文件所需的 segment 数据,除了 MachOView 工具,你也可以通过 size 工具来查看,运行 size -x -l -m MachOExplore
命令后可得到以下内容:
我们还可以通过输入 otool -s __DATA __la_symbol_ptr MachOExplore
命令来查看 __la_symbol_ptr
Section 的数据:
也可以使用 otool -V -s __TEXT __text MachOExplore
命令来查看 __text
Section 的反汇编代码:
同时 otool 也为一些常见的命令设置了缩写,例如 -t
就是 -s __TEXT __text
简称,而 -d
就是 -s __DATA __data
的简称,相关信息都可以通过 man otool
命令来查看。这些数据都可以在 MachOView 中看得很清楚,但是这些命令行工具记一下也无妨。
总结
以上便是 Mach-O 文件的探究(其实总结部分就是强行凑的(๑•̀ㅂ•́)و✧)。