Mach-O文件介绍

在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的信息:
Mach-O文件介绍_第1张图片
可以清楚的看到,Fat Header里面的内容:
首先是fat_header,包含两种架构,后面跟着两个fat_arch结构。从图中可以看到,Fat Header之后就是两个Executable文件,也就是可执行文件,也就是Mach-O文件。

Mach-O格式

Mach-O是mac OS系统可执行文件的格式,平时用到的可执行文件,动态库,静态库,Dsym文件,都是Mach-O格式的文件。看一下苹果官方文档中对Mach-O文件的介绍:
Mach-O文件介绍_第2张图片
可以看到,一个Mach-O文件包含三部分:Header、Load commands、Data。

Mach-O Header

Mach-O头部,描述了Mach-O的cpu类型,文件类型以及加载命令大小、条数等信息。还是使用MachOview看一下python可执行文件中的Mach-O文件:
Mach-O文件介绍_第3张图片
通过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 Load commands

Mach-O Header之后就是Load commands,也就是加载命令。加载命令的作用时,在Mach-O文件被加载到内存时,加载命令告诉内核加载器或者动态链接器如何调用。还是先试用MachOview看一下Mach-O文件中的加载命令:
Mach-O文件介绍_第4张图片
可以看到,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的结构

来简单看一下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文件介绍_第5张图片
这是Mach-O文件中某个Section的信息,和section的数据结构是一一对应的。

Mach-O Data

Mach-O中Load Commands之后的就是Data数据。每个段的数据都保存在这里,这里存放了具体的数据与代码。由于Mach-O Data中的内容更多的与具体的数据有关,而与格式无关,因此就不做太多的介绍了。

总结

关于Mach-O文件格式就全部介绍完了,很多的底层知识都和Mach-O有关,包括app启动过程,符号表解析,bitcode等。了解Mach-O格式,在涉及到一些底层原理时,能帮助我们更好的理解。

你可能感兴趣的:(iOS开发)