前言
关于 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(可执行文件)被称为:通用二进制文件,即多种架构都可读取运行。
- 通用二进制文件具有以下特性: 1、Apple 提出的一种程序代码,能够同时适配多种架构的二进制文件。 2、同一个程序包中,同时为多种架构提供最理想的性能。 3、通用二进制应用程序通常比单一平台二进制程序大,因为需要存储多种代码。 4、由于多种架构之间有共通的非执行资源,所以并不会比单一架构的两倍大。 5、程序在执行的时候只调用一部分代码,运行起来不需要额外的内存。
- 那么多种架构是什么意思呢?下面我们通过file指令来看一下我们的可执行文件:
通过上图,我们可以看到 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以下的模拟器) |
- Tips: 在 Xcode 中设置 Arhitectures,Debug 属性设置为 NO 的时候,会编译支持所有架构的版本,编译的速度会变慢,设置为 yes 的时候,只编译当前的 Arhitectures 版本,编译速度快。
通用二进制文件的拆分 与 组合
- 在 MachOView 中,通用二进制文件也被叫做 Fat binary。 这种二进制文件是可以拆分、或者重新组合的 ⚠️ 注意这里我采用的是真机测试,Scheme 对应的 Build Configuration 选用 Release模式。(关于 Xcode 环境的配置,有不清楚的同学可以看这里:Xcode 多环境的配置) ⚠️ 这里还有一点要注意:测试的时候,如果工程只包含一种架构,此时要手动添加其他架构。
- 我们可以通过 file 指令,也可以通过 lipo -info 指令查看二进制文件支持的架构:
可以看到,目前 test 可执行程序支持 arm64 和 arm_v7 两种架构。 那么下面我们先进行文件拆分:
拆分 Fat binary
代码语言:javascript复制
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
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
Data段又分为:__TEXT段 和 __DATA段
- __TEXT段 代码的读取是从__TEXT段开始读取的,其中不同的__TEXT代表的意思如下:
注释
__DATA
段__DATA
段在内存中紧跟在__TEXT
段之后
注释
代码语言:javascript复制OC 引用的父类列表
⚠️ 这里有一点大家需要注意,系统库的方法在我们自己的Mach-O
文件里面是找不到的,它存放在共享缓存区。那么我们自己的Mach-O
文件又怎么去调用这些系统方法实现呢?