简介
MachO文件是mac平台上一类文件的简称,它的类型有以下种类,可以在#import <mach-o/loader.h>文件中找到
1 | #define MH_OBJECT 0x1 /* relocatable object file */ |
列举一些常见的类型
| 文件类型 | 含义 |
|---|---|
| MH_OBJECT | 目标文件,平时.o结尾的文件 |
| MH_EXECUTE | 可执行文件,我们平时编译后的包中的执行文件 |
| MH_DYLIB | 一些动态库,该文件夹下很多/usr/lib/xxx.dylib |
| MH_DSYM | 符号文件,编译成功后XXX.app.dSYM |
一、MachO的分类
这里准备了一些macho文件,分别通过MachOView工具来看看,如果你的MachOView会崩溃,点击下载这个

分别用MachOView打开如下




二、MachO的组成

每个Macho文件都会有个Header对这个Macho进行整体描述,这个header根据你打包的选择的架构又分为Fat Header 和 Mach Header,先介绍下如何生成这2个文件类型

Architectures和valid Architectures 的交集就是最后打的包的架构,Architectures后面是标准的架构,我把项目设置为iOS8选择真机,将build改成release,会编译出一开始的展示的MochO_arm_fat文件放到MachOView中如图

然后选择模拟器将build改成debug ,build会出现MochO_x86,结果如图

2.1 fat_header
2.1.1 fat_header 结构
1 | struct fat_header { |
magic: 描述文件类型 值分为2组分别为FAT_CIGAM(0xbebafeca)、FAT_MAGIC(0xcafebabe)和FAT_CIGAM_64(0xbfbafeca)、FAT_MAGIC_64(0xcafebabf),值也就是大小端的区别;nfat_arch:描述当前fat有多少个架构。
2.1.2 fat_arch 结构
2.1.1 说到fat_header的nfat_arch会描述有多少个架构,其实架构的类型就是fat_arch类型的,结构如下
1 | struct fat_arch { |
cputype:说明CPU的类型一般有CPU_TYPE_X86、CPU_TYPE_X86_64、CPU_TYPE_ARM64、CPU_TYPE_ARM;cpusubtype: 对cpu类型的具体划分一般有CPU_SUBTYPE_I386_ALL、CPU_SUBTYPE_X86_ALL、CPU_SUBTYPE_ARM_V7、CPU_SUBTYPE_ARM_V7S;offset: 当前架构的偏移量;size:当前架构的大小;align:对齐大小。
通过MachOView来看下MochO_arm_fat的fat_header

也可以用otool查看
otool -f /Users/fangshufeng/Desktop/thirdPart/macho/MochO/MochO/exccute/
1 | MochO_arm_fat |
上面可以看到
架构architecture 0的偏移地址是16384,也就是16进制的0x4000;
架构architecture 1的偏移地址是131072,也就是16进制的0x20000;
我们来看下是否正确


正好是要的值
2.2 mac_header
对于不是fat的Macho文件一开始的内容就是mac_header
2.2.1 结构
1 | struct mach_header { |
magic、cputype、cpusubtype:同上;filetype:Macho文件的类型,也就是文章一开始列举的类型;ncmds:接下来load commands的数量,后面会介绍;sizeofcmds:接下来load commands的大小,后面会介绍;flags:文件的表示信息,值如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/* Constants for the flags field of the mach_header */
截图如下

同样使用otool也是可以的
1 | ➜ MochO otool -h exccute/MochO_x86 |
上面显示该文件的类型为MH_EXECUTE,Load Commands的数量为21个,数一下确实是21个。

也就是说mach_header更多的是对load Commands的描述
2.3 load Commands
load Commands是由很多的LC_Type组成的,而LC_Type有很多种,可在文件loader.h文件中查看,这边就列出前几种
1 | /* Constants for the cmd field of all load commands, the type */ |
而每个LC_Type都会有一个头部load_command结构如下
1 | struct load_command { |
是的就是对segment的描述
cmd: 当前load command的类型;cmdsize:load command的大小。
也就是下面这张图

2.3.1 LC_SEGMENT
为了方便管理,程序在内存中是分段管理的,先来看看LC_Type其中一种LC_SEGMENT的结构
1 | struct segment_command { /* for 32-bit architectures */ |
cmd和cmdsize: 就是上面的load_command类型;segname:就是当前segment的名称vmaddr:在虚拟内存中的地址,这个很重要的,以后会介绍到vmsize:在虚拟内存中所占用的大小;fileoff:在文件中的偏移量;filesize:在文件中的大小,注意和vmaddr、vmsize区别maxprot:表示页面所需要的最高内存保护;initprot:表示页面初始的内存保护;nsects: 当前segment有多少个sectionsflags:表示段的标志信息。
常见的LC_SEGMENT有以下几种
1 |
2.3.1.1 __PAGEZERO
这是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。这个段的大小,32位上是 0x4000,64位上0000000100000000也就是 4G,4GB 并不是文件的真实大小,但是规定了进程地址空间的前 4GB 被映射为 不可执行、不可写和不可读,是从0(也是NULL指针的位置)开始的,这就是为什么当读写一个 NULL 指针或更小的值时会得到一个 EXC_BAD_ACCESS 错误。
内容如下

2.3.1.2 __TEXT
这是程序的代码段,该段是可读可执行,但是不可写。常见的section如下

2.3.1.3 __DATA
数据段,包含了可读写数据。常见的section如下

2.3.2 LC_DYLD_INFO_ONLY
LC_DYLD_INFO_ONLY和LC_DYLD_INFO是同一个结构
1 | struct dyld_info_command { |
这个command是dyld在将二进制文件装载到内存链接的时候使用的
- 前面2个不介绍了,
rebase:由于Macho被加载到内存的时候首地址不是固定的,是随机分配的,针对这个做修正的; bind:在链接的时候对一些符号进行绑定的,比如我们用到了UIKIT框架的api,但是二进制中又没有这个符号,此刻就是做这个对应的工作;lazy_bind:就是一开始不必要立即绑定,后面用到的时候再绑定。
内容如下
可以通过偏移量找到对应的地方

2.3.3 LC_SYMTAB
这里面记录着所有的符号信息
1 | struct symtab_command { |
symoff:符号表的偏移量;nsyms:符号表的元素的数量;stroff:符号的字符串的偏移量;strsize:所占的字节数。

2.3.3.1 查看Symbol Table
上面说到symbol的偏移量为21568也就是0x0000 5440
选择如下

看到下图

Symbol Table装着都是结构nlist_64或者nlist可以see <mach-o/nlist.h>
1 | struct nlist_64 { |
n_strx: 在String Table中的索引值;n_type: 可选的值有N_STAB、N_PEXT、N_TYPE、N_EXT;n_sect:section的类型,要么就是NO_SECT;n_desc:n_value: 符号对应的地址
这里以AppDelegate的符号_OBJC_CLASS_$_AppDelegate来演示

根据图得出以下信息:
n_sect显示位于__DATA,__objct_data;value显示地址为0x100003F48。
跳到对应的地址看到确实是我们要找的:

具体的数据如图

读出以下信息:
_OBJC_CLASS_$_AppDelegate的isa是_OBJC_METACLASS_$_AppDelegate;- 父类是
UIResponder - 此时的缓存是空
- 缓存的数量为0
- 当前类相关的信息在地址
0x100003DC0;
到这是不是觉得特别熟悉呢,我们把AppDelegate的代码用c++看下
1 |
|
可以看到
1 | extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_AppDelegate __attribute__ ((used, section ("__DATA,__objc_data"))) = { |
和我们的工具看到的不谋而合
我们继续跳到0x100003DC0看下

确实看到了我们要的信息,而右边的又是什么呢,由刚才的C++代码可以知道0x100003DC0就是_OBJC_CLASS_RO_$_AppDelegate的地址
看下_class_ro_t的结构
1 | struct _class_ro_t { |
这个结构也即是我们截图的内容真好匹配
1 |
|
所以到这里我们可以知道该类的所有信息了,比如我们想看看它得方法列表,由截图可以知道地址为0x0000000100003C60

每一个item对应的就是_objc_method
1 | struct _objc_method { |
比如我们现在想到拿到方法- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions的名称,看图可以知道方法的字符串地址为0000000100001C90

果然找到了,其它的信息可以自己尝试去找找。更多关于类方面的知识可以看下我之前的oc主题相关的文章,现在文章已经很长了,不说这个了。
2.3.3.2 查看String Table

2.3.4 LC_DYSYMTAB
这里记录着所有的动态链接时需要的符号信息

同样我们找到00005C10

还有很多没有截取了,比如这些_NSFullUserName这些在链接的时候回去动态解析这些符号表
这个Indirect Symbols包含了所有和动态库相关的符号,包括__DATA,__la_symbol_ptr、__DATA,__nl_symbol_ptr、__DATA,__got,这个表有以下用处:
- 通过这个表的
Symbol可以找到在符号表Symbol Table的位置,从而在字符串表String Table中找到名称; - 通过这个表的
Indirect Address可以在__DATA,__la_symbol_ptr、__DATA,__nl_symbol_ptr、__DATA,__got中找到方法的地址
fishook就用到了这个,后面我会单独来介绍这个库的实现原理
2.3.5 LC_MAIN
指定了main函数的入口地址

加载到内存后增加头部地址就是函数的真正地址了

2.3.6 LC_LOAN_DYLIB
描述了一些动态库相关的信息

1 |
|
2.3.7 LC_RPATH
Runpath的简写
程序运行链接路径

xcode中可以看到

2.3.8 LC_FUNCTION_STARTS
方法是从哪里开始的


和解析出来的顺序也是一致的

2.3.9 LC_CODE_SINGATURE
签名相关的信息

找到地方0x67E0

关于签名的后面打算单独写一篇
2.4 section
结构如下
1 | struct section_64 { /* for 64-bit architectures */ |
这里只列举了section64位的,section可以自己在#include <mach-o/loader.h>查看
sectname:当前section的名字;segname:位于哪个segment;addr:当前section在内存中的地址;size:当前的section所占的内存大小;offset:当前section的偏移量;reloff: 抱歉暂时没找到实际的用处,不做解释,以免误人子弟;nreloc:这个就是表示上面reloff的数量;flags: 这个是当前section的标志位,包括sectionType和sectionAttribute,一个section可以有多个属性,但是只能有一个类型,这个很好理解了,可以通过位运算分别获取类型和属性,(section->flags & SECTION_TYPE、section->flags & SECTION_ATTRIBUTESreserved1:这是个保留字段,它可以表示偏移量也可以用来表示索引,一般用来表示Indirect Symbol Index也就是间接索引表的位置,你可以在__got、__subs等中可以查看;reserved3:也是个保留字段,一般表示数量的,比如在__subssection中就表示subs的个数;reserved3:这个真是个保留字段了,暂时没什么用处
随意截取一个section看下结构吧

本篇完。