Mach-O相关概念
一、什么是Mach-O
Mach-O
是Mach Object
的缩写,是Mac/iOS上用于存储程序、库的标准格式
二、属于Mach-O格式的文件类型
#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 /* set of mach-o's */
三、常见的Mach-O文件类型
-
MH_OBJECT
- 目标文件(.o)
- 静态库文件(.a),静态库文件其实就是N个.o合并在一起
-
MH_EXECUTE:可执行文件
-
MH_DYLB:动态库文件
- .dylib
- .framework/xx
-
MH_DYLINKER:动态链接编辑器
- /usr/bin/dyld
-
MH_DSYM:存储着二进制文件符号信息的文件
- .dSYM/Contents/Resources/DWARF/xx(常用于分析app的奔溃信息)
四、Universal Binary通用二进制文件
-
通用二进制文件:同时适用于多种架构的二进制文件;
-
由于通用二进制文件需要存储多种架构的代码,通用二进制文件通常比单一平台二进制的程序要大;
-
由于两种架构有共同的一些资源,所以并不会达到单一架构的两倍之多;
-
由于执行过程中只调用一部分代码,运行起来也不需要额外的内存;
-
因为文件比原来的要大,也被成为“胖二进制文件”(Fat Binary);
-
查看二进制支持的架构命令行
lipo -info xxx
-
胖二进制文件瘦身
lipo 胖二进制文件路径 -thin 架构类型 -output 输出文件路径
-
两种架构的二进制合并
lipo -create 文件1路径 文件2路径 -output 输出文件路径
五、Mach-O基本结构
5.1 窥探Mach-O的结构
-
命令行工具
1、file:查看Mach-O的文件类型
2、otool:查看Mach-O特定部分和段的内容
3、lipo:常用于多架构Mach-O文件的处理查看架构信息:lipo -info 文件路径 导出某种特定架构:lipo 文件路径 thin 架构类型 -output 输出文件路径 合并多种架构:lipo 文件路径1 文件路径2 -output 输出文件路径
-
GUI工具
5.2 Mach-O文件包含3个主要区域
- Header(头部) : 指明了cpu架构、大小端序、文件类型、Load commands个数等一些基本信息
- Load commands(加载命令) : 描述文件在虚拟内存中的逻辑结构、布局
- Raw segment data(数据区) : 在Load commands中定义的Segment的原始数据,包含了代码和数据等。
5.2.1 Header
字段 | |
---|---|
magic | 很多类型的文件,其起始的几个字节的内容是固定的,根据这几个字节的内容就可以确定文件类型,因此这几个字节的内容被称为魔数 (magic number)。 |
cputype | CPU类型以及子类型字段,该字段确保系统可以将适合的二进制文件在当前架构下运行 |
cpusubtype | CPU指定子类型,对于inter,arm,powerpc等CPU架构,其都有各个阶段和等级的CPU芯片,该字段就是详细描述其支持CPU子类型 |
filetype | 说明该mach-o文件类型(可执行文件,库文件,核心转储文件,内核扩展,DYSM文件,动态库) |
ncmds | 说明加载命令条数 |
sizeofcmds | 表示加载命令大小 |
flags | 标志位,该字段用位表示二进制文件支持的功能,主要是和系统加载,链接相关 |
reserved | 保留字段 |
-
magic number
苹果平台有以下几种magic类型:
脚本 - \x7FELF,常用于shell及其他解释器,如 Perl, AWK 等
通用二进制格式 - 0xcafebabe、0xbebafeca,包含多种架构支持的二进制格式,只在 macOS 上支持
MachO格式 - 根据苹果xnu内核源码,OSX和iOS上分别有以下几种不同架构对应的Magic number:
MH_CIGAM
是MH_MAGIC
的反写,表示在小端序(litter endian)环境下使用,所以MH_MAGIC是在大端序(big endian)环境下使用
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM NXSwapInt(MH_MAGIC)
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 NXSwapInt(MH_MAGIC_64)
Magic number显示涉及到大小端字节序,在class-dump中也可以看到下列源码:
_byteOrder = CDByteOrder_LittleEndian;
CDDataCursor *cursor = [[CDDataCursor alloc] initWithData:data];
_magic = [cursor readBigInt32];
if (_magic == MH_MAGIC || _magic == MH_MAGIC_64) {
_byteOrder = CDByteOrder_BigEndian;
} else if (_magic == MH_CIGAM || _magic == MH_CIGAM_64) {
_byteOrder = CDByteOrder_LittleEndian;
} else {
return nil;
}
-
常见flags:
MH_NOUNDEFS 0x1 /* Target 文件中没有带未定义的符号,不存在链接依赖 */ MH_DYLDLINK 0x4 该目标文件是dyld的输入文件 MH_SPLIT_SEGS 0x20 /* Target 文件中的只读 Segment 和可读写 Segment 分开 */ MH_TWOLEVEL 0x80 /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */ MH_FORCE_FLAT 0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */ MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */ MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */ MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */ MH_PIE 0x200000 /* 对可执行的文件类型启用地址空间 layout 随机化,系统加载进程后为其随机分配一个虚拟内存空间。 */ MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
MH_WEAK_DEFINES表示可执行文件使用了弱符号。 弱符号是一种链接技巧,可以避免在未使用的支持代码中进行链接。 例如,编译器进行分析并确定应用程序仅为整数,并告知链接器不在浮点支持代码中链接。
5.2.2 Load commands
常见的command及作用
command | 作用 |
---|---|
LC_SEGMENT/LC_SEGMENT_64 | 将对应的段中的数据加载并映射到进程的内存空间去 |
LC_SYMTAB | 符号表信息 |
LC_DYSYMTAB | 动态符号表信息 |
LC_LOAD_DYLINKER | 标明我们的MachO是被谁加载进去的,即动态加载连接器dyld |
LC_UUID | 标示该二进制文件唯一的 UUID,128bit |
LC_VERSION_MIN_IPHONEOS/MACOSX | 要求的最低系统版本(Xcode中的Deployment Target) |
LC_MAIN | 设置程序主线程的入口地址和栈大小 |
LC_ENCRYPTION_INFO | 加密信息 |
LC_LOAD_DYLIB | 加载的动态库,包括动态库地址、名称、版本号等 |
LC_FUNCTION_STARTS | 函数地址起始表 |
LC_CODE_SIGNATURE | 代码签名信息 |
LC_SEGMENT/LC_SEGMENT_64用于描述如何加载数据到进程,最为重要,常见的有:
常见Segment | 含义 |
---|---|
__TEXT | 代码段/只读数据段 |
__PAGEZERO | __PAGEZERO 是在可执行文件有的,动态库里没有。这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。 |
__DATA | 数据段 |
__LINKEDIT | 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。该段是只可读,不可写不可执行 |
__OBJC | 包含会被Objective Runtime使用到的一些数据。 |
5.2.3 Section
- 常见的section
Section | 含义 |
---|---|
__text | 主程序可执行的机器码 |
__stubs | 用于动态库链接的桩,本质上是一小段会直接跳入lazybinding的表对应项指针指向的地址的代码。 |
__stub_helper | 动态库链接的桩的辅助函数。上述提到的lazybinding的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。 |
__cstring | 去重后的常量字符串符号表描述信息,通过该区信息,可以获得常量字符串符号表地址 |
_TEXT __const | 初始化过的常量 |
__unwind_info | 用于存储处理异常情况信息 |
__objc_methname | 保存OC里面方法名 |
__objc_classname | 保存OC类的名字 |
__objc_methtype | 保存ObOCjc类的一些信息(函数签名) |
__objc_classlist | OC的类列表 |
__objc_nlclslist | OC的 +load 函数列表,比 __mod_init_func 更早执行 |
__objc_catlist | OC的category列表 |
__objc_protolist | OC的协议列表 |
__objc_imageinfo | 保存文件中OC执行代码的一些信息 |
__objc_selrefs | 指向selectors的引用 |
__objc_protorefs | 指向protocol的引用 |
__objc_classrefs | 指向classes的引用 |
__objc_superrefs | 指向super classes的引用 |
__mod_init_func | 初始化的全局函数地址,在 main 之前被调用 |
__bss | 未初始化的静态变量 |
_got | 存储引用符号的实际地址,类似于动态符号表 |
__bss | 未初始化的静态变量 |
__nl_symbol_ptr | 非lazy-binding的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 |
__la_symbol_ptr | lazy-binding的指针表,每个表项中的指针一开始指向stub_helper |
DATA.common | 没有初始化过的符号声明 |
5.2.4 _debug相关section
-
作用
用于记录调试信息
-
原理
Xcode
使用.dSYM
文件记录符号表信息,符号表文件.dSYM
实际上是从Mach-O
文件中抽取调试信息而得到的文件目录。实际用于保存调试信息的文件是DWARF
,在Mach-O
中对应_debug
相关的section
。 -
详细说明
DWARF(Debugging With Arbitrary Record Formats),是
ELF
和Mach-O
等文件格式中用来存储和处理调试信息的标准格式。.dSYM
中真正保存符号表数据的是DWARF
文件。DWARF
中不同的数据都保存在相应的section
(节)中,ELF
文件里所有的section
名称都以.debug_
开头,Mach-O
中关于section
的命名和ELF
稍有区别,把名称前的.
换成了_
,例如.debug_info
变成了_debug_info
。不管是
Xcode
勾选生成.dSYM
文件,还是手动生成调试文件,实际都调用dsymutil
工具从Mach-O
文件中提取出调试符号表文件。使用
dwarfdump
工具,可以解析查看_debug
相关section
数据。解析具体崩溃地址等:dwarfdump -e --debug-info YourPath/YourApp.dSYM/Contents/Resources/DWARF > info-e.txt
dwarfdump -e --debug-line YourPath/YourApp.dSYM/Contents/Resources/DWARF > line-e.txt
-
相关文档
六、Mach-O加载过程
通过前面内容,我们知道Mach-O
有多种文件类型,比如MH_DYLIB
文件、MH_BUNDLE
文件、MH_EXECUTE
文件(这些需要dyld动态加载),MH_OBJECT
(内核加载)等。所以一个进程往往不是只需要内核加载器就可以完成加载的,需要dyld
来进行动态加载配合。
加载过程底层执行:
execve
__mac_execve
exec_activate_image
exec_mach_imgact
load_machfile
parse_machfile
load_dylinker
一、内核加载流程
- 分配虚拟内存空间。
fork
进程。- 加载
Mach-O
到进程空间。 - 加载动态连接器
dyld
并将控制权交给dyld
处理。
二、dyld处理流程
主要有以下步骤:Load dylibs
-> Rebase
-> Bind
-> ObjC
-> Initializers
-
处理环境变量
大部分可以在Xcode
进行相关的配置,进行对应的操作(如Log相关信息) -
解析Mach-O执行文件
-
加载共享动态库
默认的动态库会合并成一个大缓存文件,放到/System/Library/Cache/com.apple.dyld/
目录下,按不同的架构分别保存着。其中包括UIKit
,Foundation
等基础库。 -
Rebase/Bind
在系统动态加载Mach-O
文件的时候,会经过Rebase
以及Bind
两个阶段,其中Rebase
是将内部指针进行固定数值的偏移,而Bind
则正式用于将外部符号转为实际指针的步骤。Rebase
数据描述了哪些是对指向Mach-O
内部的引用并将其修正,而Bind
数据描述哪些是指向外部的引用并进行修正。rebasing
和binding
包括weak_bind
以及lazy_bind
,它们在__LINKEDIT
段内数据流的编码协议基本相同,都是以操作数(opcode)、立即数(immediate)以及uleb128/sleb128编码的偏移组成。Rebase
- 程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行修复才能正常访问,这个操作就是Rebase
。
rebasing
的协议和操作相对简单,都是找到地址后给其值加上偏移即可。
rebase
协议:通过byte
&0xF0
得到opcode
(操作数),byte
&0x0F
得到immediate
(立即数),根据操作数(opcode)进行分支处理。Bind
- 由于符号在不同的库里面,所以需要符号绑定(Bind
)这个过程。
binding
相对rebasing
较复杂一些,它多了查找依赖库的部分,不过总体协议是相似的。包含non-lazy binding
、lazy binding
和weak binding
。在ObjC
中,类继承关系以及protocol
等是non-lazy
的,启动时就需要开始绑定,而在函数里的调用外部函数等等都是lazy binding
的,在第一次调用时才会进行绑定。binding
协议:和rebasing
相同,通过byte
&0xF0
得到opcode
(操作数),byte
&0x0F
得到immediate
(立即数),根据操作数(opcode)进行分支处理。每次binding
是在rebasing
之后进行的,他们交替进行,每个Mach-O
镜像加载完成后需要将内部的地址引用都修正为偏移之后的正确地址,然后执行binding
来修改外部引用地址。Export
-export
数据描述了对外可见的符号,通过objdump
命令可查看外部可见符号;在进行
rebasing
之前,内核只是将Mach-O
数据映射到虚拟内存,还未加载到内存。当rebasing
阶段开始在__DATA
段进行读取时,发现没有数据,产生了page fault
内核异常,这个时候内核才会从磁盘将相应的页(page)读到内存继续进行rebasing
. -
准备Objc环境
dyld
将主程序Mach-O
基址指针和包含的ObjC
相关类信息传递到libobjc
。
ObjC Runtime
从__DATA
段中获取ObjC
类信息,由于ObjC
是动态语言,可以通过类名获取其实例,所以Runtime
维护了一个映射所有类的全局类名表。当加载的数据包含了类的定义,类的名字就需要注册到全局表中。
获取protocol
、category
等类相关属性并与对应类进行关联。ObjC
的调用都是基于selector
的,所以需要对selector
全局唯一性进行处理。以上步骤由
dyld
启动libSystem.dylib
统一对基础库进行调用执行,这里面就包含了libobjc
的Runtime
,同时Runtime
会在dyld
绑定回调,当dyld
处理完相关数据后就会调用ObjC Runtime
执行Setup
工作。 -
Initializers
通过ObjC Runtime
在dyld
注册的通知,当Mach-O
镜像准备完毕后,dyld
会回调到ObjC
中执行+load
方法,包括以下步骤:(1)获取所有
non-lazy class
列表。
(2)按继承以及category
的顺序将类排入待加载列表。
(3)对待加载列表中的类进行方法判断并调用+load
方法。
执行C/C++
初始化构造器,如通过attribute((constructor))
注解的函数。
如果包含C++
,则dyld
同样会回调到libc++
库中对全局静态变量、隐式初始化等进行调用。