前言:
关于Mach-O
文件,在iOS App 加载流程知识中已经提到过。看到优秀简书进行一波儿转载。本文转载https://www.jianshu.com/p/d641e8270108
整体结构大致如下:
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
linpo mach-o文件名 -thin 要拆分的架构名 -output 拆分出来的文件名
拆分前的ipa
包内容:
拆分后的ipa
包内容:
⚠️ 拆分后源文件并不会发生改变,类似于从源文件中copy
出来一个架构单一的二进制文件,注意这里不是单独的分离架构。
合并 Fat binary
lipo -create macho_arm64 macho_armv7 -output newTest
合并之后的文件与原文件并无差异,我们可以通过哈希值也看一下:
Mach-O 文件结构
-
Mach-O
文件主要由 3 部分组成:
名字 | 注释 |
---|---|
Mach Header (Mach-O 头) |
描述了Mach-O 的CPU 架构、文件类型以及加载命令等信息 |
Load Commands (加载命令) |
描述了文件中数据的具体组织结构,不同的数据类型用不同的加载命令表示 |
Data (数据区) |
Data 中每一个段 (Segment ) 的数据都保存在这里,都拥有一个或多个Section ,用来存储数据和代码 |
⚠️ 既然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 |
获取加密信息 |
LC_LOAD_DYLIB |
加载额外的动态库 |
LC_FUNCTION_STARTS |
函数起始地址表 |
LC_DATA_IN_CODE |
定义在代码段(__text )内的非指令表 |
LC_CODE_SIGNATURE |
应用的签名信息 |
Data
Data
段又分为:__TEXT
段 和 __DATA
段
-
__TEXT
段
代码的读取是从__TEXT
段开始读取的,其中不同的__TEXT
代表的意思如下:
名字 | 注释 |
---|---|
__text |
主程序代码 |
__cstring |
C 语言字符串 |
__const |
const 关键字修饰的常量 |
__stubs |
用于Stub 的占位代码,很多地方称之为桩代码 |
__stubs_helper |
当Stub 无法找到真正的符号地址后的最终指向 |
__objc_methname |
OC 方法名 |
__objc_methtype |
OC 方法类型 |
__objc_classname |
OC 类名 |
-
__DATA
段
__DATA
段在内存中紧跟在__TEXT
段之后
名字 | 注释 |
---|---|
__got |
全局偏移表 |
__la_symbol_ptr |
lazy binding 的指针表,表中的指针一开始都指向__stub_helper
|
__cfstring |
工程中使用的Core Foundation 字符串(CFStringRefs ) |
__objc_classlist |
OC 类列表 |
__objc_protolist |
OC protocol 列表 |
__objc_imginfo |
OC 镜像信息 |
__const |
没有初始化过的常量 |
__objc_selfrefs |
OC 引用的SEL 列表 |
__objc_protorefs |
OC 引用的protocol 列表 |
__objc_superrefs |
OC 引用的父类列表 |
__objc_ivar |
OC ivar 信息 |
__objc_data |
class 信息 |
__bss |
BSS,存放 未初始化的全局变量,就是常说的静态内存分配 |
__data |
初始化的可变数据 |
⚠️ 这里有一点大家需要注意,系统库的方法在我们自己的Mach-O
文件里面是找不到的,它存放在共享缓存区。那么我们自己的Mach-O
文件又怎么去调用这些系统方法实现呢?