Mach-O文件

通用二进制(Universal binary)文件

macOS系统一路走来,支持的CPU及硬件平台都有了很大的变化,从早期的PowerPC平台,到后来的x86,再到如今主流的arm、x86-64平台。软件开发人员为了作到不一样硬件平台的兼容性,若是须要为每个平台编译一个可执行文件,这将是很是繁琐的。为了解决软件在多个硬件平台上的兼容性问题,苹果开发了一个通用的二进制文件格式(Universal Binary)。 又称为胖二进制(Fat Binary),通用二进制文件中将多个支持不一样CPU架构的二进制文件打包成一个文件,系统在加载运行该程序时,会根据通用二进制文件中提供的多个架构来与当前系统平台作匹配,运行适合当前系统的那个版本。有人或许会好奇,不是讲Mach-O文件吗?怎么开始讲通用二进制文件,不要着急,看下面file命令查看dyld的打印,universal binary前面不就是Mach-O吗html

苹果自家系统中存在着不少通用二进制文件。好比/usr/lib/dyld,在终端中执行file命令能够查看它的信息:git

$ file /usr/lib/dyld
/usr/lib/dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
/usr/lib/dyld (for architecture x86_64):	Mach-O 64-bit dynamic linker x86_64
/usr/lib/dyld (for architecture i386):	Mach-O dynamic linker i386
复制代码

咱们在Xcode中能够经过设置Build Settings中的Architectures来生成兼容各类架构的APP image.png 编译以后,使用file命令查看生成的ipa包里的可执行文件 image.pnggithub

系统提供了一个命令行工具lipo来操做通用二进制文件。它能够添加、提取、删除以及替换通用二进制文件中特定架构的二进制版本。数组

查看通用二进制文件信息:lipo -info testruby

提取test中armv7版本的二进制文件能够执行:lipo test -extract armv7 -output test_armv7markdown

提取test中arm64版本的二进制文件能够执行:lipo test -extract arm64 -output test_arm64架构

合并test_armv7和test_arm64:lipo -create test_armv7 test_arm64 -output test0app

删除test中armv7s版本的二进制文件能够执行:lipo test -remove armv7s -output test1socket

通用二进制的"通用"不止针对能够直接运行的能够执行文件,系统中的动态库.dylib文件,静态库.a文件以及Framework等均可以是通用二进制文件,对它们一样也可使用lipo命令来进行管理;ide

接下来打开咱们的Xcode,按command + shift + o输入mach-o/fat.h就能够看到对通用二进制文件格式的声明,从文件的命名和声明来看,将通用二进制叫做胖二进制或许更合适;胖二进制的头部定义以下: Snip20210627_130.png magic字段被定义为常量FAT_MAGIC,它的取值是固定的0xcafebabe,表示这是一个通用二进制文件;这里要说一下字节序,计算机硬件有两种储存数据的方式,分别为大端字节序,和小端字节序,大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。小端字节序:低位字节在前,高位字节在后,是大多数机器读取数据的方式,以下图所示
bg2016112201.gif
nfat_arch字段表示后面的Mach-O文件的数量
每一个通用二进制架构信息都使用fat_arch结构体表示,在fat_header结构体以后,紧接着的是一个或多个连续的fat_arch结构体,它的定义以下: image.png cputype字段是cpu说明符,类型是cpu_type_t,定义在<mach/machine.h>文件,使用一样的command + shift + o而后输入头文件的方法能够打开<mach/machine.h>文件,在macOS上取值通常为CPU_TYPE_I386CPU_TYPE_X86_64,在iOS平台上通常是CPU_TYPE_ARMCPU_TYPE_ARM64
cpu_subtype字段是机器说明符,类型是cpu_subtype_t,一样定义在<mach/machine.h>文件,macOS上通常是CPU_SUBTYPE_I386_ALL,CPU_SUBTYPE_X86_64_ALL,在iOS上通常则是CPU_SUBTYPE_ARM64_ALL,CPU_SUBTYPE_ARM_V7
offset字段指明了当前Mach-O数据相对于当前文件开头的偏移值
size字段指明了数据的大小
align字段指明了数据的内存对齐边界,取值必须是2的n次方,它确保了当前cpu架构的目标文件加载到内存中时,数据是通过内存优化对齐的

使用MachOView能够十分清楚的看到这些信息 image.png 在fat_arch结构体往下就是具体的Mach-O格式文件了,它的内容复杂得多,将在下一小节进行讨论。

Mach-O文件

简介

到底什么是Mach-O文件我翻阅了网上无数的文章,几乎没有人给出明确的答案,我这里给出我本身的理解,只要是符合某种特定结构或者说格式的二进制文件均可以称之为Mach-O文件,也能够说Mach-O文件是一种有着特定结构的二进制文件,这个特定的结构咱们后面会讲到,熟悉Mach-O文件格式,有助于了解苹果软件底层运行机制,更好的掌握dyld加载Mach-O的步骤,为本身动手开发Mach-O相关的加解密工具打下基础

  1. MacOS上的可执行文件是一种Mach-O文件(好比ruby,phtyon...),但不是全部可执行文件都是Mach-O文件
  2. 库文件是一种Mach-O文件,动态库.dylib,静态库.a,还有Framework都是一种Mach-O文件
  3. .o文件(clang编译c源文件获得的)是一种Mach-O文件
  4. .dsym文件(符号表)也是一种Mach-O文件
  5. dyld也是一种Mach-O文件

以上这些都属于Mach-O文件,固然除了以上这五种,还有其余类型的Mach-O文件,只是这五种比较常见...其余还有八种,其余八种会在下面对Mach-O文件结构的介绍中提到

从上面MachOView的截图中能够看到,test文件内有4种不一样架构的文件,每种架构的文件均可以称它为一个Mach-O文件,而刚刚所讲的通用二进制文件就是一个文件若是包含了1种以上的Mach-O文件,那么他就是通用二进制文件

咱们知道了Mach-O文件就是一堆有着特定结构的二进制数据,那么咱们如何从这一堆的二进制里获取咱们所须要的数据?若是作过股票行情APP,IM通信底层SDK或者说使用过socket长链接对二进制数据进行过处理,发送,接收的同窗,必定会知道对一堆的二进制如何有效的处理,提取咱们想要的数据的;以我曾经作过的一款股票行情软件为例,里面就定义了大量的结构体类型,用结构体来对二进制数据进行解析,获得咱们想要的数据,那么这个Mach-O文件的解析有没有对应的结构体呢?固然有,在Xcode中使用command + shift + o搜索mach-o/loader.h就会发现一堆的结构体,这些结构体都是系统用来解析Mach-O文件的,咱们也能从中获取到很多的信息

结构

一个典型的Mach-O文件结构以下图所示: v2-35f7008ce676b29129f9ec8bed3c464f_r.png 从图中能够了解到一个Mach-O文件的结构包括Header,Load commands和Data

  • Header: 描述了Mach-O的cpu架构、文件类型以及加载命令等信息。
  • Load commands: 描述了文件中数据的具体组织结构,不一样的数据类型使用不一样的加载命令表示
  • Data: 每一个段(segment)都有一个或多个Section,它们存放了具体的数据与代码。

Header

可使用otool命令来查看Mach-O文件的头部信息 image.png 这个部分的定义,能够经过在Xcode中,按command + shift + o输入mach-o/loader.h的方式找到 image.png

  • magic在截图中都能看到的宏定义,对32位架构的程序来讲,它的值就是0xfeedface,可使用MH_MAGIC宏代替;对64位架构的程序来讲,它的值就是0xfeedfacf,对应的宏MH_MAGIC_64
  • cputype和上一节中所讲的fat_header结构体的含义彻底相同
  • cpusubtype同上
  • filetype表示Mach-O文件的具体类型,值有下图所示的12种,常见的有MH_EXECUTE(可执行文件),MH_DYLIB(动态库),MH_DYLINKER(动态链接器),MH_DSYM(符号表文件)

image.png

  • ncmdsload commands的数量
  • sizeofcmds全部load commands的占的字节数
  • flags标记,值比较多,最好去头文件中查看详细说明
#define	MH_NOUNDEFS	0x1		/* the object file has no undefined
					   references */
#define	MH_INCRLINK	0x2		/* the object file is the output of an
					   incremental link against a base file
					   and can't be link edited again */
#define MH_DYLDLINK	0x4		/* the object file is input for the
					   dynamic linker and can't be staticly
					   link edited again */
#define MH_BINDATLOAD	0x8		/* the object file's undefined
					   references are bound by the dynamic
					   linker when loaded. */
#define MH_PREBOUND	0x10		/* the file has its dynamic undefined
					   references prebound. */
......
复制代码
  • reserved这个字段只在64位架构的Mach-O文件中才有,目前它的取值系统保留

使用MachOView查看Header的信息 image.png

Load Commands

Load Commands描述的是文件的加载信息,加载信息有不少,加载的段、符号表、动态库信息等都在Commands中取到。这个部分信息仍是比较有用的,咱们能够从这里获取到符号表和字符串表的偏移量,下文中会有详细的解释。

Load Commands加载命令紧跟在Header以后,全部加载命令的前两个字段必须是cmd和cmdsize,cmd字段用该命令类型的常量填充,头文件中定义了许多的宏用于该字段,每一个命令类型都有一个特定的结构;cmdsize字段是以字节为单位的特定加载命令结构的大小,再加上它后面做为加载命令一部分的任何内容(即节结构、字符串等)要前进到下一个加载命令,能够将cmdsize加上当前加载命令的偏移量 image.png cmd字段的取值有目前有50多种,太多了就不所有粘贴出来了...

#define LC_REQ_DYLD 0x80000000

/* Constants for the cmd field of all load commands, the type */
#define	LC_SEGMENT	0x1	/* segment of this file to be mapped */
#define	LC_SYMTAB	0x2	/* link-edit stab symbol table info */
#define	LC_SYMSEG	0x3	/* link-edit gdb symbol table info (obsolete) */
#define	LC_THREAD	0x4	/* thread */
#define	LC_UNIXTHREAD	0x5	/* unix thread (includes a stack) */
#define	LC_LOADFVMLIB	0x6	/* load a specified fixed VM shared library */
#define	LC_IDFVMLIB	0x7	/* fixed VM shared library identification */
#define	LC_IDENT	0x8	/* object identification info (obsolete) */
......
复制代码

全部的这些加载命令由系统内核加载器直接使用,或由动态连接器处理。其中几个常见的加载命令有LC_LOAD_DYLIBLC_SEGMENTLC_MAINLC_CODE_SIGNATURELC_ENCRYPTION_INFO等,下面介绍其中的几个

LC_LOAD_DYLIB

LC_LOAD_DYLIB:表示这是一个须要动态加载的连接库。它使用dylib_command结构体表示。定义以下:

struct dylib_command {
	uint32_t	cmd;		/* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
					   LC_REEXPORT_DYLIB */
	uint32_t	cmdsize;	/* includes pathname string */
	struct dylib	dylib;		/* the library identification */
};
复制代码

当cmd类型是LC_ID_DYLIB,LC_LOAD_DYLIB,LC_LOAD_WEAK_DYLIB,LC_REEXPORT_DYLIB时,都使用dylib_command结构体表示;其中dylib结构体存储要加载的动态库的具体信息以下

struct dylib {
    union lc_str  name;			/* library's path name */
    uint32_t timestamp;			/* library's build time stamp */
    uint32_t current_version;		/* library's current version number */
    uint32_t compatibility_version;	/* library's compatibility vers number*/
};
复制代码

name字段是连接库的完整路径,动态连接器在加载库时,通用此路径来进行加载它。
timestamp字段描述了库构建时的时间戳。
current_versioncompatibility_version指明了前当版本与兼容的版本号
若是你看了个人上一篇文章代码注入里面提到了yololib,这个工具的原理基本就是利用这条LC_LOAD_DYLIB加载命令的相关信息实现的

LC_MAIN

LC_MAIN: 此加载命令记录了可执行文件的主函数main()的位置。它使用entry_point_command结构体表示。定义以下:

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 */
};
复制代码

entryoff字段中就指定了main()函数的文件偏移。stacksize指定了初始的堆栈大小。

LC_SEGMENT/LC_SEGMENT_64

LC_SEGMENT/LC_SEGMENT_64:段加载命令,描述了32位或64位Mach-O文件的段的信息,,常见的段有__PAGEZERO,__TEXT,__DATA,__LINKEDIT,__PAGEZERO是一个空段,它位于文件起始段的位置,__TEXT__DATA分别是文本段和数据段,分别存储了代码信息和数据信息,__LINKEDIT是连接信息段;段(segment)又能够细分为section,每一个段(segment)能够包含多个section

段使用segment_command结构体来表示,它的定义以下:

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 */
};
复制代码

segname字段是一个16字节大小的空间,用来存储段的名称,好比__TEXT...
vmaddr字段指明了段要加载的虚拟内存地址
vmsize字段指明了段所占的虚拟内存的大小
fileoff字段指明了段数据所在文件中偏移地址
filesize字段指明了段数据实际的大小
maxprot字段指明了页面所须要的最高内存保护
initprot字段指明了页面初始的内存保护
nsects字段指明了段所包含的节区(section)
flags字段指明了段的标志信息
还有不少Load Commands加载命令,这里就不一一介绍了...贴一个图大概了解下 image.png

使用MachOView查看Load Commands的内容 image.png

Data

数据区,除了Header和Load Commands外全部的原始数据。Load Commands是对数据的汇总提示,而数据区则是真实的数据。Load Commands与数据区的关系就像书的目录与章节的关系,如图所示,Segment为__TEXT的段里,显示有8个section,每一个section具体的内容就在Data区里了 image.png 接下里介绍几个比较重要的section

(__TEXT,__text)

这里存放的是汇编后的代码,当咱们进行编译时,每一个.m文件会通过预编译->编译->汇编造成.o文件,称之为目标文件。汇编后,全部的代码会造成汇编指令存储在.o文件的(__TEXT,__text)区((__DATA,__data)也是相似)。连接后,全部的.o文件会合并成一个文件,全部.o文件的(__TEXT,__text)数据都会按连接顺序存放到应用文件的(__TEXT,__text)中。 image.png

(__TEXT,__objc_methname)

这里存放了项目里,全部咱们用Objective-C写的方法名 image.png

(__TEXT,__objc_classname)

这里存放了项目里全部Objective-C类的名字 image.png class-dump工具可以解析出每一个类的方法,属性,成员变量,应该就是来自上面两个section的数据了,固然这只是个人猜想,具体怎么实现的就要去看class-dump的源码了

Symbol Table

符号表,这个是重点中的重点,符号表是将地址和符号联系起来的桥梁。符号表并不能直接存储符号,而是存储符号位于字符串表的位置。 image.png

String Table

字符串表全部的变量名、函数名等,都以字符串的形式存储在字符串表中。 image.png

Dynamic Symbol Table

动态符号表存储的是动态库函数位于符号表的偏移信息。(__DATA,__la_symbol_ptr) section 能够从动态符号表中获取到该section位于符号表的索引数组。动态符号表并不存储符号信息,而是存储其位于符号表的偏移信息。Fishhook源码看起来比较复杂主要是由于hook的是动态连接的函数,索引和连接关系比较绕。可是咱们本身编写的C函数不是动态连接的,而是在编译连接后代码指令就存储在文件内部的函数,所以不会用到动态符号表。 image.png

固然,关于Mach-O文件的知识远不止这么点,可是要彻底讲清楚里面的全部内容,那估计不是这么一篇文章可以讲的清楚的,至少也得是一本书了,我也只是网上收集到的一些资料,本身写了篇总结而已
另外这篇文章借鉴和参考了如下这两篇文章: