前言
本篇文章主要分析MachO文件
(也称作二进制可执行文件
),相信大家在平时开发中都会碰到MachO文件
这个概念,但是大部分人不清楚是个什么东西,本篇文章就和大家一起来具体分析它的由来
以及它的内部结构
。
一、Mach-O
首先大家看看上面这张图,从左到右可以看出👇
1.不论是哪种高级语言(C OC Swift
等),第一步都会生成AST语法树
,只是编译器前端
不同而已(有Clang、Swift或Rust
)
- 接着通过
CIL MIR 或SIL生成器
生成IR中间代码
,这个中间代码都属于LLVM IR
- 最后交给
MIR
生成机器代码
,这个机器代码就是MachO文件
整个过程其实都是LLVM
帮我们完成的,至于LLVM
是什么,大家可以参考我之前的文章 👉 LLVM编译流程。
常见的Mach-O文件格式
- 目标文件.o
- 库文件
- .a
- .dylib
- .framework
- 可执行文件
- dyld
- .dsym
验证
.o
、.out
、可执行文件
案例一
新建test.c文件,内容如下👇
#include <stdio.h>
int main() {
printf("test\n");
return 0;
}
验证.o
文件👇
⚠️注意:
不指定-c
默认生成.out
格式,如果报找不到'stdio.h' file not found
,则可以指定-isysroot
。
验证.out
可执行文件👇
验证可执行文件👇
再直接生成一个test3
可执行文件👇
那么问题来了 👉 生成的a.out、test2、test3
是一样的么?👇
可以看到生成的可执行文件md5相同
。
⚠️注意:原则上test3的md5应该和test2和a.out相同。源码没有变化,所以应该相同的。在指定
-isysroot
后生成的可能不同
,推测和CommandLineTools
有关(系统中有一个,Xcode中也有一个)。
案例二
再创建一个test1.c文件,内容如下👇
#include <stdio.h>
void test1Func() {
printf("test1 func \n");
}
修改test.c👇
#include <stdio.h>
void test1Func();
int main() {
test1Func();
printf("test\n");
return 0;
}
这个时候相当于多了个test1.c
文件了,编译生成可执行文件demo、demo1、demo2
👇
clang -o demo test1.c test.c
clang -c test1.c test.c
clang -o demo1 test.o test1.o
clang -o demo2 test1.o test.o
查看他们的MD5👇
这里demo1和demo2的md5不同
,是因为test.o和test1.o顺序不同
。
objdump命令查看Mach-O
objdump --macho -d demo
上图明显可见方法调用的顺序不同
,这也就解释了md5不同
的原因。这里很像Xcode中Build Phases -> Compile Sources
中源文件的顺序
。
⚠️注意:源文件顺序不同,编译出来的二进制文件不同( 大小相同),二进制排列顺序不同。
.a
文件、
直接创建一个library库查看👇
//find /usr -name "*.a"
file libTestLibrary.a
libTestLibrary.a: current ar archive random library
.dylib
文件
file /usr/lib/libprequelite.dylib
/usr/lib/libprequelite.dylib: Mach-O 64-bit dynamically linked shared library x86_64
⚠️注意:
dyld
不是
可执行文件,它是一个dynamic linker
,由系统内核
触发。
dyld
文件
cd /usr/lib
file dyld
dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
dyld (for architecture i386): Mach-O dynamic linker i386
.dsym
文件
file TestDsym.app.dSYM
TestDsym.app.dSYM: directory
cd TestDsym.app.dSYM/Contents/Resources/DWARF
file TestDsym
TestDsym: Mach-O 64-bit dSYM companion file arm64
二、工程配置
2.1 查看Mach-O文件的类型
我们可以在工程配置中查看Mach-O文件的类型,如下图👇
也可以使用命令行查看👇
file
your Mach-O文件路径
可以看到是支持2个架构的:arm64
和armv7
。当然也可以在Xcode中直观的看到支持的架构👇
同时Xcode中架构设置在Build Settings -> Architectures
中👇
主要有以下配置选项👇
-
Architectures
👉 支持的架构。 -
Build Active Architecture Only
👉 默认情况下debug模式下只编译当前设备架构,release模式下需要根据支持的设备。 -
$(ARCHS_STANDARD)
👉 环境变量,代表当前支持的架构。
如果我们要修改架构直接在Architectures中配置(增加armv7s)👇
2.2 通用二进制文件(Universal binary)
- 苹果公司提出的一种程序代码,能同时适用多种架构的二进制文件。
- 同一个程序包中同时为多种架构提供最理想的性能。
- 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大。
- 由于多种架构有共同的非执行资源(代码以外的),所以并不会达到单一版本的多倍之多(特殊情况下,只有少量代码文件的情况下有可能会大于多倍)。
- 由于执行中只调用一部分代码,运行起来不需要额外的内存。
当我们将通用二进制文件拖入Hopper时,能够看到让我们选择对应的架构👇
2.3 lipo命令
lipo
是管理Fat File
的工具,可以查看cpu架构,提取特定架构,整合和拆分库文件。
1. 查看MachO
文件支持的架构
lipo -info MachO文件
lipo -info EvergrandeCustomerApp_Example
Architectures in the fat file: EvergrandeCustomerApp_Example are: armv7 arm64
2. lifo –thin 拆分某种架构
lipo MachO文件 –thin 架构 –output 输出文件路径
3. 使用lipo -create合并多种架构
lipo -create MachO1 MachO2 -output 输出文件路径
三、MachO文件结构
macho
文件是mac os
或ios
系统可执行文件
的格式,系统通过加载
这个格式来执行代码
。相关结构如下图👇
上图中Mach-O
的组成结构如图所示包括👇
-
Header
包含该二进制文件的一般信息- 字节顺序、架构类型、加载指令的数量等
- 使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么
-
Load commands
一张包含很多内容的表- 内容包括区域的位置、符号表、动态符号表等
-
Data
通常是对象文件中最大的部分- 包含
Segement
的具体数据
- 包含
3.1 查看MachO文件的方式
有2种方式可查看MachO文件的结构👇
- 命令行
otool -f MachO文件
$ otool -f xxx.app/xxx
Fat headers
fat_magic 0xcafebabe
nfat_arch 2
architecture 0
cputype 12
cpusubtype 9
capabilities 0x0
offset 16384
size 69642576
align 2^14 (16384)
architecture 1
cputype 16777228
cpusubtype 0
capabilities 0x0
offset 69664768
size 80306624
align 2^14 (16384)
-
MachO View
可视化工具
3.2 MachO Header的结构
Fat Header
首先我们来看看Fat Header
,什么是Fat Header
👇
对于
多架构MachO
会有一个Fat Header
,其中包含了CPU类型和架构
。Offset
和Size
代表了每一个架构
在二进制文件
中的偏移
和大小
。
上图中,armv7
的偏移量
和大小
分别是16384
和 79315040
,再看arm64的偏移量
是79347712
,可以发现16384 + 79315040 = 79331424 < 79347712
,但是79347712 - 16384 = 79331328
,79331328/(1024 * 16) = 4842
,其中(1024 * 16)
代表16k字节
大小,因为👇
iOS中一页
16K
,MachO中都是以页
为单位对齐
的。
这也验证了以页对齐
,并且这也是Load Commands
中可以插入LC_LOAD_DYLIB
的原因。
Header的数据
上图是arm64架构下的Header,对应dyld的定义代码结构如下(loader.h
)👇
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 */
};
参数 | 释义 |
---|---|
magic |
魔数,快速定位属于64位还是32位 |
cputype |
CPU类型,比如ARM |
cpusubtype |
CPU具体类型,arm64,armv7 |
filetype |
文件类型,比如可执行文件 |
ncmds |
Number of Load Commands ,即Load Commands总的条数
|
sizeofcmds |
Size of Load Commands ,即Load Commands总的大小
|
flags |
标识二进制文件支持的功能,主要是和系统加载、链接有关 |
reserved |
arm64特有 ,保留字段 |
其中,filetype
的类型有👇
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static
linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug
sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
#define MH_FILESET 0xc /* a file composed of other Mach-Os to
be run in the same userspace sharing
a single linkedit. */
3.3 Load Commands
dyld
检索完Header
之后就开始加载和解析Load Commands
了,Load Comands
的大致结构如下👇
2.3.1 load_command
结构体
对应的代码👇
/*
* The load commands directly follow the mach_header. The total size of all
* of the commands is given by the sizeofcmds field in the mach_header. All
* load commands must have as their first two fields cmd and cmdsize. The cmd
* field is filled in with a constant for that command type. Each command type
* has a structure specifically for it. The cmdsize field is the size in bytes
* of the particular load command structure plus anything that follows it that
* is a part of the load command (i.e. section structures, strings, etc.). To
* advance to the next load command the cmdsize can be added to the offset or
* pointer of the current load command. The cmdsize for 32-bit architectures
* MUST be a multiple of 4 bytes and for 64-bit architectures MUST be a multiple
* of 8 bytes (these are forever the maximum alignment of any load commands).
* The padded bytes must be zero. All tables in the object file must also
* follow these rules so the file can be memory mapped. Otherwise the pointers
* to these tables will not work well or at all on some machines. With all
* padding zeroed like objects will compare byte for byte.
*/
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
每一个load_command
都需要包含👇
-
cmd
👉 加载类型 -
cmdsize
👉 加载的大小
2.3.2 所有load_command
的具体信息
接下来我们仔细看看每个load_command
具体包含哪些信息👇
__PAGEZERO
空指针陷阱
,目的是为了和32位
指令完全分开。(32位
地址在4G以下
,64位
地址大于4G
,其中0xffffffff = 4G
)。有以下几个重要的描述👇
-
Segment Name
👉__PAGEZERO
不占用数据(file size为0),唯一有的是VM Size
(arm64 4G,armv7比较小)。 -
VM Addr
👉 虚拟内存地址 -
VM Size
👉 虚拟内存大小。运行时刻在内存中的大小,一般情况下和File size相同,__PAGEZERO例外。 -
File offset
👉 数据在文件中偏移量。 -
File size
👉 数据在文件中的大小。
一般我们定位地址是通过
VM Addr + ASLR
。
__TEXT、__DATA、__LINKEDIT
他们的结构和__PAGEZERO
大致差不多,用途是将文件中(32位/64位)的段映射
到进程地址空间中。分为三大块
👉 分别对应DATA
中的Section(__TEXT + __DATA)
、__LINKEDIT
,告诉dyld
占用多大空间。
LC_DYLD_INFO_ONLY
动态链接相关信息。
-
Rebase
👉 重定向(ASLR)偏移地址
和大小
。从Rebase Info Offset + ASLR
开始加载336
个字节数据。 -
Binding
👉 绑定外部符号
。 -
Weak Binding
👉 弱绑定。 -
Lazy Binding
👉 懒绑定,用到的时候再绑定。 -
Export info
👉 对外开放的函数。
LC_SYMTAB
符号表地址。
-
Symbol Table Offset
👉 符号表地址偏移量 -
Number of Symbol
👉 符号总数量 -
String Table Offset
👉 字符串表地址偏移量 -
Symbol Table Size
👉 字符串表大小
LC_DSYMTAB
动态符号表地址。
也是包含一些索引、数量、地址偏移量
等信息。
LC_LOAD_DYLINKER
使用谁加载,iOS系统是使用dyld
加载,如下图👇
LC_UUID
文件的UUID,即MachO
文件的唯一识别标识
。
LC_VERSION_MIN_IPHONES
支持最低
的操作系统版本。
LC_SOURCE_VERSION
源代码的版本号。
LC_MAIN
程序主程序的入口地址和栈大小。
LC_ENCRYPTION_INFO_64
加密的信息。
LC_LOAD_DYLIB
依赖的库的路径,包含第三方的库。
系统的库👇
第三方库👇
LC_RPATH
Frameworks库的路径。
-
@executable_path
👇
-
@loader_path
👇
LC_FUNCTION_STARTS
函数起始地址表。
LC_DATA_IN_CODE
定义在代码段内的非指令的表。
LC_DATA_SIGNATURE
代码签名。
3.4 Data
Data
包含Section(__TEXT + __DATA)
、__LINKEDIT
。
3.4.1 __TEXT
__TEXT
即代码段,就是我们写的代码。主要的几个子段有👇
1. __text: 代码节,存放机器编译后的代码
2. __stubs: 用于辅助做动态链接代码(dyld).
3. __stub_helper:用于辅助做动态链接(dyld).
4. __objc_methname:objc的方法名称
5. __cstring:代码运行中包含的字符串常量,比如代码中定义`#define kGeTuiPushAESKey @"DWE2#@e2!"`,那DWE2#@e2!会存在这个区里。
6. __objc_classname:objc类名
7. __objc_methtype:objc方法类型
8. __ustring:
9. __gcc_except_tab:
10. __const:存储const修饰的常量
11. __dof_RACSignal:
12. __dof_RACCompou:
13. __unwind_info:
3.4.2 __DATA
__DATA数据段。主要的几个子段有👇
1. __got:存储引用符号的实际地址,类似于动态符号表,存储了`__nl_symbol_ptr`相关函数指针。
2. __la_symbol_ptr:lazy symbol pointers。懒加载的函数指针地址(C代码实现的函数对应实现的地址)。和__stubs和stub_helper配合使用。具体原理暂留。
3. __mod_init_func:模块初始化的方法。
4. __const:存储constant常量的数据。比如使用extern导出的const修饰的常量。
5. __cfstring:使用Core Foundation字符串
6. __objc_classlist:objc类列表,保存类信息,映射了__objc_data的地址
7. __objc_nlclslist:Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行。
8. __objc_catlist: categories
9. __objc_nlcatlist:Objective-C 的categories的 +load函数列表。
10. __objc_protolist:objc协议列表
11. __objc_imageinfo:objc镜像信息
12. __objc_const:objc常量。保存objc_classdata结构体数据。用于映射类相关数据的地址,比如类名,方法名等。
13. __objc_selrefs:引用到的objc方法
14. __objc_protorefs:引用到的objc协议
15. __objc_classrefs:引用到的objc类
16. __objc_superrefs:objc超类引用
17. __objc_ivar:objc ivar指针,存储属性。
18. __objc_data:objc的数据。用于保存类需要的数据。最主要的内容是映射__objc_const地址,用于找到类的相关数据。
19. __data:暂时没理解,从日志看存放了协议和一些固定了地址(已经初始化)的静态量。
20. __bss:存储未初始化的静态量。比如:`static NSThread *_networkRequestThread = nil;`其中这里面的size表示应用运行占用的内存,不是实际的占用空间。所以计算大小的时候应该去掉这部分数据。
21. __common:存储导出的全局的数据。类似于static,但是没有用static修饰。比如KSCrash里面`NSDictionary* g_registerOrders;`, g_registerOrders就存储在__common里面
3.4.3 __LINKEDIT
__LINKEDIT
主要包含👇
Dynamic Loader Info
👉 动态加载信息Function Starts
👉 入口函数Symbol Table
👉 符号表Dynamic Symbol Table
👉 动态库符号表String Table
👉 字符串表Code Signature
👉 代码签名
验证
我们知道,获取类名
可以用_objc_classname
, 获取方法名
可以用_objc_methname
,但是这两个数据怎么串联匹配起来的?根据查相关资料,是通过__objc_classlist
来映射的。
验证该问题需借助2个工具 👉 MachOView
和Hopper
。
-
MachOView
中打开Mach-O文件,直接看__objc_classlist
👇
我们选择第一个地址,在Hopper
中看看102725E28
(按G
搜索)👇
双击,对应到__objc_class
👇
而__objc_class
对应的源码👇
typedef struct objc_class{
struct __objc_class* isa;
struct __objc_class* superclass;
struct __objc_cache* cache;
struct __objc_vtable* vtable;
struct __objc_ data* data;
}objc_class;
- 第1个成员是
isa
指针,指向了MetaClass
,对应的地址是102badf90
👇
- 第2个成员是指向父类的指针,对应地址为
0000000000000000
- 第5个成员指向
__objc_ data
,双击它,对应的地址为102737e30
👇
接着我们看看__objc_data
对应的数据结构源码👇
typedef struct objc_data{
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
void* ivarlayout;
char* name;
struct __objc_method_list* baseMethod;
struct __objc_protos* baseProtocol;
struct __objc_ivars* ivars;
struct __objc_ivars weakIvarLayout;
struct __objc_ivars baseProperties;
}
主要的几个成员👇
- 第6个成员
name
保存的类名
,对应的地址是0x102445615
该地址对应的类名称是_AFURLSessionTaskSwizzling
,至此,找到了类名称
。
- 第7个成员
baseMethod
保存了类所有方法,一样,对应的地址是102737de0
👇
接着查看__objc_method_list
的源码👇
typedef struct objc_method_list{
uint32_t flags;
uint32_t count;
}
使用到的数据主要是count
,对应数据为3
,对应10进制
数也是3
,说明有3个方法
👇
具体方法对应的数据结构为👇
typedef struct objc_method{
char* name;
char* signature;
void* implementation;
}
objc_method_list
结构体占用8(4+4)字节,而__objc_method_list
的地址是0000000102737de0
+ 8字节 = 第1个方法
的地址0000000102737de8
,objc_method
结构体占用24(8*3)字节,再加24字节得到第2个方法
的地址0000000102737e00
,同理再加24字节得到第3个方法
的地址0000000102737e18
。
接着我们在MachOView
中查看第1个方法的地址0000000102737de8
👇
上图中,0000000102737de8
中存储的第一个8字节
地址是0102331F27
,再去到Hopper
中搜索该地址👇
同理,第2个8字节
地址是01023326C4
👇
第3个8字节
地址是010232C51D
👇
至此,我们从__objc_classlist
中查找地址,首先通过__objc_class
的isa
指针找到类名称
,接着找到下面的成员变量base method
的地址,找到objc_method_list
方法列表,再根据objc_method
结构体大小,计算内存平移
后的地址,找到了所有的方法名称
。
以上就是dyld加载类名并关联方法列表
的一个示例过程。
总结
- Mach-O属于一种文件格式
- 包含:可执行文件、静态库、动态库、dyld等
- 可执行文件:
- 通用二进制文件:集合了多种架构
- lipo命令
- -info 查看架构
- ‐thin 拆分架构
- ‐creat 合并架构
- Mach-O结构
- Header:用于快速确定文件的CPU类型、文件类型等
- Load Commands:指示加载器(例如dyld)如何设置并且加载二进制数据
- Data:存放数据👇
- 代码
- 数据
- 字符串常量
- 类
- 方法