(二)|(二) Mach-O 文件结构

# 进程与二进制格式 # 相关工具 # 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 文件结构
文章图片
可以看的出 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-O 头(Mach Header)描述了 Mach-O 的 CPU 架构、大小端、文件类型以及加载命令等信息。它的作用是让内核在读取该文件创建虚拟进程空间的时候,检查文件的合法性以及当前硬件的特性是否能支持程序的运行。
以下只给出 64 位定义的代码,因为 32 位的区别是缺少了一个预留字段:
#define MH_MAGIC0xfeedface/* the mach magic number */ #define MH_CIGAM0xcefaedfe/* NXSwapInt(MH_MAGIC) */struct mach_header_64 { uint32_tmagic; / magic(魔数):用来确认文件的格式,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。 / cpu_type_tcputype; / CPU架构 / cpu_subtype_tcpusubtype; / CPU子版本 / uint32_tfiletype; / 文件类型,常见的Mach-O文件有:MH_OBJECT(目标文件)、MH_EXECUTABLE(可执行二进制文件)、MH_DYLIB(动态库)等等。这些文件类型定义在 loader.h 文件中同样可以找到 / uint32_tncmds; / 加载器中加载命令的数量 / uint32_tsizeofcmds; / 加载器中所有加载命令的总大小 / uint32_tflags; / dyld 加载需要的一些标志,其中MH_PIE表示启用地址空间布局随机化(ASLR)。其他的值在loader.h文件中同样可以找到 / uint32_treserved; / 64位的保留字段 / };

魔数会表明文件的格式。filetype会表明具体是什么文件类型(都是猫,也分黑猫、白猫)。
// magic:常见的魔数(Mac是小端模式) Mach-O文件。用途:macOS 的原生二进制格式 #defineMH_MAGIC0xfeedface/ 32位设备上的魔数,大端模式(符合人类阅读习惯,高位数据在前) / #defineMH_CIGAM0xcefaedfe/ 32位、小端(高位地址在后),CIGAM就是MAGIC反过来写,从命名上也可以看出端倪 / #defineMH_MAGIC_64 0xfeedfacf/ 64位、大端 / #defineMH_CIGAM_64 0xcffaedfe/ 64位、小端 /通用二进制格式FAT。用途:包含多种架构支持的二进制格式,只在 macOS 上支持。(在文章末尾简单介绍一下,有兴趣可以瞜一眼) #define FAT_MAGIC0xcafebabe #define FAT_CIGAM0xbebafeca/* NXSwapLong(FAT_MAGIC) */ #define FAT_MAGIC_640xcafebabf #define FAT_CIGAM_640xbfbafeca/* NXSwapLong(FAT_MAGIC_64) */脚本格式。用途:主要用于 shell 脚本,但是也常用语其他解释器,如 Perl, AWK 等。也就是我们常见的脚本文件中在 `#!` 标记后的字符串,即为执行命令的指令方式,以文件的 stdin 来传递命令。 魔数为 \x7FELF// filetype:常见的Mach-O格式的文件类型 #define MH_OBJECT0x1/ 可重定位的目标文件 / #define MH_EXECUTE0x2/ 可执行二进制文件 / #define MH_DYLIB0x6/ 动态绑定共享库 / #define MH_DYLINKER 0x7/ 动态链接编辑器,如dyld / #define MH_BUNDLE0x8/ 动态绑定bundle(包)文件 / #define MH_DSYM0xa/ 调试所用的符号文件 /

举例:利用otool工具查看Mach-o文件的头部
$ otool -hv bibi.decrypted Mach header magic cputype cpusubtypecapsfiletype ncmds sizeofcmdsflags MH_MAGICARMV70x00EXECUTE596016NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIEMach header magic cputype cpusubtypecapsfiletype ncmds sizeofcmdsflags MH_MAGIC_64ARM64ALL0x00EXECUTE596744NOUNDEFS 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_tcmd; / Load Command类型,这里LC_SEGMENT_64代表将文件中64位的段映射到进程的地址空间。LC_SEGMENT_64和LC_SEGMENT的结构差别不大 / uint32_tcmdsize; / 代表Load commands的大小 / charsegname[16]; / 16字节的段名称 / uint64_tvmaddr; / 段映射到虚拟地址中的内存起始地址 / uint64_tvmsize; / 段映射到虚拟地址中的内存大小 / uint64_tfileoff; / 段在当前架构(MachO)文件中的偏移量,如果是胖二进制文件,也指的是相对于当前MachO文件的偏移 / uint64_tfilesize; / 段在文件中的大小 / vm_prot_tmaxprot; / 段页面的最高内存保护,用八进制表示(4=r(read),2=w(write),1=x(execute执行权限)) / vm_prot_tinitprot; / 段页面最初始的内存保护 / uint32_tnsects; / 段(segment)包含的区(section)的个数(如果存在的话) / uint32_tflags; / 段页面标志 / };

系统将 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 { charsectname[16]; / Section 的名字 / charsegname[16]; / Section 所在的 Segment 名称 / uint64_taddr; / Section 映射到虚拟地址的偏移(所在的内存地址) / uint64_tsize; / Section 的大小 / uint32_toffset; / Section 在当前架构文件中的偏移 / uint32_talign; / Section 的内存对齐边界 (2 的次幂) / uint32_treloff; / 重定位入口的文件偏移 / uint32_tnreloc; / 重定位入口的数目 / uint32_tflags; / Section标志属性 / uint32_treserved1; / 保留字段1 (for offset or index) / uint32_treserved2; / 保留字段2 (for count or sizeof) / uint32_treserved3; / 保留字段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_funcdestructor(析构)函数 __cfstringOC字符串 __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_tcmd; / 32位的cmd值(表示类型) ,下面列举了部分 / uint32_tcmdsize; / 32位的cmdsize值(32位二进制为4的倍数,64位二进制为8的倍数) / .../ 记录命令本身的一些信息 / }//下面列举一些load command的类型(对应的cmd值),这里只列举了部分,全面的可以看源码,总共50多种load command。按照加载命令是由内核加载器、动态链接器处理分开记录。 内核加载器处理的加载命令: #defineLC_SEGMENT0x1/ 定义一个段(Segment),加载后被映射到内存中,包括里面的节(Section) / #defineLC_LOAD_DYLINKER0xe/ 默认的加载器路径。通常路径是“/usr/lib/dyld” / #defineLC_UUID0x1b/ 用于标识Mach-0文件的ID,匹配二进制文件与符号表。在分析崩溃堆栈信息能用到,通过地址在符号表中找到符号 / #define LC_CODE_SIGNATURE0x1d/ 代码签名信息 / #defineLC_ENCRYPTION_INFO_640x2C/ 文件是否加密的标志,加密内容的偏移和大小 /动态链接器处理的加载命令: #defineLC_SYMTAB0x2/ 为文件定义符号表和字符串表,在链接文件时被链接器使用,同时也用于调试器映射符号到源文件。符号表定义的本地符号仅用于调试,而已定义和未定义的 external 符号被链接器使用 / #defineLC_DYSYMTAB0xb/ 将符号表中给出符号的额外符号信息提供给动态链接器。 / #defineLC_ID_DYLIB0xd/ 依赖的动态库,包括动态库名称、当前版本号、兼容版本号。可以使用“otool-L xxx”命令查看 / #defineLC_RPATH(0x1c | LC_REQ_DYLD)/ RunpathSearchPaths,@rpath搜索的路径 / #defineLC_DYLD_INFO_ONLY(0x22 | LC_REQ_DYLD)/ 记录了有关链接的重要信息,包括在__LINKEDIT中动态链接相关信息的具体偏移和大小。ONLY表示这个加载指令是程序运行所必需的,如果旧的链接器无法识别它,程序就会出错 / #defineLC_VERSION_MIN_IPHONEOS0x25/ 系统要求的最低版本 / #defineLC_FUNCTION_STARTS0x26/ 函数起始地址表,使调试器和其他程序能很容易地看到一个地址是否在函数内 / #defineLC_MAIN(0x28 | LC_REQ_DYLD)/ 程序的入口。dyld获取该地址,然后跳转到该处执行。replacement for LC_UNIXTHREAD / #defineLC_DATA_IN_CODE0x29/ 定义在代码段内的非指令的表 / #defineLC_SOURCE_VERSION0x2A/ 构建二进制文件的源代码版本号 /

有一些命令是由内核加载器(定义在bsd/kern/mach_loader.c文件中) 直接使用的, 其他命令是由动态链接器处理的。
(二)|(二) Mach-O 文件结构
文章图片
在Mach-O文件加载解析时,多个Load Command会告诉操作系统应当如何加载文件中每个Segment的数据,对系统内核加载器和动态链接器起引导作用。(不同的数据对应不同的加载命令,可以看到segment_command_64symtab_commanddylib_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)崩溃)。
(二)|(二) Mach-O 文件结构
文章图片
LC_Main对应的加载命令如下,记录了可执行文件的入口函数int main(int argc, char * argv[])的信息:
struct entry_point_command { uint32_tcmd; / LC_MAIN only used in MH_EXECUTE filetypes / uint32_tcmdsize; / 24 / uint64_tentryoff; / file (__TEXT) offset of main() / uint64_tstacksize; / 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; } }}

【(二)|(二) Mach-O 文件结构】这里简单看一下这几种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_MAGIC0xcafebabe #define FAT_CIGAM0xbebafeca/* NXSwapLong(FAT_MAGIC) */struct fat_header { uint32_tmagic; /* FAT_MAGIC 或 FAT_MAGIC_64 */ uint32_tnfat_arch; /* 结构体实例的个数 */ }; struct fat_arch { cpu_type_tcputype; /* cpu 说明符 (int) */ cpu_subtype_tcpusubtype; /* 指定 cpu 确切型号的整数 (int) */ uint32_toffset; /* CPU 架构数据相对于当前文件开头的偏移值 */ uint32_tsize; /* 数据大小 */ uint32_talign; /* 数据内润对其边界,取值为 2 的幂 */ };

对于 cputypecpusubtype 两个字段这里不讲述,可以参看 /usr/include/mach/machine.h 头中对其的定义,另外 Apple 官方文档中也有简单的描述。
fat_header 中,magic 也就是我们之前在表中罗列的 magic 标识符,也可以类比成 UNIX 中 ELF 文件的 magic 标识。加载器会通过这个符号来判断这是什么文件,通用二进制的 magic0xcafebabenfat_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 的效果:
(二)|(二) Mach-O 文件结构
文章图片
  • 从第一个段中得到 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)

    推荐阅读