茶茶的小屋

不管生活有多不容易,都要守住自己的那一份优雅。

理解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. 逆向

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


扫描二维码,在手机上阅读!

评论卡

shijiebei 365bet manbetx 188bet xinshui caipiao 95zz tongbaoyule beplay 88bifa 18luck betway bwin hg0088 aomenjinshayulecheng ca88 shenbotaiyangcheng vwin w88 weide