理解Mach O文件格式(1)

998 阅读6分钟

原文地址

写在之前

之前工作中对Mach-O文件有一定的接触, 原本早就想写一篇文章分享一下,但是奈何只是不够深入, 总怕分析的有问题误导读者。

最近又在阅读深入解析Mac OS X 与iOS 操作系统,借着这个机会记录下自己的学习成果, 并结合之前的经验, 加上一些实例让读者更好的理解。 毕竟对于程序员来说 大部分人对抽象的概念的感觉就是 听说过很多原理, 依然不知道大佬说的是什么

Mac OS 与 iOS 支持的文件类型

Unix-Like系列的操作系统, 可以通过命令 chmod +x 给予文件可执行权限, 但是这不代表这个文件具有可执行权限, 实际上 Apple家的操作系统只支持三种文件格式。

  1. #!开头的脚本文件
  2. 通用二进制文件
  3. Mach-O格式文件

但是实际上 以#!开头的脚本文件其实是shell解释器找到后面指定的脚本解释器来执行的, 而通用二进制文件其实是多个架构的Mach-O文件的打包体。 通用二进制文件其实有个更加形象化的名字fat binary 那么操作系统如何知道你打开的文件是何种类型的? 其实是通过这些文件头的固定数字来区分的, 对于这些固定数字通常叫做 Magic Number(魔数).

对于fat binary的魔数是 0xcafebabe(小端)0xbebafeca大端 对于Mach-O的魔数是 0xfeedface(32位) 0xfeedfacf(64位)

多说无益~~上代码

我们以/usr/bin/perl为例 (这是一个fat binary)
$ file /usr/bin/perl
/usr/bin/perl: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [i386:Mach-O executable i386]
/usr/bin/perl (for architecture x86_64):	Mach-O 64-bit executable x86_64
/usr/bin/perl (for architecture i386):	Mach-O executable i386
$ otool -vh /usr/bin/perl
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL LIB64     EXECUTE    17       1800   NOUNDEFS DYLDLINK TWOLEVEL PIE

不过可能你觉得拿着系统的命令来看感觉不那么真实, 那么cat命令我们都用过吧,来看下

/usr/include/mach-o/fat.h路径下有关于fat binary文件的头文件定义

struct fat_header {
	uint32_t	magic;		/* FAT_MAGIC or FAT_MAGIC_64 */
	uint32_t	nfat_arch;	/* 包含的架构数 */
};

struct fat_arch {
	cpu_type_t	cputype;	/* cpu类型 */
	cpu_subtype_t	cpusubtype;	/* 机器标示符  */
	uint32_t	offset;		/* 当前架构在这个文件中的便宜量 */
	uint32_t	size;		/* 当前架构在文件中的长度*/
	uint32_t	align;		/* 对齐方式 */
};

不知道大家还记得不记得之前使用windows的时候有System32和64之分, 那是因为在windows操作系统中不同架构的可执行文件是分开存放的。

苹果在某次WWDC大会声称自己优雅的将多个架构合并在了一个文件中。引来果粉一阵鼓掌。 其实fat binary文件的真正布局非常简单。

以/usr/bin/perl为例

Apple的实现只是将不同架构的文件并排放在一起,然后在文件头部添加不同架构的描述信息, 然后再加载当前架构的Mach-O文件 丢弃掉其他架构的部分即可。实在是简单粗暴~~

Mach-O文件结构

Unix标准了一个可移植的二进制格式ELF但是苹果并没有实现它而是维护了一套NeXTSTEP的遗物 Mach-Object简称Mach-O。 但是这并不是说苹果不遵守POSXI规范,这个规范通常说的是源码级别的跨平台性,对于二进制则不强制要求。

下面是一个官方提供的图片。

Mach-0 Header

先来介绍Mach-O的Header(只介绍64位)信息。 相关头文件定义在/usr/include/mach-o/loader.h里面。如果需要使用只需要加载<mach-O/loader.h>

struct mach_header_64 {
	uint32_t	magic;		/* mach magic number identifier */
	cpu_type_t	cputype;	/* cpu specifier */
	cpu_subtype_t	cpusubtype;	/* machine specifier */
	uint32_t	filetype;	/* 文件类型 */
	uint32_t	ncmds;		/* load commadns的个数 */
	uint32_t	sizeofcmds;	/* load commands的总大小 */
	uint32_t	flags;		/* 动态连接器标志*/
	uint32_t	reserved;	/* 保留*/
};

/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* 小端 */
#define MH_CIGAM_64 0xcffaedfe /* 大端 */


注: Mach-O文件不仅仅是可执行文件, 也包括目标文件(.o) 动态库, Bundle插件等。 标志位 flag 标记了一些dyld加载 执行 中可配置的信息。 关于Mach-O文件的魔数信息,有兴趣的读者可以按照之前的方式亲自动手尝试一下

Mach-O Load commands

Mach-O文件中最重要的元信息就是 load Commands,加载命令紧跟在文件头信息之后。

//   [_mach_header_|___load_commands___||___load_commands___||____other____|]

struct load_command {
	uint32_t cmd;		/*  load command的类型 */
	uint32_t cmdsize;	/*  command 的长度 */
};

LC_SEGMENT

对于加载命令是LC_SEGMENT的命令指定了内核如何设置新运行的进程的内存空间 对应的头文件也在<mach-o/loader.h>

struct segment_command_64 { /* for 64-bit architectures */
	uint32_t	cmd;		/* LC_SEGMENT_64 */
	uint32_t	cmdsize;	/* includes sizeof section_64 structs */
	char		segname[16];	/* segment name */
	uint64_t	vmaddr;		/* 当前segment加载的虚拟内存起始地址 */
	uint64_t	vmsize;		/* 当前segment加载的虚拟内存地址占用的长度  */
	uint64_t	fileoff;	/* segment在文件中的偏移 */
	uint64_t	filesize;	/* segment在文件中的长度 */
	vm_prot_t	maxprot;	/* 最大的保护级别 */
	vm_prot_t	initprot;	/* 初始化的保护级别 */
	uint32_t	nsects;		/* 包含sections的个数  */
	uint32_t	flags;		/* 标志位 */
};

由于有了LC_SEGMENT命令。对于每一个Segment,将文件中偏移量为fileOff长度为filesize的文件内容加载到虚拟地址为vmaddr的位置,长度为vmsize, 页面的权限通过initprot来初始化(比如设定读/写/执行, 段的保护级别可以动态设置最大不超过maxprot

常见的Segment有以下几个

  1. __TEXT 代码段
  2. __PAGEZERO 空指针陷阱
  3. __DATA 数据段
  4. __LINKEDIT 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。
  5. __OBJC(现已经被合并到__DATA部分)包含会被Objective Runtime使用到的一些数据。

当然读者如果有兴趣查看其他所有的loadcommands可以去loader.h头文件定义去查看,也可以实际操练一下 如 使用otool 查看某些mach-O文件的所有load_commands

otool -l /bin/ls

section

类型声明如下
struct section_64 { /* for 64-bit architectures */
	char		sectname[16];	/* name of this section */
	char		segname[16];	/* segment this section goes in */
	uint64_t	addr;		/* memory address of this section */
	uint64_t	size;		/* size in bytes of this section */
	uint32_t	offset;		/* file offset of this section */
	uint32_t	align;		/* section alignment (power of 2) */
	uint32_t	reloff;		/* file offset of relocation entries */
	uint32_t	nreloc;		/* number of relocation entries */
	uint32_t	flags;		/* flags (section type and attributes)*/
	uint32_t	reserved1;	/* reserved (for offset or index) */
	uint32_t	reserved2;	/* reserved (for count or sizeof) */
	uint32_t	reserved3;	/* reserved */
};

对于__TEXT, __DATA下面, 又有细分的各种Section,常见的如

名称 作用
TEXT.text 只有可执行的机器码
TEXT.cstring 硬编码去重后的C字符串
TEXT.const 初始化过的常量
DATA.data 初始化过的可变的数据
DATA.bss 没有初始化的静态变量
DATA.common 没有初始化过的符号声明
DATA.objc_clasname oc类名称
DATA.objc_classlist 类列表
DATA.objc_protocollist 协议列表

··· 其他的就不一一列举,建议读者亲自动手试一试, 会发现很多有价值的东西

了解这些有什么用?

相信看了这些内容, 你已经大致知道Mach-O文件的物理布局, 那么我们知道了这个文件格式能用来做什么呢? 理解了这个可以用来做下面一些东西:

  1. 依赖解耦
  2. 元信息获取
  3. 调试代码
  4. CI工具插件检测
  5. 逆向

相关一些示例放在下篇文章讲解。