在Linux操作系统上,以及很多的类Unix操作系统,可执行文件的格式是ELF。mac OS虽然也是类Unix操作系统,然而mac OS以及iOS上,可执行文件的格式是Mach-O。
先理清一个易混淆的点,Mach-O和Mac没有什么关系。Mac是苹果电脑Macintosh的简称,而Mach是一种操作系统微内核,苹果公司的设备上操作系统内核使用的是Mach。在Mach内核中,一种可执行文件格式是Mach-O。所以不要被Mach-O和Mac相似的名字迷惑了,实际上两者的关系不大。
在介绍Mach-O文件格式之前,首先了解一下通用二进制格式。
通用二进制格式(Universal Binary),又称为胖二进制(Fat Binary)。通用二进制文件实际上就是将支持不同CPU架构的二进制文件打包成一个文件,系统在加载运行时,会根据通用二进制文件中提供的架构,选择和当前系统匹配的二进制文件。因此,很多人认为,将通用二进制文件称为胖二进制文件更为合适。
mac OS中自带了很多的通用二进制文件,使用file命令可以查看这些通用二进制文件的信息,比如使用file命令查看python的信息:
file /Users/.../Desktop/python
/Desktop/python: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [i386:Mach-O executable i386]
/Desktop/python (for architecture x86_64): Mach-O 64-bit executable x86_64
/Desktop/python (for architecture i386): Mach-O executable i386
可以看到,python通用二进制文件包含两种架构的Mach-O文件,分别是x86_64架构和i386架构。
看一下代码中是如何定义通用二进制文件的,在/usr/include/mach-o目录下有通用二进制相关的文件。在fat.h中可以看到通用二进制文件头部结构fat_header的定义(从文件名的角度来看,通用二进制文件称为胖二进制文件也更为合适):
#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 */
};
magic是一个固定的值,值是0xcafebabe或0xbebafeca,表示这是一个通用二进制文件;nfat_arch表示的是该通用二进制文件包含多少个架构文件(也就是Mach-O文件)。
在fat_header之后紧跟着的是多个fat_arch结构体,fat_arch的定义如下:
struct fat_arch_64 {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint64_t offset; /* file offset to this object file */
uint64_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
uint32_t reserved; /* reserved */
};
其中,cputype指定了cpu的类型,cpusubtype指定了cpu的子类型,offset指定了该架构数据相对于文件开头的偏移量,size指定了该架构数据的大小,align指定了数据的内存对齐边界,值必须是2的次方,reserved是保留字段,只有64位架构的有,32位架构的无此字段。
cputype的部分取值有:
#define CPU_TYPE_X86 ((cpu_type_t) 7)
#define CPU_TYPE_I386 CPU_TYPE_X86
#define CPU_TYPE_X86_64 (CPU_TYPE_X86 | CPU_ARCH_ABI64)
#define CPU_TYPE_ARM ((cpu_type_t) 12)
#define CPU_TYPE_ARM64 (CPU_TYPE_ARM | CPU_ARCH_ABI64)
#define CPU_TYPE_POWERPC ((cpu_type_t) 18)
#define CPU_TYPE_POWERPC64 (CPU_TYPE_POWERPC | CPU_ARCH_ABI64
cpusubtype的部分取值有:
#define CPU_SUBTYPE_X86_ALL ((cpu_subtype_t)3)
#define CPU_SUBTYPE_X86_64_ALL ((cpu_subtype_t)3)
#define CPU_SUBTYPE_X86_ARCH1 ((cpu_subtype_t)4)
#define CPU_SUBTYPE_X86_64_H ((cpu_subtype_t)8)
使用MachOview软件看一下python的信息:
可以清楚的看到,Fat Header里面的内容:
首先是fat_header,包含两种架构,后面跟着两个fat_arch结构。从图中可以看到,Fat Header之后就是两个Executable文件,也就是可执行文件,也就是Mach-O文件。
Mach-O是mac OS系统可执行文件的格式,平时用到的可执行文件,动态库,静态库,Dsym文件,都是Mach-O格式的文件。看一下苹果官方文档中对Mach-O文件的介绍:
可以看到,一个Mach-O文件包含三部分:Header、Load commands、Data。
Mach-O头部,描述了Mach-O的cpu类型,文件类型以及加载命令大小、条数等信息。还是使用MachOview看一下python可执行文件中的Mach-O文件:
通过MachOview可以得到Mach-O header中包含的信息,包含了Magic Number、Cpu type、Cpu subtype、file type等。看一下代码中对于Mach-O header的定义,相关的代码在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; /* 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 */
};
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
magic字段是一个固定的值,为0xfeedfacf或者0xcffaedfe,表示的是这是一个Mach-O格式的文件。
cpu_type_t 和 cpu_subtype_t和上文中提到的一样,这里不再介绍。
filetype表示Mach-O文件的具体类型,它的部分取值如下:
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DSYM 0xa /* companion file with only debug sections */
这里python的Mach-O文件类型是MH_EXECUTE,如果我们查看的是Dsym文件,则文件类型是MH_DSYM。
ncmds 表示的是Mach-O文件中加载命令的数量。
sizeofcmds 表示的是Mach-O文件加载命令的大小。
flags 表示文件标志。
reserved 是保留字段,64位cpu架构特有。
Mach-O Header之后就是Load commands,也就是加载命令。加载命令的作用时,在Mach-O文件被加载到内存时,加载命令告诉内核加载器或者动态链接器如何调用。还是先试用MachOview看一下Mach-O文件中的加载命令:
可以看到,Mach-O文件中有多条加载命令,加载命令的前两个字段分别是Command和Command Size。load command的数据结构定义如下:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
cmdsize字段表示当前加载命令的大小。
cmd字段代表当前加载命令的类型,加载命令的类型不同,结构体就不同。对于不同类型的加载命令,他们都会在load_command结构体后面加上一个或者多个字段来表示自己特定的结构体信息。加载命令的类型比较多,其部分取值如下:
#define LC_SEGMENT 0x1 /* segment of this file to be mapped */
#define LC_THREAD 0x4 /* thread */
#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */
#define LC_PREPAGE 0xa /* prepage command (internal use) */
#define LC_DYSYMTAB 0xb /* dynamic link-edit symbol table info */
#define LC_LOAD_DYLIB 0xc /* load a dynamically linked shared library */
#define LC_CODE_SIGNATURE 0x1d /* local of code signature */
……
LC_SEGMENT是一个段加载命令;LC_LOAD_DYLIB表示这事一个需要动态加载的链接库;LC_CODE_SIGNATURE和签名、加密有关。
上图中看到加载命令是LC_SEGMENT_64,表示的是将64位的段映射到进程的地址空间。可以看一下段加载命令的数据结构:
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 */
};
cmd 和 cmdsize上面已经介绍过了,不再重复。
segname字段表示的是该segment的名称。
vmaddr字段表示段的虚拟内存地址。
vmsize字段表示段所占的虚拟内存的大小。
fileoff字段表示段数据在文件中的偏移。
filesize字段表示段数据的实际大小。
maxprot字段表示页面所需要的最高内存保护。
initprot字段表示页面初始的内存保护。
nsects字段表示该segment包含了多少个section(节区),一个段可以包含0个或者多个section。
flags字段是段的标志信息。
来简单看一下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 */
};
sectname字段表示该section的name。
segname字段表示该section所属的segment的segmentName。
addr字段表示该section的内存起始地址。
size字段表示该section的大小。
offset字段表示该section相对文件的偏移量。
align字段表示字节区的内存对齐边界。
reloff表示重定位信息的文件偏移。
nreloc表示重定位条目的数目。
flags是section的一些标志属性。
使用MachOview看一下Section的信息:
这是Mach-O文件中某个Section的信息,和section的数据结构是一一对应的。
Mach-O中Load Commands之后的就是Data数据。每个段的数据都保存在这里,这里存放了具体的数据与代码。由于Mach-O Data中的内容更多的与具体的数据有关,而与格式无关,因此就不做太多的介绍了。
关于Mach-O文件格式就全部介绍完了,很多的底层知识都和Mach-O有关,包括app启动过程,符号表解析,bitcode等。了解Mach-O格式,在涉及到一些底层原理时,能帮助我们更好的理解。