众所周知,我们用Xcode构建一个程序过程中,会把源文件转换成一个可执行文件,这个可执行文件中包含的字节码会被CPU执行。
就像Windows上的PE和Linux上的ELF,OS X系统可执行文件的格式为Mach-O,接下来简单谈谈Mach-O。
先来看看苹果官方文档对Mach-O结构组成的描述
由图可知,Mach-O主要分为3大部分:
1.Header:用于快速确认一些关于当前文件对应的处理器,文件类型,Load connamd个数基本信息
2.Load commands:紧跟在Header之后,用于告诉加载器怎样处理二进制数据
3.Data:包含Segment中的具体信息
headers的定义在开源内核代码中可以找到(/usr/include/mach-o/loader.h)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| struct mach_header { 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 */ }; /* Constant for the magic field of the mach_header (32-bit architectures) */ #define MH_MAGIC 0xfeedface /* the mach magic number */ #define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */ /* * The 64-bit mach header appears at the very beginning of object files for * 64-bit architectures. */ 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) */ /*
|
根据mach_header和mach_header_64的定义,可以很明显的看出Headers的主要作用就是迅速确认当前文件的文件类型运行环境等。
这里找个例子,首先创建一个helloworld.c文件,
1 2 3 4 5 6
| #include <stdio.h> int main(int argc, char *argv[]) { printf("Hello World!\n"); return 0; }
|
保存后在终端后再当前目录下输入代码
clang helloworld.c
如果我们没有指定名字的话,编译器会默认把helloworld.c文件编译成名为a.out的二进制文件
我们用MachOView打开这个二进制文件,来看看headers中的详细信息(这里可以通过otool -l a.out
来查看,但看起来比较吃力)
- Magic:魔数,用于快速确认该文件用于64位还是32位,32位是0xfeedface,64位是0xfeedfacf
- Cpu Type:CPU类型,
- Cpusub Type:对应的具体类型,比如arm64、armv7
- File Type:文件类型,比如可执行文件、库文件、Dsym文件,图中代表可执行文件,如例子中的MH_EXECUTE为可执行文件
- MH_PIE:加载程序在随机的地址空间,只在MH_EXECUTE中使用
- MH_TWOLEVEL:两级空间名称
- MH_DYLDLINK:连接器linker
- dyld:动态链接器,他是苹果开源的一个项目,可以在这里下载,当内核执行LC_DYLINK时,连接器会启动,查找进程所依赖的动态库,并加载到内存中。dyld是一个用户态进程,他不属于内核部分,而是作为一个单独的开源项目被苹果维护(但是属于Darwin部分)
- 随机地址空间ASLR:进程每一次启动,地址空间都会简单的随机化,否则,如果程序每一次启动的虚拟内存镜像都是一致的,容易被黑客采取重写内存的方式来破解。
Load Commands
1 2 3
| struct load_command { uint32_t cmd; /* type of load command */ uint32_t cmdsize; /* total size of command in bytes */
|
Load Commands直接跟在headers后面,加载headers之后通过解析LoadCommands来加载接下来的数据,LC_SEGMENT_64和LC_SEGMENT是加载的主要命令,他们负责指导内核设置进程的内存空间
关于具体如何通过load_commands的cmd字段来加载mach-o 文件,有兴趣可以去/usr/include/mach-o 下面看看源码,这里只要知道根据cmd字段的类型不同,使用了不同的函数来加载,基本看语义都可以解决。
这里大概讲一下前面这些段分别代表的含义,_TEXT对应的就是代码段,_DATA对应的是可读或可写的数据,_LINKEDIT是支持dyld的,里面包含符号表等数据。
Section
一个可执行文件包含多个段,也就是多个 section。可执行文件不同的部分将加载进不同的section,并且不同的section会转换进不同的segment中。这个概念对于所有的可执行文件都是成立的。
参考文章:https://objccn.io/issue-6-3/