1. 介绍
Mach-O
是Mach Object
文件格式的缩写。它是用于可执行文件,动态库,目标代码的文件格式。作为a.out
格式的替代,Mach-O
格式提供了更强的扩展性,以及更快的符号表信息访问速度。
熟悉Mach-O
文件格式,有助于了解苹果底层的软件运行机制,更好的掌握dyld
(dyld是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作) 加载Mach-O
的步骤。
比如,在我们项目下的Products
下的xxx.app
文件,其实就是一个文件夹,我们在其右键显示包内容,可以看到一个xxx
的Unix
可执行文件,这个就是iOS
可执行文件,符合Mach-O
格式的。
2. Mach-O文件类型
对于OS X
和iOS
来说,Mach-O
是其可执行文件的格式,主要包括以下几种文件类型:
Executable
可执行文件Dylib
动态链接库Bundle
无法被链接的动态库,只能在运行时使用dlopen加载Image
指的是Executable、Dylib和Bundle的一种Framework
包含Dylib、资源文件和头文件的集合
2.1 使用 file
命令查看文件类型
-
以导出来的 developer 包为例,我们看一下可执行文件的类型,把
截屏2020-07-30 下午9.12.05.png.ipa
改为.zip
,直接解压拿到Payload
文件:
-
然后右键
截屏2020-07-30 下午9.13.10.pngPayload
里面的文件,“显示包内容”找到可执行文件:
-
使用
截屏2020-07-30 下午9.16.19.pngfile
命令查看:
我们可以看到是一个 64 位的 arm64
架构的可执行文件。
3. 通用二进制文件(Universal binary
)
包含了支持多架构的
Mach-O ececutable
可执行文件被称为 : 通用二进制文件 , 即多种架构都可读取运行。
通常也被称为Universal binary
, 在MachOView
(用于查看Mach-O文件
) 等中叫做Fat binary
,这种二进制文件是可以完全拆分开来 , 或者重新组合的。
3.1 介绍
- 苹果公司提出的一种程序代码,能同时适用多种架构的二进制文件
- 同一个程序包中同时为多种架构提供最理想的性能
- 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大
- 由于两种架构之间有共同的非执行资源,所以并不会达到单一版本的两倍至多
- 而且由于执行中只调用一部分代码,运行起来也不需要额外的内存
我们还是通过 file
命令,去查看一个通用二进制文件:
可以看到,上面👆该文件包含了 2 个架构:
armv7 arm64
。
3.2 Fat binary
的组合和拆分
3.2.1 使用 lipo -info
命令
还可以使用 lipo -info
查看 mach-o
文件支持的架构
3.2.2 拆分 Fat binary
lipo macho文件名称 -thin 要拆分哪个架构 -output 拆分出来文件名
例:
截屏2020-07-30 下午9.33.00.png
我们可以看到文件夹下面多了一个 Test_arm64
的文件,使用 lipo -info
命令再查看一下:
这样,我们就拆分出来 arm64
架构的 mach-o
文件了,另外,拆分后源文件并不会改变的,自己可以去验证一下。
3.2.3 合并 Fat binary
我们还用上面提到的方法,把源文件的 armv7
架构拆分出来,名字为:Test_armv7
,然后,使用下面的合并命令:
lipo -create [arm64架构文件名] [armv7架构文件名] -output [新的 mach-o文件名]
例:
lipo -create Test_arm64 Test_armv7 -output Test_new
然后就生成了一个新的通用二进制文件,名字为:Test_new
。
我们再来看下新文件和源文件的哈希值,是一模一样的:
截屏2020-07-30 下午9.39.55.png
注意:上面这种方式在我们合并静态库的时候会经常用到的,因为静态库本身就是
Mach-O
文件嘛,另外,我们在分析Mach-O
文件的时候,只需要分析单一的一种架构即可。
4. Mach-O
文件结构
4.1 Mach-O
文件结构图解
我们可以看下面这张 Mach-O
镜像文件格式:
通过上图,可以看出
Mach-O
主要由以下三部分组成:
-
Mach-O 头部(
Mach Header
)
对Mach-O
的概要说明,主要是描述了Mach-O
的CPU
架构,文件类型,以及加载命令等信息。它能帮助校验Mach-O
合法性和定位文件的运行环境。 -
加载命令 (
Load Commands
)
描述了文件中数据的具体组织结构,不同的数据类型会使用不同的加载命令表示,其占用的内存和加载命令的总数在Headers
中已经指出。
通过图中看出,应该就是是Data
中不同的Segment
的加载命令。` -
Data
Data
中每个段(Segment
)的具体数据都保存在这里,每个段都有一个或多个Section
,它们存放了具体的数据和代码,主要包含代码,数据,例如符号表,动态符号表等等,Segment
根据对应的Load Command
被dyld
加载入内存中。
那么Data
部分包含的Segment
都有哪些呢?
绝大多数Mach-O
包括以下三个段(也支持用户自定义Segment
,但是很少见)
-
__TEXT 代码段
,只读,包括函数,和只读的字符串 -
__DATA 数据段
,读写,包括可读写的全局变量等 -
__LINKEDIT __LINKEDIT
包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。
4.2 使用 MachOView
下载地址:https://sourceforge.net/projects/machoview/,down 下来直接运行,就可以,然后 open
我们的 Mach-O
文件:
下面我们就来看看,每一部分都是什么内容,下面提到的一些代码,在这里:mach-o源代码。
4.3 Mach Header
截屏2020-07-30 下午9.57.00.pngHeader
中存储的内容大致如上图所示,存储的是Mach-O
文件格式有关的结构体,针对32
位和64
位架构的cpu
,分别使用了mach_header
和mach_header_64
结构体来描述Mach-O
头部。
mach_header64
结构体的定义如下:
struct mach_header_64 {
uint32_t magic; /* 标志符,用来标识Mach-O的平台属性,确认文件的类型。快速定位 64 位/32 位*/
cpu_type_t cputype; /* cpu 类型 比如 ARM */
cpu_subtype_t cpusubtype; /* cpu 具体类型 比如arm64 , armv7 */
uint32_t filetype; /* 标识Mach-O文件的具体类型,比如 MH_EXECUTE,代表可执行文件,MH_DYLINKER,表明该文件是动态链接器程序文件,符号文件(DYSM) */
uint32_t ncmds; /* 加载命令 load commands 的个数 */
uint32_t sizeofcmds; /* 加载命令 load commands 的大小 */
uint32_t flags; /* 标志位标识二进制文件支持的功能 , 主要是和系统加载、链接有关 */
uint32_t reserved; /* reserved , 保留字段 */
};
mach_header_64
相较于 mach_header
,也就是 32
位头文件,只是多了一个reserved
保留字段,mach_header
是链接器加载时最先读取的内容,它决定了一些基础架构、系统类型、指令条数等信息。
4.3.1 filetype
这里记录下Mach-O的文件类型,包括:
#define MH_OBJECT 0x1 /* Target 文件:编译器对源码编译后得到的中间结果 */
#define MH_EXECUTE 0x2 /* 可执行二进制文件 */
#define MH_FVMLIB 0x3 /* VM 共享库文件(还不清楚是什么东西) */
#define MH_CORE 0x4 /* Core 文件,一般在 App Crash 产生 */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* 动态库 */
#define MH_DYLINKER 0x7 /* 动态连接器 /usr/lib/dyld */
#define MH_BUNDLE 0x8 /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
#define MH_DYLIB_STUB 0x9 /* 静态链接文件(还不清楚是什么东西) */
#define MH_DSYM 0xa /* 符号文件以及调试信息,在解析堆栈符号中常用 */
#define MH_KEXT_BUNDLE 0xb /* x86_64 内核扩展 */
4.3.2 flags
Mach-O
文件的标志位。主要作用是告诉系统该如何加载这个Mach-O
文件以及该文件的一些特性。有很多值,我们取常见的几种:
#define MH_NOUNDEFS 0x1 /* Target 文件中没有带未定义的符号,常为静态二进制文件 */
#define MH_SPLIT_SEGS 0x20 /* Target 文件中的只读 Segment 和可读写 Segment 分开 */
#define MH_TWOLEVEL 0x80 /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
#define MH_FORCE_FLAT 0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
#define MH_PIE 0x200000 /* 加载程序在随机的地址空间,只在 MH_EXECUTE中使用 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
- MH_PIE
随机地址空间,标明使用了ASLR
。
4.4 Load Commands
在内存中,mach_header
之后是Load Command
加载命令,这些加载命令在Mach-O
文件加载解析时,被内核加载器或者动态链接器调用,在内存中的结构如下:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
-
cmd
: 具体的加载类型 -
cmdsize
: 具体的load_command
结构所占内存的大小
苹果为cmd定义了若干的宏,用来表示cmd的类型,下面列举出几种:
// 描述该如何将32或64位的segment 加载入内存,对应segment command类型
#define LC_SEGMENT 0x1
#define LC_SEGMENT_64 0x19
// UUID, 2进制文件的唯一标识符
#define LC_UUID 0x1b
// 启动动态加载器dyld
#define LC_LOAD_DYLINKER 0xe
来看下都有哪些类型呢:
image.png
4.4.1 Segment load command
在这么多的load command
中,需要我们重点关注的是segment load command
。segment command
解释了该如何将Data
中的各个Segment
加载入内存中,而和我们APP相关的逻辑及数据,则大部分位于各个Segment
中。
Segment load command
分为32位和64位:
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
32位和64位的Segment load command基本类似,只不过在64位的结构中,把和寻址相关的数据类型,由32位的uint32_t改为了64位的uint64_t类型。
- segname segment的名称
- vmaddr 段的虚拟内存起始地址
- vmsize 段的虚拟内存大小
- fileoff 段在文件中的偏移量
- filesize 段在文件中的大小
- maxprot 段页面所需要的最高内存保护(可读 可写 可执行)
- initprot 段页面初始的内存保护
- nsects 段中包含section的数量
- flags 其他杂项标志位。
可以结合下面这张图去理解:
屏幕快照 2019-09-05 上午11.14.37.png
4.4.2 __PAGEZERO
这里有一个特殊的Segment
,叫做__PAGEZERO Segment
,这里说它特殊,是因为这个Segment其实是苹果虚拟出来的,只是一个逻辑上的段,而在Data中,根本没有对应的内容,也没有占用任何硬盘空间。
__PAGEZERO Segment在VM中被置为Read only,逻辑上占用APP最开始的4GB空间,用来处理空指针。
如下图,可以看到其vm size是4GB,但其真正的物理地址File size和offset都是0:
截屏2020-09-10 下午3.38.02.png
4.4.3 Section header
在Data
中,程序的逻辑和数据是按照Segment
(段)存储,在Segment
中,又分为0或多个section
,每个section
中在存储实际的内容。而之所以这么做的原因在于,在section
中,可以不用内存对齐达到节约内存的作用,而所有的section
作为整体的Segment
,又可以整体的内存对齐。
在Mach-O
文件中,每一个Segment load command
下面,都会包含对应Segment
下所有section
的header
。
定义如下:
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};
- sectname 就是这个section的名称
- segname 该section所属的segment名
- addr 该section在内存的起始位置
- size 该section的大小
- offset 该section的文件偏移
- align 字节大小对齐
- reloff 重定位入口的文件偏移
- nreloc 需要重定位的入口数量
- flags包含section的type和attributes
可以对比下图去理解一下:
屏幕快照 2019-09-05 上午11.26.01.png
4.5 Data部分
Mach-O的Data部分,其实是真正存储APP 二进制数据的地方,前面的header和load command,仅是提供文件的说明以及加载信息的功能。
Data部分也被分为若干的部分,除了我们前面提到的Segment外,还包括符号表,代码签名,动态加载器信息等。
而程序的逻辑和数据,则是放在以Segment分割的Data部分中的。我们在这里,仅关心Data中的Segment的部分。
先来看Segment,Mach-O中有如下几种Segment:
#define SEG_PAGEZERO "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,表示空指针区域 */
#define SEG_TEXT "__TEXT" /* 代码/只读数据段 */
#define SEG_DATA "__DATA" /* 数据段 */
#define SEG_OBJC "__OBJC" /* Objective-C runtime 段 */
#define SEG_LINKEDIT "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */
其中_TEXT
段和_DATA
段,是我们经常需要研究的,MachOView
下面也有详细列出:
4.5.1 _TEXT段
我们来看看_TEXT
段里都存放了什么,其实真正读取是从_TEXT
段开始读取的:
名称 | 内容 |
---|---|
__TEXT.__text | 主程序代码 |
__TEXT.__const | const 关键字修饰的常量 |
__TEXT.__stubs | 用于 Stub 的占位代码,很多地方称之为桩代码 |
__TEXT.__stubs_helper | 当 Stub 无法找到真正的符号地址后的最终指向 |
__TEXT.__objc_methname | 方法名称 |
__TEXT.__objc_classname | 类名称 |
__TEXT.__objc_methtype | 方法类型 ( v@: ) |
__TEXT.__cstring | 静态字符串常量 |
4.5.2 _DATA段
_DATA
在内存中是紧跟在_TEXT
段之后的,__DATA
段用于存储程序中所定义的数据,可读写。__DATA
段下常见的section
有:
名称 | 内容 |
---|---|
__DATA.__data | 初始化过的可变数据 |
__DATA.__la_symbol_ptr | lazy binding 的指针表,表中的指针一开始都指向 __stub_helper |
__DATA.nl_symbol_ptr | 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 |
__DATA.__const | 没有初始化过的常量 |
__DATA.__cfstring | 程序中使用的 Core Foundation 字符串(CFStringRefs) |
__DATA.__bss BSS, | 存放为初始化的全局变量,即常说的静态内存分配 |
__DATA.__common | 没有初始化过的符号声明 |
__DATA.__objc_classlist | Objective-C 类列表 |
__DATA.__objc_protolist | Objective-C 原型 |
__DATA.__objc_imginfo | Objective-C 镜像信息 |
__DATA.__objc_selfrefs | Objective-C self 引用 |
__DATA.__objc_protorefs | Objective-C 原型引用 |
__DATA.__objc_superrefs | Objective-C 超类引用 |
在__DATA段下,还有许多以__objc开头的section,而这些section,均是和runtime的加载有关的:
截屏2020-09-10 下午3.52.22.png
网友评论