Mach-O文件

通用二进制(Universal binary)文件

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

苹果自家系统中存在着很多通用二进制文件。比如/usr/lib/dyld,在终端中执行file命令可以查看它的信息:

$ 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.png

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

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

提取test中armv7版本的二进制文件可以执行:lipo test -extract armv7 -output test_armv7

提取test中arm64版本的二进制文件可以执行:lipo test -extract arm64 -output test_arm64

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

删除test中armv7s版本的二进制文件可以执行:lipo test -remove armv7s -output test1

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

接下来打开我们的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文件的知识远不止这么点,但是要完全讲清楚里面的所有内容,那估计不是这么一篇文章能够讲的清楚的,至少也得是一本书了,我也只是网上收集到的一些资料,自己写了篇总结而已
另外这篇文章借鉴和参考了以下这两篇文章:

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享