iOS程序员的自我修养-MachO文件结构分析(二)

10,868 阅读9分钟

目录

可执行文件

进程是特殊文件在内存中加载得到的结果。那这种文件的格式必须是系统内核可以理解的,系统内核才能正确解析。

不同操作系统的可执行文件格式不同:

可执行格式 魔数 用途
PE32/PE32+ MZ Windows的可执行文件
ELF \x7FELF Linux和大部分UNIX的可执行文件和库文件
脚本 #! 主要用于shell脚本,也有一些解释器脚本使用这个格式。这是一种特殊的二进制文件格式,#! 后面指向真正的可执行文件(比如python),而脚本其它内容,都被当做输入传递给这个命令。
通用二进制格式(胖二进制格式) 0xcafebabe(小端) 包含多种架构支持的Mach-O格式,iOS和OS X支持的格式
Mach-O 0xfeedface(32位) 0xfeedfacf(64位) iOS和OS x支持的格式

系统内核将文件读入内存,然后寻找文件的头签名(魔数magic),根据magic就可以判断二进制文件的格式。

其实PE/ELF/Mach-O这三种可执行文件格式都是COFF(Common file format)格式的变种。COFF的主要贡献是目标文件里面引入了“段”的机制,不同的目标文件可以拥有不同数量和不同类型的“段”。

接下来我将介绍通用二进制文件和Mach-O文件:

通用二进制文件

为什么有通用二进制文件

为什么有了Mach-O格式了,苹果还搞通用二进制格式?因为不同CPU平台支持的指令不同,比如arm64和x86,那我们是不是可以把arm64和x86对应的Mach-O格式打包在一起,然后系统根据自己的CPU平台,选择合适的Mach-O。通用二进制格式就是多种架构的Mach-O文件“打包”在一起,所以通用二进制格式,更多被叫做胖二进制格式。

通用二进制文件格式

通用二进制格式定义在<mach-o/fat.h>中。

#define FAT_MAGIC	0xcafebabe
#define FAT_CIGAM	0xbebafeca	/* NXSwapLong(FAT_MAGIC) */

struct fat_header {
	uint32_t	magic;		/* FAT_MAGIC or FAT_MAGIC_64 */
	uint32_t	nfat_arch;	/* number of structs that follow */
};

struct fat_arch {
	cpu_type_t	cputype;	/* cpu specifier (int) */
	cpu_subtype_t	cpusubtype;	/* machine specifier (int) */
	uint32_t	offset;		/* file offset to this object file */
	uint32_t	size;		/* size of this object file */
	uint32_t	align;		/* alignment as a power of 2 */
};

通用二进制文件开始是fat_header结构体,magic可以让系统内核读取该文件时候知道是通用二进制文件;nfat_arch表明下面有多少个fat_arch结构体(也可以说这个通用二进制文件包含多少个Mach-O)。

fat_arch结构体是描述Mach-O。cputype和cpusubtype说明Mach-O适用什么平台;offset(偏移)、size(大小)和align(页对齐)可以清楚描述Mach-O二进制位于通用二进制文件哪里。

操作通用二进制文件的常用命令

file 命令查看
$ file bq   
bq: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64]
bq (for architecture armv7):	Mach-O executable arm_v7
bq (for architecture arm64):	Mach-O 64-bit executable arm64

otool 命令查看fat_header信息
$ otool -f -V bq
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 74952848
    align 2^14 (16384)
architecture arm64
    cputype CPU_TYPE_ARM64
    cpusubtype CPU_SUBTYPE_ARM64_ALL
    capabilities 0x0
    offset 74973184
    size 84135936
    align 2^14 (16384)
    
    
lipo(脂肪) 可以增、删、提取胖二进制文件中的特定架构(Mach-O)

提取特定Mach-O
lipo bq -extract armv7 -o bq_v7   

删除特定Mach-O
lipo bq -remove armv7 -o bq_v7

瘦身为Mach-O文件格式
lipo bq -thin armv7 -o bq_v7

通用二进制文件意义

从上面可以知道,尽管通用二进制文件会占用大量的磁盘空间,但是系统会挑选合适的Mach-O来执行,不相关的架构代码不会占用内存空间,且执行效率高了。

挑选合适的Mach-O的函数定义在<mach-o/arch.h>中,NXGetLocalArchInfo()函数获得主机的架构信息,NXFindBestFatArch()函数匹配最合适的Mach-O。

Mach-O文件

网上很多介绍Mach-O格式的文章,但是大篇幅都是介绍各种加载命令,让刚接触Mach-O的读者一上来就懵逼了,以为掌握Mach-O,就是记忆各种加载命令,让学习Mach-O文件格式变得枯燥且困难。

读者只需跟着我这系列文章,由浅入深,保你早日拿下Mach-O~~

Mach-O文件是什么

Mach-O文件格式就是COFF(Common file format)格式的变种。而COFF引入了“段”的机制,不同的Mach-O文件可以拥有不同数量和不同类型的“段”。Mach-O目标文件是源代码编译得到的文件,那至少文件里有机器指令、数据吧。其实除了这些之外,还有链接时候需要的一些信息,比如符号表、调试信息、字符串等。然后按照不同的信息,放在不同的“段”(segment)中。机器指令一般放在代码段里,全局变量和局部静态变量一般放在数据段里。

这里简单说下数据分段的好处,比如数据和机器指令分段:

  1. 数据和指令可以被映射到两个不同的虚拟内存区域。数据区域是可读写的,指令区域是只读可执行。那就可以方便分别设置这两个区域的操作权限。
  2. 两个区域分离,有助于提高缓存的命中率。(提高了程序的局部性)
  3. 最主要是,系统运行多个该程序的副本时,它们指令是一样的,那内存只需要保存一份指令部分,可读写的数据区域进程私有。是不是节约了内存,动态链接那篇也是讲这样的方式来节约内存。

Mach-O文件格式

从很早以前苹果官网的这个老图中,我们知道了Mach-O文件由:Header、Load Commands、Data三部分组成。

文件最开始的Header是mach_header结构体,定义在<mach-o/loader.h>。

//后面默认都讲64位操作系统的,老早就淘汰的古董机iPhone5s就是64位操作系统了。。。
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;	/* type of file */
	uint32_t	ncmds;		/* number of load commands */
	uint32_t	sizeofcmds;	/* the size of all the load commands */
	uint32_t	flags;		/* flags */
	uint32_t	reserved;	/* reserved */
};
  1. magic:0xfeedface(32位) 0xfeedfacf(64位),系统内核用来判断是否是mach-o格式
  2. cputype和cpusubtype: 作用同上面胖二进制文件里的
  3. filetype:由于可执行文件、目标文件、静态库和动态库等都是mach-o格式,所以需要filetype来说明mach-o文件是属于哪种文件。
  4. ncms:加载命令的条数 (加载命令紧跟Header之后)
  5. sizeofcmds:加载命令的大小
  6. 动态连接器(dyld)的标志,可以先不管
  7. reserved:保留字段

其中filetype常取字段有:

#define	MH_OBJECT	0x1	 目标文件	
#define	MH_EXECUTE	0x2	可执行文件	
#define	MH_DYLIB	0x6	 动态库	
#define	MH_DYLINKER	0x7	动态连接器	
#define	MH_DSYM		0xa	存储二进制文件符号信息,用于Debug分析

加载命令

进程是特殊文件在内存中加载得到的结果。那这种文件的格式必须是系统内核可以理解的,系统内核才能正确解析。 --本文最开始

上面介绍了Mach-O有不同类型的“段”,且系统内核(或链接器)需要不同的加载方式来加载对应的段,而加载命令就是指导系统内核如何加载,所以有了不同的加载命令。

为了讲清楚Mach-O格式,我仅讲一个最普通且有代表意义的加载命令:段加载命令(LC_SEGMENT_64),其它加载命令,后面篇章用到时候,再具体讲解。

LC_SEGMENT_64

// 定义在<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;		/* memory address of this segment */
	uint64_t	vmsize;		/* memory size of this segment */
	uint64_t	fileoff;	/* file offset of this segment */
	uint64_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 */
};
  1. cmd表示加载命令类型,cmdsize表示加载命令大小(还包括了紧跟其后的nsects个section的大小);需要知道的是,虽然不同加载命令的结构体不同,但是所有结构体的前两个字段都是cmd和cmdsize。这样系统在迭代所有加载命令时候,可以准确找到每个加载命令。
  2. segname:加载命令名字
  3. 从fileoff(偏移)处,取filesize字节的二进制数据,放到内存的vmaddr处的vmsize字节。(fileoff处到filesize字节的二进制数据,就是“段”)
  4. 每一个段的权限相同(或者说,编译时候,编译器把相同权限的数据放在一起,成为段),其权限根据initprot初始化,initprot指定了如何通过读/写/执行位初始化页面的保护级别。段的保护设置可以动态改变,但是不能超过maxprot中指定的值(在iOS中,+x和+w是互斥的)。
  5. nsects:段中section数量

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 */
};

加载命令如果有section,后面会紧跟nsects个section。section的header结构体是一样的。

为什么要同时存在segment和section

真的要讲清这个,需要理解虚拟内存。我这里抛砖引玉,希望读者在看Mach-O文件结构时候,也能想下为什么这么设计。

其实从链接的角度来看,Mach-O文件是按照section来存储文件的,segment只不过是把多个section打包放在一起而已;但是从Mach-O文件装载到内存的角度来看,Mach-O文件是按照segment(编译时候,编译器把相同权限的数据放在一起,成为segment)来存储的,即使一个segment里的内容小于1页空间的内存,但是还是会占用一页空间的内存,所以segment里不仅有filesize,也有vmsize,而section不需要有vmsize。

这样做,是为了节约内存,减少页面内部碎片。

查看Mach-O文件格式

  1. 命令: otool -l mach-o文件
  2. MachOView

Mach-O格式图

通过上面分析,最后给出Mach-O格式图,如果你对这个格式图有不理解地方,再回过头看看上面对应地方的分析~

引用

  1. 《程序员的自我修养-链接、装载与库》
  2. 《深入解析Mac OS X & iOS操作系统》