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

原文地址

写在之前

之前工作中对Mach-O文件有一定的接触, 原本早就想写一篇文章分享一下,但是奈何只是不够深入, 总怕分析的有问题误导读者。

最近又在阅读深入解析Mac OS X 与iOS 操作系统,借着这个机会记录下自己的学习成果, 并结合之前的经验, 加上一些实例让读者更好的理解。
毕竟对于程序员来说 大部分人对抽象的概念的感觉就是 听说过很多原理, 依然不知道大佬说的是什么

Mac OS 与 iOS 支持的文件类型

Unix-Like系列的操作系统, 可以通过命令 chmod +x 给予文件可执行权限, 但是这不代表这个文件具有可执行权限, 实际上 Apple家的操作系统只支持三种文件格式。

  1. #!开头的脚本文件
  2. 通用二进制文件
  3. 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有以下几个

  1. __TEXT 代码段
  2. __PAGEZERO 空指针陷阱
  3. __DATA 数据段
  4. __LINKEDIT 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。
  5. __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文件的物理布局, 那么我们知道了这个文件格式能用来做什么呢?
理解了这个可以用来做下面一些东西:

  1. 依赖解耦
  2. 元信息获取
  3. 调试代码
  4. CI工具插件检测
  5. 逆向

相关一些示例放在下篇文章讲解。

推荐阅读更多精彩内容

  • Mach-O 概述 和 部分命令介绍 我们知道Windows下的文件都是PE文件,同样在OS X和iOS中可执行文...
    青花瓷的平方阅读 13,407评论 2 50
  • 熟悉Linux和windows开发的同学都知道,ELF是Linux下可执行文件的格式,PE32/PE32+是win...
    Klaus_J阅读 2,396评论 1 10
  • linux资料总章2.1 1.0写的不好抱歉 但是2.0已经改了很多 但是错误还是无法避免 以后资料会慢慢更新 大...
    O感悟人生O阅读 8,575评论 2 29
  • 本文所读的源码,可以从这里找到,这是 Mach-O 系列的第一篇 我们的程序想要跑起来,肯定它的可执行文件格式要被...
    Joy___阅读 20,102评论 9 92
  • 名利.名利.先有名后有利.利益.利益.先有利.才有益.还是先有益才有利.多少人看中了利.忘记了益.做个有益别人的人...
    月老的女婿阅读 65评论 0 0