# 进程与二进制格式
# 相关工具
# Mach-O 文件格式
## 示例
## Mach-O 头
## Data
### Segment(段)
### Section(节)
### 两个Section:__TEXT.__stubs、__TEXT.__stub_helper
## Load Command
### LC_CODE_SIGNATURE(数字签名)
### LC_SEGMENT(进程虚拟内存设置)
### LC_MAIN(设置主线程入口地址)
# 通用二进制格式(Universal Binary)
# 参考链接
上一篇说到源码经过预处理、编译、汇编之后生成目标文件,这一章介绍一下iOS、Mac OS中目标文件的格式Mach-O的结构
,方便了解之后的链接
生成可执行文件的过程。
先附上相关源码地址:与Mach-O 文件格式有关的结构体定义都可以从 /usr/include/mach-o/loader.h
中找到(直接在xcode项目中找到loader.h
,然后Show In Finder即可)。
# 进程与二进制格式
进程在众多操作系统中都有提及,它是作为一个正在执行的程序的实例,这是 UNIX 的一个基本概念。而进程的出现是特殊文件在内从中加载得到的结果,这种文件必须使用操作系统可以认知的格式,这样才对该文件引入依赖库,初始化运行环境以及顺利地执行创造条件。
Mach-O(Mach Object File Format)是 macOS 上的可执行文件格式,类似于 Linux 和大部分 UNIX 的原生格式 ELF(Extensible Firmware Interface)。macOS 支持三种可执行格式:解释器脚本格式、通用二进制格式和 Mach-O 格式(关于三者区别,在下面说到Mach-O Header的时候介绍)。
# 相关工具
命令行工具
file 命令,查看Mach-O文件的基本信息:
file 文件路径
otool 命令,查看Mach-O特定部分和段的内容
#查看Mach-O文件的header信息
otool -h 文件路径
#查看Mach-O文件的load commands信息
otool -l 文件路径
# 更多使用方法,终端输入otool -help查看
- lipo 命令,来处理多架构Mach-O文件,常用命令如下
#查看架构信息
lipo -info 文件路径
#导出某种类型的架构
lipo 文件路径 -thin 架构类型 -output 输出文件路径
#合并多种架构类型
lipo 文件路径1 文件路径2 -output 输出文件路径
GUI工具
- MachOView:文件浏览。MachOView官网
- hopper:反汇编工具
# Mach-O 文件格式
Mach-O 文件格式在官方文档中有一个描述图,很多教程中都引用到。官网文档
可以看的出 Mach-O 主要由 3 部分组成,下面一一讲述。Load Command的作用是指导内核加载器、动态链接器怎么将可执行文件装载到内存进行执行。所以Load Command放到最后一部分。
## 示例
用 helloworld 来做个试验:
/// main.cpp
#import
int main() {
printf("hello");
return 0;
}
使用 clang -g main.cpp -o main
生成执行文件。然后拖入到 MachOView 中来查看一下加载 Segment 的结构(当然使用 Synalyze It! 也能捕捉到这些信息的,但是 MachOView 更对结构的分层更加一目了然):
## Mach-O 头
Mach-O 头(Mach Header)描述了 Mach-O 的 CPU 架构、大小端、文件类型以及加载命令等信息。它的作用是让内核在读取该文件创建虚拟进程空间的时候,检查文件的合法性以及当前硬件的特性是否能支持程序的运行。
以下只给出 64 位定义的代码,因为 32 位的区别是缺少了一个预留字段:
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
struct mach_header_64 {
uint32_t magic; / magic(魔数):用来确认文件的格式,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。 /
cpu_type_t cputype; / CPU架构 /
cpu_subtype_t cpusubtype; / CPU子版本 /
uint32_t filetype; / 文件类型,常见的Mach-O文件有:MH_OBJECT(目标文件)、MH_EXECUTABLE(可执行二进制文件)、MH_DYLIB(动态库)等等。这些文件类型定义在 loader.h 文件中同样可以找到 /
uint32_t ncmds; / 加载器中加载命令的数量 /
uint32_t sizeofcmds; / 加载器中所有加载命令的总大小 /
uint32_t flags; / dyld 加载需要的一些标志,其中MH_PIE表示启用地址空间布局随机化(ASLR)。其他的值在loader.h文件中同样可以找到 /
uint32_t reserved; / 64位的保留字段 /
};
魔数会表明文件的格式。filetype会表明具体是什么文件类型(都是猫,也分黑猫、白猫)。
// magic:常见的魔数(Mac是小端模式)
Mach-O文件。用途:macOS 的原生二进制格式
#define MH_MAGIC 0xfeedface / 32位设备上的魔数,大端模式(符合人类阅读习惯,高位数据在前) /
#define MH_CIGAM 0xcefaedfe / 32位、小端(高位地址在后),CIGAM就是MAGIC反过来写,从命名上也可以看出端倪 /
#define MH_MAGIC_64 0xfeedfacf / 64位、大端 /
#define MH_CIGAM_64 0xcffaedfe / 64位、小端 /
通用二进制格式FAT。用途:包含多种架构支持的二进制格式,只在 macOS 上支持。(在文章末尾简单介绍一下,有兴趣可以瞜一眼)
#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
#define FAT_MAGIC_64 0xcafebabf
#define FAT_CIGAM_64 0xbfbafeca /* NXSwapLong(FAT_MAGIC_64) */
脚本格式。用途:主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 `#!` 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令。
魔数为 \x7FELF
// filetype:常见的Mach-O格式的文件类型
#define MH_OBJECT 0x1 / 可重定位的目标文件 /
#define MH_EXECUTE 0x2 / 可执行二进制文件 /
#define MH_DYLIB 0x6 / 动态绑定共享库 /
#define MH_DYLINKER 0x7 / 动态链接编辑器,如dyld /
#define MH_BUNDLE 0x8 / 动态绑定bundle(包)文件 /
#define MH_DSYM 0xa / 调试所用的符号文件 /
举例:利用otool工具查看Mach-o文件的头部
$ otool -hv bibi.decrypted
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC ARM V7 0x00 EXECUTE 59 6016 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 59 6744 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
## Data
数据区(Data):Data 中每一个段(Segment)的数据都保存在此,段的概念和 ELF 文件中段的概念类似,都拥有一个或多个 Section ,用来存放数据和代码。
Raw segment data存放了所有的原始数据,而Load commands相当于Raw segment data的索引目录
### Segment(段)
其中,LC_SEGMENT_64定义了一个64位的段,当文件加载后映射到地址空间(包括段里面节的定义)。64位段的定义如下:
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; / Load Command类型,这里LC_SEGMENT_64代表将文件中64位的段映射到进程的地址空间。LC_SEGMENT_64和LC_SEGMENT的结构差别不大 /
uint32_t cmdsize; / 代表Load commands的大小 /
char segname[16]; / 16字节的段名称 /
uint64_t vmaddr; / 段映射到虚拟地址中的内存起始地址 /
uint64_t vmsize; / 段映射到虚拟地址中的内存大小 /
uint64_t fileoff; / 段在当前架构(MachO)文件中的偏移量,如果是胖二进制文件,也指的是相对于当前MachO文件的偏移 /
uint64_t filesize; / 段在文件中的大小 /
vm_prot_t maxprot; / 段页面的最高内存保护,用八进制表示(4=r(read),2=w(write),1=x(execute执行权限)) /
vm_prot_t initprot; / 段页面最初始的内存保护 /
uint32_t nsects; / 段(segment)包含的区(section)的个数(如果存在的话) /
uint32_t flags; / 段页面标志 /
};
系统将 fileoff 偏移处 filesize 大小的内容加载到虚拟内存的 vmaddr 处,大小为vmsize,段页面的权限由initprot进行初始化。它的权限可以动态改变,但是不能超过maxprot的值,例如 _TEXT 初始化和最大权限都是可读/可执行/不可写。
常见的LC_SEGMENT
Segment (cmd为LC_SEGMET
),其segname[16]
有以下几种值:
- __PAGEZERO:空指针陷阱段,映射到虚拟内存空间的第1页,用于捕捉对 NULL 指针的引用。
- __TEXT:代码段/只读数据段。
- __DATA:读取和写入数据的段。
- __LINKEDIT:动态链接器需要使用的信息,包括符号表、重定位表、绑定信息、懒加载信息等。
- __OBJC:包含会被Objective Runtime使用到的一些数据。(从Macho文档上看,他包含了一些编译器私有的节。没有任何公开的资料描述)
### Section(节)
从示例图中可以看到,部分的 Segment (__TEXT
和 __DATA
) 可以进一步分解为 Section。
之所以按照 Segment(段) -> Section(节) 的结构组织方式,是因为在同一个 Segment 下的 Section,在内存中的权限相同(编译时,编译器把相同权限的section放在一起,成为segment),可以不完全按照 Page 的大小进行内存对齐,节省内存的空间。而 Segment 对外整体暴露,在装载程序时,完整映射成一个vma(Virtual Memory Address),更好的做到内存对齐,减少内存碎片(可以参考《OS X & iOS Kernel Programming》第一章内容)。
Section 具体的数据结构如下:
struct section_64 {
char sectname[16]; / Section 的名字 /
char segname[16]; / Section 所在的 Segment 名称 /
uint64_t addr; / Section 映射到虚拟地址的偏移(所在的内存地址) /
uint64_t size; / Section 的大小 /
uint32_t offset; / Section 在当前架构文件中的偏移 /
uint32_t align; / Section 的内存对齐边界 (2 的次幂) /
uint32_t reloff; / 重定位入口的文件偏移 /
uint32_t nreloc; / 重定位入口的数目 /
uint32_t flags; / Section标志属性 /
uint32_t reserved1; / 保留字段1 (for offset or index) /
uint32_t reserved2; / 保留字段2 (for count or sizeof) /
uint32_t reserved3; / 保留字段3 /
};
结合示例图,下面列举一些常见(并非全部)的 Section:
__TEXT Segment(段)下面的节:
__text 程序可执行的代码区域
__stubs 间接符号存根。本质上是一小段代码,跳转到懒加载/延迟绑定(lazybinding)指针表(即__DATA.la_symbol_ptr)。找到对应项指针指向的地址。
__sub_helper 辅助函数。帮助解决懒加载符号加载,上述提到的lazybinding的表(__DATA.la_symbol_ptr)中对应项的指针在没有找到真正的符号地址的时候,都指向这。
__objc_methname 方法名
__objc_classname 类名
__objc_methtype 方法签名
__cstring 去重后的只读的C风格字符串,包含OC的部分字符串和属性名
__const 初始化过的常量
__unwind_info 用户存储处理异常情况信息
__eh_frame 调试辅助信息
__DATA Segment(段)下面的节:
__data 初始化过的可变的数据
__const 没有初始化过的常量
__bss 没有初始化的静态变量
__common 没有初始化过的符号声明
__nl_symbol_ptr 非延迟导入/非懒加载(lazy-binding)符号指针表,每个表项中的指针都指向一个在dyld加载过程中,搜索完成的符号。即在dyld加载时会立即绑定值。
__la_symbol_ptr 延迟导入/懒加载(lazy-binding)符号指针表,每个表项中的指针一开始指向stub_helper。在第 1 次调用时才会绑定值。
__got 非懒加载全局指针表
__mod_init_func 初始化/constructor(构造)函数
__mod_term_func destructor(析构)函数
__cfstring OC字符串
__objc_classlist 程序中的类列表
__objc_nlclslist 程序中自己实现了+load方法的类
__objc_protolist 协议的列表
__objc_classrefs 被引用的类列表
__objc_ivar 成员变量
## 两个section:__TEXT.__stubs、__TEXT.__stub_helper
在 wikipedia 有一个关于 Method stub 的词条,大意就是:Stub 是指用来替换一部分功能的程序段。桩程序可以用来模拟已有程序的行为(比如一个远端机器的过程)或是对将要开发的代码的一种临时替代。
总结来说:
- stub就是一段代码,功能为:跳转到
__DATA.__la_symbol_ptr
(__DATA
Segment 中的__la_symbol_ptr
Section) 对应表项的数据,所指向的地址。 -
__la_symbol_ptr
里面的所有表项的数据在初始时都会被 binding 成__stub_helper
。 - 当懒加载符号第一次使用到的时候,按照上面的结构,会跳转到
__stub_helper
这个section的代码,然后代码中会调用dyld_stub_binder
来执行真正的bind。 bind结束后,就将__la_symbol_ptr
中该懒加载符号 原本对应的指向__stub_helper
的地址 修改为 符号的真实地址。 - 之后的调用中,虽然依旧会跳到
__stub
区域,但是__la_symbol_ptr
表由于在之前的调用中获取到了符号的真实地址而已经修正完成,所以无需在进入dyld_stub_binder
阶段,可以直接使用符号。
这样就完成了LazyBind的过程。Stub 机制 其实和 wikipedia
上的说法一致,设置一个桩函数(模拟、占位函数)并采用 lazy 思想做成延迟 binding 的流程。
在《深入解析 Mac OS X & iOS操作系统》中有详细的验证,也可以参考深入剖析Macho (1) 自己动手验证一下。
## Load Command
Mach-O文件头中包含了非常详细的指令,这些指令在被调用时清晰地指导了如何设置并加载二进制数据。这些指令,或称为“加载命令”,紧跟在基本的mach_header之后。
每一条命令,在load.c
文件中,都有对应的结构体,来记录信息。共同点是都采用“类型-长度-值
”的格式:
struct xxx_command {
uint32_t cmd; / 32位的cmd值(表示类型) ,下面列举了部分 /
uint32_t cmdsize; / 32位的cmdsize值(32位二进制为4的倍数,64位二进制为8的倍数) /
... / 记录命令本身的一些信息 /
}
//下面列举一些load command的类型(对应的cmd值),这里只列举了部分,全面的可以看源码,总共50多种load command。按照加载命令是由内核加载器、动态链接器处理分开记录。
内核加载器处理的加载命令:
#define LC_SEGMENT 0x1 / 定义一个段(Segment),加载后被映射到内存中,包括里面的节(Section) /
#define LC_LOAD_DYLINKER 0xe / 默认的加载器路径。通常路径是“/usr/lib/dyld” /
#define LC_UUID 0x1b / 用于标识Mach-0文件的ID,匹配二进制文件与符号表。在分析崩溃堆栈信息能用到,通过地址在符号表中找到符号 /
#define LC_CODE_SIGNATURE 0x1d / 代码签名信息 /
#define LC_ENCRYPTION_INFO_64 0x2C / 文件是否加密的标志,加密内容的偏移和大小 /
动态链接器处理的加载命令:
#define LC_SYMTAB 0x2 / 为文件定义符号表和字符串表,在链接文件时被链接器使用,同时也用于调试器映射符号到源文件。符号表定义的本地符号仅用于调试,而已定义和未定义的 external 符号被链接器使用 /
#define LC_DYSYMTAB 0xb / 将符号表中给出符号的额外符号信息提供给动态链接器。 /
#define LC_ID_DYLIB 0xd / 依赖的动态库,包括动态库名称、当前版本号、兼容版本号。可以使用“otool-L xxx”命令查看 /
#define LC_RPATH (0x1c | LC_REQ_DYLD) / RunpathSearchPaths,@rpath搜索的路径 /
#define LC_DYLD_INFO_ONLY (0x22 | LC_REQ_DYLD) / 记录了有关链接的重要信息,包括在__LINKEDIT中动态链接相关信息的具体偏移和大小。ONLY表示这个加载指令是程序运行所必需的,如果旧的链接器无法识别它,程序就会出错 /
#define LC_VERSION_MIN_IPHONEOS 0x25 / 系统要求的最低版本 /
#define LC_FUNCTION_STARTS 0x26 / 函数起始地址表,使调试器和其他程序能很容易地看到一个地址是否在函数内 /
#define LC_MAIN (0x28 | LC_REQ_DYLD) / 程序的入口。dyld获取该地址,然后跳转到该处执行。replacement for LC_UNIXTHREAD /
#define LC_DATA_IN_CODE 0x29 / 定义在代码段内的非指令的表 /
#define LC_SOURCE_VERSION 0x2A / 构建二进制文件的源代码版本号 /
有一些命令是由内核加载器
(定义在bsd/kern/mach_loader.c
文件中) 直接使用的, 其他命令是由动态链接器
处理的。
在Mach-O文件加载解析时,多个Load Command会告诉操作系统应当如何加载文件中每个Segment的数据,对系统内核加载器和动态链接器起引导作用。(不同的数据对应不同的加载命令,可以看到segment_command_64
、symtab_command
、dylib_command
等,下面我们会讲解Segment的加载命令,下一节讲静态链接时,会涉及符号表symtab的加载命令)。
下面,以三个内核加载器负责解析处理的load command,来简单看下:
### LC_CODE_SIGNATURE(数字签名)
Mach-O二进制文件有一个重要特性就是可以进行数字签名。尽管在 OS X 中仍然没怎么使用数字签名,不过由于代码签名和新改进的沙盒机制绑定在一起,所以签名的使用率也越来越高。在 iOS 中,代码签名是强制要求的,这也是苹果尽可能对系统封锁的另一种尝试:在 iOS 中只有苹果自己的签名才会被认可。在 OS X 中,code sign(1) 工具可以用于操纵和显示代码签名。man手册页,以及 Apple's code signing guide 和 Mac OS X Code Signing In Depth文档都从系统管理员的角度详细解释了代码签名机制。
LC_CODE_SIGNATURE
包含了 Mach-O 二进制文件的代码签名,如果这个签名和代码本身不匹配(或者如果在iOS上这条命令不存在),那么内核会立即给进程发送一个SIGKILL信号将进程杀掉,没有商量的余地,毫不留情。
在iOS 4之前,还可以通过两条sysctl(8)命令覆盖负责强制执行(利用内核的MAC,即Mandatory AccessControl)的内核变量,从而实现禁用代码签名检查:
sysctl -w security.mac.proc_enforce = 0 //禁用进程的MAC
sysctl -w security.mac.vnode_enforce=0 //禁用VNode的MAC
而在之后版本的iOS中,苹果意识到只要能够获得root权限,越狱者就可以覆盖内核变量。因此这些变量变成了只读变量。untethered越狱(即完美越狱)因为利用了一个内核漏洞所以可以修改这些变量。由于这些变量的默认值都是启用签名检查,所以不完美越狱会导致非苹果签名的应用程序崩溃——除非i设备以完美越狱的方式引导。
此外,通过 Saurik 的 ldid 这类工具可以在 Mach-O 中嵌入伪代码签名。这个工具可以替代OS X的code sign(1),允许生成自我签署认证的伪签名。这在iOS中尤为重要,因为签名和沙盒模型的应用程序“entitlement”绑定在一起, 而后者在iOS中是强制要求的。entitlement 是声明式的许可(以plist的形式保存),必须内嵌在Mach-O中并且通过签名盖章,从而允许执行安全敏感的操作时具有运行时权限。
OS X 和 iOS 都有一个特殊的系统调用csops(#169)用于代码签名的操作
### LC_SEGMENT(进程虚拟内存设置)
LC_SEGMENT(或LC_SEGMENT_64) 命令是最主要的加载命令,这条命令指导内核如何设置新运行的进程的内存空间。这些“段”直接从Mach-O二进制文件加载到内存中。
每一条LC_SEGMENT[64] 命令都提供了段布局的所有必要细节信息。见上文的数据结构成员变量。
有了LC_SEGMENT命令,设置进程虚拟内存的过程就变成遵循LC_SEGMENT命令的简单操作。对于每一个段,将文件中相应的内容加载到内存中:从偏移量为 fileoff 处加载 filesize 字节到虚拟内存地址 vmaddr 处的 vmsize 字节。每一个段的页面都根据 initprot 进行初始化,initprot 指定了如何通过读/写/执行位初始化页面的保护级别。段的保护设置可以动态改变,但是不能超过 maxprot 中指定的值(在iOS中,+x和+w是互斥的)。
### LC_MAIN(设置主线程入口地址)
从Mountain Lion开始,一条新的加载命令LC_MAIN
替代了LC_UNIX_THREAD
命令。
- 后者的作用是:开启一个unix线程,初始化栈和寄存器,通常情况下,除了指令指针(Intel的IP)或程序计数器(ARM的r15)之外,所有的寄存器值都为0。
- 前者作用是设置程序主线程的入口点地址和栈大小。
这条命令比LC_UNIXTHREAD命令更实用一些, 因为无论如何除了程序计数器之外所有的寄存器都设置为0了。由于没有LC_UNIXTHREAD命令, 所以不可以在之前版本的 OS X 上运行使用了LC_MAIN的二进制文件(在加载时会导致dyld(1)崩溃)。
LC_Main对应的加载命令如下,记录了可执行文件的入口函数int main(int argc, char * argv[])
的信息:
struct entry_point_command {
uint32_t cmd; / LC_MAIN only used in MH_EXECUTE filetypes /
uint32_t cmdsize; / 24 /
uint64_t entryoff; / file (__TEXT) offset of main() /
uint64_t stacksize; / if not zero, initial stack size /
};
从定义上可以看到入口函数的地址计算:Entry Point = vm_addr(__TEXT) + entryOff + Slide
从dyld的源码里能看到对Entry Point的获取和调用:
dyld
▼ __dyld_start // 源码在dyldStartup.s这个文件,用汇编实现
▼ dyldbootstrap::start() // dyldInitialization.cpp
▼ dyld::_main()
▼ //函数的最后,调用 getEntryFromLC_MAIN,从 Load Command 读取LC_MAIN入口,如果没有LC_MAIN入口,就读取LC_UNIXTHREAD,然后跳到主程序的入口处执行
namespace dyldbootstrap {
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue) {
//
// Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue) {
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
return result;
}
}
}
这里简单看一下这几种load command所表示的信息。关于进程地址空间分布、线程入口在第四节 —— 装载会从进程启动到运行详细梳理一下流程。
# 通用二进制格式(Universal Binary)
通常也被称为胖二进制格式(Fat Binary)
,Apple 提出这个概念是为了解决一些历史原因,macOS(更确切的应该说是 OS X)最早是构建于 PPC 架构智商,后来才移植到 Intel 架构(从 Mac OS X Tiger 10.4.7 开始),通用二进制格式的二进制文件可以在 PPC 和 x86 两种处理器上执行。
说到底,通用二进制格式只不过是对多架构的二进制文件的打包集合文件,而 macOS 中的多架构二进制文件也就是适配不同架构的 Mach-O 文件。即一个通用二进制格式包含了很多个 Mach-O 格式文件。它有以下特点:
- 因为需要存储多种架构的代码,所以通用二进制文件要比单架构二进制文件要大
- 因为两种种架构之间可以共用一些资源,所以两种架构的通用二进制文件大小不会达到单一架构版本的两倍。
- 运行过程中只会调用其中的部分代码,所以运行起来不会占用额外的内存
Fat Header 的数据结构在
头文件中有定义,可以参看 /usr/include/mach-o/fat.h
找到定义头:
#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
uint32_t magic; /* FAT_MAGIC 或 FAT_MAGIC_64 */
uint32_t nfat_arch; /* 结构体实例的个数 */
};
struct fat_arch {
cpu_type_t cputype; /* cpu 说明符 (int) */
cpu_subtype_t cpusubtype; /* 指定 cpu 确切型号的整数 (int) */
uint32_t offset; /* CPU 架构数据相对于当前文件开头的偏移值 */
uint32_t size; /* 数据大小 */
uint32_t align; /* 数据内润对其边界,取值为 2 的幂 */
};
对于 cputype
和 cpusubtype
两个字段这里不讲述,可以参看 /usr/include/mach/machine.h
头中对其的定义,另外 Apple 官方文档中也有简单的描述。
在 fat_header
中,magic
也就是我们之前在表中罗列的 magic 标识符,也可以类比成 UNIX 中 ELF 文件的 magic 标识。加载器会通过这个符号来判断这是什么文件,通用二进制的 magic 为 0xcafebabe
。nfat_arch
字段指明当前的通用二进制文件中包含了多少个不同架构的 Mach-O 文件。fat_header
后会跟着多个 fat_arch
,并与多个 Mach-O 文件及其描述信息(文件大小、CPU 架构、CPU 型号、内存对齐方式)相关联。
这里可以通过 file
命令来查看简要的架构信息,这里以 iOS 平台 WeChat 4.5.1 版本为例:
~ file Desktop/WeChat.app/WeChat
Desktop/WeChat.app/WeChat: Mach-O universal binary with 2 architectures: [arm_v7: Mach-O executable arm_v7] [arm64]
Desktop/WeChat.app/WeChat (for architecture armv7): Mach-O executable arm_v7
Desktop/WeChat.app/WeChat (for architecture arm64): Mach-O 64-bit executable arm64
进一步,也可以使用 otool
工具来打印其 fat_header
详细信息:
~ otool -f -V Desktop/WeChat.app/WeChat
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture armv7
cputype CPU_TYPE_ARM
cpusubtype CPU_SUBTYPE_ARM_V7
capabilities 0x0
offset 16384
size 56450224
align 2^14 (16384)
architecture arm64
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64_ALL
capabilities 0x0
offset 56475648
size 64571648
align 2^14 (16384)
之后我们用 Synalyze It! 来查看 WeChat 的 Mach64 Header 的效果:
- 从第一个段中得到
magic = 0xcafebabe
,说明是FAT_MAGIC
。 - 第二段中所存储的字段为
nfat_arch = 0x00000002
,说明该 App 中包含了两种 CPU 架构。 - 后续的则是
fat_arch
结构体中的内容,cputype(0x0000000c)
、cpusubtype(0x00000009)
、offset(0x00004000)
、size(0x03505C00)
等等。如果只含有一种 CPU 架构,是没有 fat 头定义的,这部分则可跳过,从而直接过去arch
数据。
# 参考链接
- iOS逆向学习之四(初识Mach-O)
- Mach-O 文件格式探索
- 《iOS应用逆向与安全》— 刘培庆
- 深入剖析Macho(1)