理解Mach-O文件格式(1)

2017-11-02 12:53:00来源:https://www.valiantcat.cn/index.php/2017/10/30/52.html作者:茶茶的小屋人点击

分享
写在之前

之前工作中对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 cpusubtypecapsfiletype ncmds sizeofcmdsflags
MH_MAGIC_64X86_64ALL LIB64 EXECUTE17 1800 NOUNDEFS DYLDLINK TWOLEVEL PIE

不过可能你觉得拿着系统的命令来看感觉不那么真实, 那么cat命令我们都用过吧,来看下



在 /usr/include/mach-o/fat.h 路径下有关于 fat binary 文件的头文件定义


struct fat_header {
uint32_tmagic;/* FAT_MAGIC or FAT_MAGIC_64 */
uint32_tnfat_arch;/* 包含的架构数 */
};
struct fat_arch {
cpu_type_tcputype;/* cpu类型 */
cpu_subtype_t cpusubtype; /* 机器标示符*/
uint32_toffset; /* 当前架构在这个文件中的便宜量 */
uint32_tsize; /* 当前架构在文件中的长度*/
uint32_talign;/* 对齐方式 */
};

不知道大家还记得不记得之前使用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_tmagic;/* mach magic number identifier */
cpu_type_tcputype;/* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_tfiletype; /* 文件类型 */
uint32_tncmds;/* load commadns的个数 */
uint32_tsizeofcmds; /* load commands的总大小 */
uint32_tflags;/* 动态连接器标志*/
uint32_treserved; /* 保留*/
};
/* 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_tcmd;/* LC_SEGMENT_64 */
uint32_tcmdsize;/* includes sizeof section_64 structs */
charsegname[16];/* segment name */
uint64_tvmaddr; /* 当前segment加载的虚拟内存起始地址 */
uint64_tvmsize; /* 当前segment加载的虚拟内存地址占用的长度*/
uint64_tfileoff;/* segment在文件中的偏移 */
uint64_tfilesize; /* segment在文件中的长度 */
vm_prot_t maxprot;/* 最大的保护级别 */
vm_prot_t initprot; /* 初始化的保护级别 */
uint32_tnsects; /* 包含sections的个数*/
uint32_tflags;/* 标志位 */
};

由于有了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 */
charsectname[16]; /* name of this section */
charsegname[16];/* segment this section goes in */
uint64_taddr; /* memory address of this section */
uint64_tsize; /* size in bytes of this section */
uint32_toffset; /* file offset of this section */
uint32_talign;/* section alignment (power of 2) */
uint32_treloff; /* file offset of relocation entries */
uint32_tnreloc; /* number of relocation entries */
uint32_tflags;/* flags (section type and attributes)*/
uint32_treserved1;/* reserved (for offset or index) */
uint32_treserved2;/* reserved (for count or sizeof) */
uint32_treserved3;/* 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|协议列表|


···


其他的就不一一列举,建议读者亲自动手试一试, 会发现很多有价值的东西


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台