主要内容:
- 理解可执行文件
- 理解
Mach-O文件 -
Mach-O文件结构 Mach HeaderLoad CommandsData- 理解大小端模式
- 理解通用二进制文件
一、理解可执行文件
1.可执行文件
-
进程,其实就是可执行文件在内存中加载得到的结果; -
可执行文件必须是操作系统可理解的格式,而且不同系统的可执行文件的格式也是不同的;
2.不同平台的可执行文件
-
Linux:ELF文件 -
Windows:PE32/PE32+文件 -
OS和iOS:Mach-O(Mach Object)文件
二、理解Mach-O文件
作为iOS,iPadOS、macOS平台的可执行文件格式,Mach-O文件涉及App启动运行、bitcode分析、 crash符号化等诸多多个功能:
1. Mach-O文件
-
Mach-O文件是iOS,iPadOS、macOS平台的可执行文件格式。对应系统通过应用二进制接口(application binary interface,缩写为ABI)来运行该格式的文件; -
Mach-O格式用来替代BSD系统中的a.out格式,保存了在编译和链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供单一文件格式。 -
Mach-O提供了更强的扩展性,以及更快的符号表信息访问速度;
2.Mach-O格式的常见文件类型
-
Executable:可执行文件(.out.o); -
Dylib:动态链接库; -
Bundle:不能被链接,只能在运行时使用dlopen()加载; -
Image:包含Executable、Dylib和Bundle; -
Framework:包含Dylib、资源文件和头文件的文件夹;
三、Mach-O文件结构
1.查看Mach-O的两种方法
- 使用
MachOView软件,可直接查看MachO文件的结构; - 使用终端命令
objdump;
2.查看Mach-O文件结构
使用MachOView查看Mach-O,效果如下:

image
Mach-O文件中包含三个主要的部分:
-
Header:头部,描述CPU类型、文件类型、加载命令的条数大小等信息; -
Load Commands:加载命令,其条数和大小已经在header中被提供; -
Data:数据段;
其他的信息还有:
-
Dynamic Loader Info:动态库加载信息 -
Function Starts:入口函数 -
Symbol Table:符号表 -
Dynamic Symbol Table: 动态库符号表 -
String Table:字符串表
四、Mach Header(可执行文件头)
1.功能总结
-
Header是链接器加载时最先读取的内容,因为它决定了一些基础架构、系统类型等信息; -
Header包含整个Mach-O文件的关键信息,如CPU类型、文件类型、加载命令的条数大小等信息,使得系统能够迅速定位Mach-O文件的运行环境; -
Header针对32位和64位架构的CPU,分别对应mach_header和mach_header_64的结构体;
2.源码分析
Header被定义在loader.h文件中,具体代码如下:
struct mach_header_64 {
uint32_t magic; // 32位或者64位,系统内核用来判断是否是mach-o格式
cpu_type_t cputype; // CPU架构类型,比如ARM
cpu_subtype_t cpusubtype; // CPU的具体类型,例如arm64、armv7
uint32_t filetype; // mach-o文件类型, 可执行文件、目标文件或者静态库和动态库
uint32_t ncmds; // LoadCommands加载命令的条数(加载命令紧跟header之后)
uint32_t sizeofcmds; // 全部LoadCommands加载命令的大小
uint32_t flags; // 标志位标识二进制文件支持的功能,主要是和系统加载、链接有关
uint32_t reserved; // 保留字段(相比于32位多出的字段)
};
由于可执行文件、目标文件或者静态库和动态库等都是Mach-O格式,所以才需要filetype来说明。常用的文件类型有以下几种:
#define MH_OBJECT 0x1 /* 目标文件*/
#define MH_EXECUTE 0x2 /* 可执行文件*/
#define MH_DYLIB 0x6 /* 动态库*/
#define MH_DYLINKER 0x7 /* 动态链接器*/
#define MH_DSYM 0xa /* 存储二进制文件符号信息,用于debug分析*/
3.MachOView演示
image
五、分析Load Commands
1.功能总结
-
Load Commands是加载命令的列表,用于描述Data在二进制文件和虚拟内存中的布局信息; -
Load Commands记录了很多信息,例如动态链接器的位置、程序的入口、依赖库的信息、代码的位置、符号表的位置等; -
Load commands由内核定义,不同版本的command数量不同,其条数和大小记录在header中; -
Load commands的type是以LC_为前缀常量,譬如LC_SEGMENT、LC_SYMTAB等;
2..代码分析
Load Command被定义在loader.h文件中,具体代码如下:
struct load_command {
uint32_t cmd; /* 加载命令的类型 */
uint32_t cmdsize; /* 加载命令的大小 */
};
每个Load Command都有独立的结构,但是所有结构的前两个字段是固定的。比如LC_SEGMENT_64,这是一个读取segment、section有关命令,具体代码如下:
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; // 表示加载命令类型
uint32_t cmdsize; // 表示加载命令大小(还包括了紧跟其后的nsects个section的大小)
char segname[16]; // 16个字节的段名字
uint64_t vmaddr; // 段的虚拟内存起始地址
uint64_t vmsize; // 段的虚拟内存大小
uint64_t fileoff; // 段在文件中的偏移量
uint64_t filesize; // 段在文件中的大小
vm_prot_t maxprot; // 段页面所需要的最高内存保护(4 = r,2 = w,1 = x)
vm_prot_t initprot; // 段页面初始的内存保护
uint32_t nsects; // 段中section数量
uint32_t flags; // 标志位
};
六、Data
1.功能总结
-
Data中存储了实际的数据与代码,主要包含方法、符号表、动态符号表、动态库加载信息(重定向、符号绑定等)等; -
Data中的排布完全按照Load Command中的描述; -
Data由Segment(段)和Section(节)的方式来组成,通常,Data拥有多个segment,每个segment可以有零到多个section节; - 不同的
segment都有一段虚拟地址映射到进程的地址空间;
几乎所有的Mach-O文件都包含3个segment
-
__TEXT:代码段,只读可执行,存储
函数的二进制代码(__text),常量字符串(__cstring),OC的类/方法名等信息 -
__DATA:数据段, 可读可写,存储
OC的字符串(__cfstring),以及运行时的元数据:class/protocol/method,以及全局变量,静态变量等; -
__LINKEDIT:只读,存储启动
App需要的信息,如bind & rebase 的地址、函数的名称和地址等信息;
2.源码分析
在Data区中,Section占了很大的比例,而且在Mach-O中集中体现在__TEXT和__DATA两段里。
Section被定义在loader.h文件中,具体代码如下:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; // 当前section的名称
char segname[16]; // section所在的segment名称
uint64_t addr; // 内存中起始位置
uint64_t size; // section大小
uint32_t offset; // section的文件偏移
uint32_t align; // 字节大小对齐
uint32_t reloff; // 重定位入口的文件偏移
uint32_t nreloc; // 重定位入口数量
uint32_t flags; // 标志,section的类型和属性
uint32_t reserved1; // 保留(用于偏移量或索引)
uint32_t reserved2; // 保留(用于count或sizeof)
uint32_t reserved3; // 保留
};
七、理解大小端模式
分析Mach-O文件时,经常会看到内存地址相关的内容,这里就涉及到了大小端模式的概念;
- 小端模式:数据的低字节,保存在内存的低地址;
- 大端模式:数据的低字节,保存在内存的高地址;
iOS设备的处理器是基于ARM架构的,默认是采用小端模式(低字节放低位)读取数据的,而网络和蓝牙传输数据通常是用的大端模式(低字节放高位):
下面以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value
Little-Endian: 低地址存放低位,如下:
低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
Big-Endian: 低地址存放高位,如下:
低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
| 内存地址 | 小端模式存放内容 | 大端模式存放内容 |
|---|---|---|
| 0x4000 | 0x78 | 0x12 |
| 0x4001 | 0x56 | 0x34 |
| 0x4002 | 0x34 | 0x56 |
| 0x4003 | 0x12 | 0x78 |
八、理解通用二进制文件
1.基本概念
- 通用二进制文件的存储结构,是将多种架构的
Mach-O文件打包在一起,CPU在读取该二进制文件时可以自动检测并选用合适的架构; - 通用二进制文件会同时存储多种架构,所以比单一架构的二进制文件大很多,会占用大量的磁盘空间。但由于系统运行时会自动选择最合适的,不相关的架构代码,不会占用内存空间,所以执行效率提高了;
- 通用二进制格式也被称为胖二进制格式;
2.通用二进制格式分析
通用二进制格式的定义在<mach-o/fat.h>中:
- 下载xnu后,依次在
xnu -> EXTERNAL_HEADERS ->mach-o中找到该文件。 - 通用二进制文件有两个重要结构体:
fat_header、fat_arch;
两个结构体的定义如下:
/*
- magic:可以让系统内核读取该文件时知道是通用二进制文件
- nfat_arch:表明下面有多个fat_arch结构体,即通用二进制文件包含多少个Mach-O
*/
struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
};
/*
fat_arch是描述Mach-O
- cputype 和 cpusubtype:说明Mach-O适用的平台
- offset(偏移)、size(大小)、align(页对齐)描述了Mach-O二进制位于通用二进制文件的位置
*/
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};