主要内容:
- 理解可执行文件
- 理解
Mach-O
文件 -
Mach-O
文件结构 Mach Header
Load Commands
Data
- 理解大小端模式
- 理解通用二进制文件
一、理解可执行文件
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
,效果如下:
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演示
五、分析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 */
};