理解Mach-O文件格式(1)
写在之前
之前工作中对Mach-O文件有一定的接触, 原本早就想写一篇文章分享一下,但是奈何只是不够深入, 总怕分析的有问题误导读者。
最近又在阅读深入解析Mac OS X 与iOS 操作系统,借着这个机会记录下自己的学习成果, 并结合之前的经验, 加上一些实例让读者更好的理解。
毕竟对于程序员来说 大部分人对抽象的概念的感觉就是 听说过很多原理, 依然不知道大佬说的是什么
Mac OS 与 iOS 支持的文件类型
在Unix-Like
系列的操作系统, 可以通过命令 chmod +x
给予文件可执行权限, 但是这不代表这个文件具有可执行权限, 实际上 Apple家的操作系统只支持三种文件格式。
- 以
#!
开头的脚本文件 - 通用二进制文件
- Mach-O格式文件
但是实际上 以#!
开头的脚本文件其实是shell解释器找到后面指定的脚本解释器来执行的, 而通用二进制文件其实是多个架构的Mach-O文件的打包体。
通用二进制文件其实有个更加形象化的名字fat binary
那么操作系统如何知道你打开的文件是何种类型的?
其实是通过这些文件头的固定数字来区分的, 对于这些固定数字通常叫做 Magic Number
(魔数).
对于fat binary
的魔数是 0xcafebabe
(小端)0xbebafeca
大端
对于Mach-O
的魔数是 0xfeedface
(32位) 0xfeedfacf
(64位)
多说无益~~上代码
我们以/usr/bin/perl为例 (这是一个fat binary)
$ file /usr/bin/perl
/usr/bin/perl: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [i386:Mach-O executable i386]
/usr/bin/perl (for architecture x86_64): Mach-O 64-bit executable x86_64
/usr/bin/perl (for architecture i386): Mach-O executable i386
$ otool -vh /usr/bin/perl
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 17 1800 NOUNDEFS DYLDLINK TWOLEVEL PIE
不过可能你觉得拿着系统的命令来看感觉不那么真实, 那么cat命令我们都用过吧,来看下
在/usr/include/mach-o/fat.h
路径下有关于fat binary
文件的头文件定义
struct fat_header {
uint32_t magic; /* FAT_MAGIC or FAT_MAGIC_64 */
uint32_t nfat_arch; /* 包含的架构数 */
};
struct fat_arch {
cpu_type_t cputype; /* cpu类型 */
cpu_subtype_t cpusubtype; /* 机器标示符 */
uint32_t offset; /* 当前架构在这个文件中的便宜量 */
uint32_t size; /* 当前架构在文件中的长度*/
uint32_t align; /* 对齐方式 */
};
不知道大家还记得不记得之前使用windows的时候有System32和64之分, 那是因为在windows操作系统中不同架构的可执行文件是分开存放的。
苹果在某次WWDC大会声称自己优雅的将多个架构合并在了一个文件中。引来果粉一阵鼓掌。
其实fat binary
文件的真正布局非常简单。
以/usr/bin/perl为例
Apple的实现只是将不同架构的文件并排放在一起,然后在文件头部添加不同架构的描述信息, 然后再加载当前架构的Mach-O文件 丢弃掉其他架构的部分即可。实在是简单粗暴~~
Mach-O文件结构
Unix标准了一个可移植的二进制格式ELF
但是苹果并没有实现它而是维护了一套NeXTSTEP的遗物 Mach-Object
简称Mach-O
。
但是这并不是说苹果不遵守POSXI
规范,这个规范通常说的是源码级别的跨平台性,对于二进制则不强制要求。
下面是一个官方提供的图片。
Mach-0 Header
先来介绍Mach-O的Header(只介绍64位)信息。
相关头文件定义在/usr/include/mach-o/loader.h
里面。如果需要使用只需要加载<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; /* 文件类型 */
uint32_t ncmds; /* load commadns的个数 */
uint32_t sizeofcmds; /* load commands的总大小 */
uint32_t flags; /* 动态连接器标志*/
uint32_t reserved; /* 保留*/
};
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* 小端 */
#define MH_CIGAM_64 0xcffaedfe /* 大端 */
注: Mach-O文件不仅仅是可执行文件, 也包括目标文件(.o) 动态库, Bundle插件等。
标志位
flag 标记了一些dyld加载 执行 中可配置的信息。
关于Mach-O文件的魔数信息,有兴趣的读者可以按照之前的方式亲自动手尝试一下
Mach-O Load commands
Mach-O文件中最重要的元信息就是 load Commands,加载命令紧跟在文件头信息之后。
// [_mach_header_|___load_commands___||___load_commands___||____other____|]
struct load_command {
uint32_t cmd; /* load command的类型 */
uint32_t cmdsize; /* command 的长度 */
};
LC_SEGMENT
对于加载命令是LC_SEGMENT的命令指定了内核如何设置新运行的进程的内存空间
对应的头文件也在<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; /* 当前segment加载的虚拟内存起始地址 */
uint64_t vmsize; /* 当前segment加载的虚拟内存地址占用的长度 */
uint64_t fileoff; /* segment在文件中的偏移 */
uint64_t filesize; /* segment在文件中的长度 */
vm_prot_t maxprot; /* 最大的保护级别 */
vm_prot_t initprot; /* 初始化的保护级别 */
uint32_t nsects; /* 包含sections的个数 */
uint32_t flags; /* 标志位 */
};
由于有了LC_SEGMENT命令。对于每一个Segment,将文件中偏移量为fileOff长度为filesize的文件内容加载到虚拟地址为vmaddr的位置,长度为vmsize, 页面的权限通过initprot来初始化(比如设定读/写/执行, 段的保护级别可以动态设置最大不超过maxprot
常见的Segment有以下几个
- __TEXT 代码段
- __PAGEZERO 空指针陷阱
- __DATA 数据段
- __LINKEDIT 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。
- __OBJC(现已经被合并到__DATA部分)包含会被Objective Runtime使用到的一些数据。
当然读者如果有兴趣查看其他所有的loadcommands可以去loader.h头文件定义去查看,也可以实际操练一下
如 使用otool 查看某些mach-O文件的所有load_commands
otool -l /bin/ls
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 */
};
对于__TEXT, __DATA下面, 又有细分的各种Section,常见的如
名称 | 作用 |
---|---|
TEXT.text | 只有可执行的机器码 |
TEXT.cstring | 硬编码去重后的C字符串 |
TEXT.const | 初始化过的常量 |
DATA.data | 初始化过的可变的数据 |
DATA.bss | 没有初始化的静态变量 |
DATA.common | 没有初始化过的符号声明 |
DATA.objc_clasname | oc类名称 |
DATA.objc_classlist | 类列表 |
DATA.objc_protocollist | 协议列表 |
···
其他的就不一一列举,建议读者亲自动手试一试, 会发现很多有价值的东西
了解这些有什么用?
相信看了这些内容, 你已经大致知道Mach-O文件的物理布局, 那么我们知道了这个文件格式能用来做什么呢?
理解了这个可以用来做下面一些东西:
- 依赖解耦
- 元信息获取
- 调试代码
- CI工具插件检测
- 逆向
相关一些示例放在下篇文章讲解。