进程是特殊文件在内存中加载获得的结果。那这种文件的格式必须是系统内核能够理解的,系统内核才能正确解析。python
不一样操做系统的可执行文件格式不一样:git
可执行格式 | 魔数 | 用途 |
---|---|---|
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的主要贡献是目标文件里面引入了“段”的机制,不一样的目标文件能够拥有不一样数量和不一样类型的“段”。github
接下来我将介绍通用二进制文件和Mach-O文件:shell
为何有了Mach-O格式了,苹果还搞通用二进制格式?由于不一样CPU平台支持的指令不一样,好比arm64和x86,那咱们是否是能够把arm64和x86对应的Mach-O格式打包在一块儿,而后系统根据本身的CPU平台,选择合适的Mach-O。通用二进制格式就是多种架构的Mach-O文件“打包”在一块儿,因此通用二进制格式,更多被叫作胖二进制格式。缓存
通用二进制格式定义在<mach-o/fat.h>中。bash
#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二进制位于通用二进制文件哪里。ide
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文件格式就是COFF(Common file format)格式的变种。而COFF引入了“段”的机制,不一样的Mach-O文件能够拥有不一样数量和不一样类型的“段”。Mach-O目标文件是源代码编译获得的文件,那至少文件里有机器指令、数据吧。其实除了这些以外,还有连接时候须要的一些信息,好比符号表、调试信息、字符串等。而后按照不一样的信息,放在不一样的“段”(segment)中。机器指令通常放在代码段里,全局变量和局部静态变量通常放在数据段里。
这里简单说下数据分段的好处,好比数据和机器指令分段:
从很早之前苹果官网的这个老图中,咱们知道了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 */
};
复制代码
其中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),其它加载命令,后面篇章用到时候,再具体讲解。
// 定义在<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 */
};
复制代码
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结构体是同样的。
真的要讲清这个,须要理解虚拟内存。我这里抛砖引玉,但愿读者在看Mach-O文件结构时候,也能想下为何这么设计。
其实从连接的角度来看,Mach-O文件是按照section来存储文件的,segment只不过是把多个section打包放在一块儿而已;可是从Mach-O文件装载到内存的角度来看,Mach-O文件是按照segment(编译时候,编译器把相同权限的数据放在一块儿,成为segment)来存储的,即便一个segment里的内容小于1页空间的内存,可是仍是会占用一页空间的内存,因此segment里不只有filesize,也有vmsize,而section不须要有vmsize。
这样作,是为了节约内存,减小页面内部碎片。
经过上面分析,最后给出Mach-O格式图,若是你对这个格式图有不理解地方,再回过头看看上面对应地方的分析~