前言
关于 Mach-O 文件,在iOS App 加载流程知识中已经提到过。
整体结构大致如下:
Mach-O 定义
Mach-O(Mach Object)是 macOS、iOS、iPadOS 存储程序和库的文件格式。对应系统通过应用二进制接口(application binary interface,缩写为ABI) 来运行该格式的文件。
Mach-O 格式用来代替 BSD 系统的 a.out 格式。Mach-O 文件格式保存了在编译过程和链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供了单一文件格式。
Mach-O = 文件配置 + 二进制文件
除了可执行文件
之外,还有一些文件也是Mach-O
格式,比如:
.o
.a
.dylib
Framework
dyld
(动态链接器).dsym
(符号表)由此我们知道,可执行文件
只是Mach-O
的一种,因此我们将Mach-O
文件分为以下几种:
名称 | 注释 |
---|---|
Mach-O Object | 目标文件 |
Mach-O ececutable | 可执行文件 |
Mach-O dynamically | 动态库文件 |
Mach-O dynamic linker | 动态链接器文件 |
Mach-O DSYM companion | 符号表文件 |
通用二进制文件(Universal binary)
支持多架构的Mach-O ececutable(可执行文件)被称为:通用二进制文件,即多种架构都可读取运行。
通过上图,我们可以看到 test 可执行文件的类型是 Mach-O;架构是 x86_64,这是我们用模拟器运行的可执行文件。
我们再实际开发中遇到的设置 arm64 & armv7 这些都是对应的架构:
名称 | 注释 |
---|---|
arm64 | 真机64位处理器需要arm64架构(iphone6,iphone6p以上的真机) |
armv7s | 真机32位处理器 ( ipnone5,iphone5s真机/armv7s) |
armv7 | 真机32位处理器 (iphone4真机/armv7) |
x86_64 | 模拟器64位处理器 (iphone6以上的模拟器) |
i386 | 模拟器32位处理器 (iphone5,iphone5s以下的模拟器) |
可以看到,目前 test 可执行程序支持 arm64 和 arm_v7 两种架构。 那么下面我们先进行文件拆分:
linpo mach-o文件名 -thin 要拆分的架构名 -output 拆分出来的文件名
拆分前的ipa
包内容:
拆分后的ipa
包内容:
⚠️ 拆分后源文件并不会发生改变,类似于从源文件中copy
出来一个架构单一的二进制文件,注意这里不是单独的分离架构。
合并 Fat binary
lipo -create macho_arm64 macho_armv7 -output newTest
合并之后的文件与原文件并无差异,我们可以通过哈希值也看一下:
Mach-O 文件结构
Mach-O
文件主要由 3 部分组成注释
⚠️ 既然Mach-O
是二进制文件,那么它又是怎么知道哪一块内容是Load commands
,哪一块又是Header
的呢?
其实这里涉及到一个概念叫做结构体对齐,简单的讲就是:按照一定的规则组合到一起,再按照既定的规则拆分就可以了。
Mach Header
可以看到Mach Header
里面有很多的Description
(描述)那么对应的都是什么意思呢?
我们可以在工程中搜索一下,使用快捷键(command + shift + o)
搜索load.h
文件,打开该文件,由于是当前是64位的,所以找到:
/*
* 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 */
};
在load.h
文件中mach_header_64
比mach_header
(32位的头文件)多了一个保留字段
uint32_t reserved; /* reserved */
mach_header
是链接器加载的时候最先读取的内容,它决定了一些基础架构,系统类型,指令条数等信息。
Load Commands
详细保存着加载指令的内容,告诉链接器如何去加载当前的Mach-O文件。
那么每一条Load Command
对应的又是什么意思呢?
同样的我们也可以在load.h搜索的到,我们以LC_SEGMENT_64
为例:
下面我们列举一些常见的:
名字 | 注释 |
---|---|
LC_SEGMENT_64 | 将文件中的段映射到进程地址空间中 |
LC_DYLD_INFO_ONLY | 加载动态链接库信息(重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值信息) |
LC_SYMTAB | 载入符号表地址 |
LC_DYSYMTAB | 载入动态符号表地址 |
LC_LOAD_DYLINKER | 加载动态链接器 |
LC_UUID | 唯一标识,crash解析中也会用到,检查dysm文件和crash文件是否匹配 |
LC_VERSION_MIN_MACOSX / LC_VERSION_MIN_IPHONEOS | 二进制文件支持的最底操作系统版本 |
LC_SOURCE_VERSION | 构建二进制文件使用的源代码版本 |
LC_MAIN | 设置程序主线程的入口地址和栈大小(这也就是为什么我们的程序每次运行都是从main()进来的原因) |
LC_ENCRYPTION_INFO_64 | 获取加密信息 |
加载额外的动态库
Data段又分为:__TEXT段 和 __DATA段
注释
__DATA
段
__DATA
段在内存中紧跟在__TEXT
段之后注释
OC 引用的父类列表
⚠️ 这里有一点大家需要注意,系统库的方法在我们自己的Mach-O
文件里面是找不到的,它存放在共享缓存区。那么我们自己的Mach-O
文件又怎么去调用这些系统方法实现呢?