探索Mach-O文件

Mach-O

Mach-O文件格式是 OS XiOS 系统上的可执行文件格式,类似于windowsPE 文件 与 Linux(其他 Unix like)的 ELF 文件。

Mach-O 没有类似于 XMLYAMLJSON 等诸如此类的特殊格式,它只是一个二进制字节流,被划分为了有意义的数据块。这些块包含元信息,比如字节顺序cpu 类型块的大小等等。

它由3部分组成:

1、Header:保存了Mach-O的一些基本信息,包括了平台、文件类型、LoadCommands的个数等等。
2、LoadCommands:这一段紧跟Header,加载Mach-O文件时会使用这里的数据来确定内存的分布。
3、Data:每一个segment的具体数据都保存在这里,这里包含了具体的代码、数据等等。

Mach-O文件的格式如下图所示:

mach

OS X有两种类型的目标文件:Mach-O 文件通用二进制文件,也叫作胖文件。它们之间的区别是:Mach-O 文件包含一种架构(i386、x86_64、arm64 等等)的对象代码,而胖文件可能包含若干包含不同架构(i386、x86_64、arm、arm64 等等)对象代码的对象文件。

Fat Header 文件

胖文件包含不同架构的数据,不过每一个架构的结构跟Mach-O 文件一样。

FAT二进制数据Header数据结构定义在 <mach-o/fat.h>里面。

#define FAT_MAGIC   0xcafebabe 32位 大端
#define FAT_CIGAM   0xbebafeca 32位 小端 /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC or FAT_MAGIC_64 */
    uint32_t    nfat_arch;   /* 结构体实例的个数 */
};

struct fat_arch {
    cpu_type_t  cputype;    /* cpu 说明符 (int) */
    cpu_subtype_t   cpusubtype; /* 指定 cpu 确切型号的整数 (int) */
    uint32_t    offset;     /* CPU 架构数据相对于当前文件开头的偏移值 */
    uint32_t    size;       /* 数据大小 */
    uint32_t    align;      /* 数据内润对其边界,取值为 2 的幂 */
};

/*
 * then the 64-bit fat file format is used.
 */
#define FAT_MAGIC_64    0xcafebabf 64位 大端
#define FAT_CIGAM_64    0xbfbafeca 64位 小端   /* NXSwapLong(FAT_MAGIC_64) */

struct fat_arch_64 {
    cpu_type_t  cputype;    /* cpu 说明符 (int) */
    cpu_subtype_t   cpusubtype;  /* 指定 cpu 确切型号的整数 (int) */
    uint64_t    offset;     /* CPU 架构数据相对于当前文件开头的偏移值 */
    uint64_t    size;       /* 数据大小 */
    uint32_t    align;       /* 数据内润对其边界,取值为 2 的幂 */
    uint32_t    reserved;   /* 保留位 */
};
大小端

struct fat_header里说 magic 只能取FAT_MAGIC 或者 FAT_MAGIC_64 , 但是为什么这里又来了一个FAT_CIGAM, 这是什么情况?
如果心细的话, 会发现CIGAM其实就是 MAGIC 反过来写了而已. 因此这里需要了解一个概念, 就是大小端.
数据在内存中存储有2种形式, 一种是高数值在低内存, 另外一种相反则是低数值在低内存, 说起来比较抽象, 直接以 0xabcdef12 来比较大小端存储情况:

// 大端:
// | 0x00000 | ab | <-- 最大的数字在低位
// | 0x00008 | cd |
// | 0x00010 | ef |
// | 0x00018 | 12 |

// 小端:
// | 0x00000 | 12 | <-- 最小的数字在低位
// | 0x00004 | ab |
// | 0x00008 | cd |
// | 0x0000c | ef |

在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器)。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

大小端在不同的架构上是不一样的。

大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。

fat实例

Mach Header 文件

Mach-O 文件Header数据结构定义在 <mach-o/loader.h>里面。

/* 32-bit architectures */
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) */

/* 64-bit architectures */
struct mach_header_64 {
    uint32_t    magic;      /* mach magic 标识符 */
    cpu_type_t  cputype;    /* CPU 类型标识符,同通用二进制格式中的定义 */
    cpu_subtype_t   cpusubtype; /* CPU 子类型标识符,同通用二级制格式中的定义 */
    uint32_t    filetype;   /* 文件类型 */
    uint32_t    ncmds;      /* 加载器中加载命令的条数 */
    uint32_t    sizeofcmds; /* 加载器中加载命令的总大小 */
    uint32_t    flags;      /* dyld 的标志 */
    uint32_t    reserved;    /* 64 位的保留字段 */
};

/* 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-O 支持多种类型文件,所以此处引入了 filetype 字段来标明,这些文件类型定义在 loader.h 文件中同样可以找到。

#define    MH_OBJECT    0x1        /* Target 文件:编译器对源码编译后得到的中间结果 */
#define    MH_EXECUTE    0x2        /* 可执行二进制文件 */
#define    MH_FVMLIB    0x3        /* VM 共享库文件(还不清楚是什么东西) */
#define    MH_CORE        0x4        /* Core 文件,一般在 App Crash 产生 */
#define    MH_PRELOAD    0x5        /* preloaded executable file */
#define    MH_DYLIB    0x6        /* 动态库 */
#define    MH_DYLINKER    0x7        /* 动态连接器 /usr/lib/dyld */
#define    MH_BUNDLE    0x8        /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
#define    MH_DYLIB_STUB    0x9        /* 静态链接文件(还不清楚是什么东西) */
#define    MH_DSYM        0xa        /* 符号文件以及调试信息,在解析堆栈符号中常用 */
#define    MH_KEXT_BUNDLE    0xb        /* x86_64 内核扩展 */

另外在loader.h中还可以找到 flags 中所取值的全部定义,这里只介绍常用的:

#define    MH_NOUNDEFS    0x1        /* Target 文件中没有带未定义的符号,常为静态二进制文件 */
#define MH_SPLIT_SEGS    0x20  /* Target 文件中的只读 Segment 和可读写 Segment 分开  */
#define MH_TWOLEVEL    0x80        /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
#define MH_FORCE_FLAT    0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES    0x8000 /* 二进制文件使用了弱符号 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
#define    MH_PIE 0x200000  /* 对可执行的文件类型启用地址空间 layout 随机化 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */

Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且ncmdssizeofcmds 字段将会用在加载命令的过程中。

mach-o

LoadCommands

mach_header之后的是Load Command加载命令,这些加载命令在Mach-O文件加载解析时,被内核加载器或者动态链接器调用,基本的加载命令的数据结构如下:

struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

此结构对应的成员只有2个:cmd字段代表当前加载命令的类型。cmdsize字段代表当前加载命令的大小。
cmd的类型不同,所代表的加载命令的类型就不同,它的结构体也会有所不一样,对于不同类型的加载命令,它们都会在load_command结构体后面加上一个或多个字段来表示自己特定的结构体信息。

macOS系统在进化的过程中,加载命令算是比较频繁被更新的一个数据结构体,截止到macOS 10.13系统,加载命令的类型cmd的取值共有53种。

通过查看XNU-4570代码可以发现内核会处理9个命令,其中还有LC_SEGMENT、LC_ENCRYPTION_INFO,它们是32位的处理方式。

Command类型 处理函数 用途
LC_SEGMENT_64 load_segment 将segment数据加载映射到进程的内存空间
LC_UNIXTHREAD load_unixthread 开启一个UNIX线程
LC_MAIN load_main 加载main函数
LC_LOAD_DYLINKER load_dylinker 调用/usr/lib/dyld程序
LC_UUID load_uuid 加载128-bit的唯一ID
LC_CODE_SIGNATURE load_code_signature 进行数字签名
LC_ENCRYPTION_INFO_64 set_code_unprotect 加密二进制文件

至于内核dyld加载mach-o的流程,这里不多介绍(水平不够 🐶),可以自行查阅相关资料,或者看相关源码。

LoadCommands结构如下图:

image

根据偏移量找到每一个段区第一个变量cmd的地址,根据它的value类型,转成相应的结构体,所以每一个段区的结构体前面2位都是cmdcmdsize

Segment

LC_SEGMENT 意味着这部分文件需要映射到进程的地址空间去,它是是最常见的段结构,这里看下他们的数据结构。

段名一般为大写,节名一般为小写,名字前面加2个下划线__

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];    /* 一个16字节大小的空间,用来存储段的名称 */
    uint64_t    vmaddr;     /* 段要加载的虚拟内存地址 */
    uint64_t    vmsize;     /* 段所占的虚拟内存的大小 */
    uint64_t    fileoff;    /* 段数据所在文件中偏移地址 */
    uint64_t    filesize;   /* 段数据实际的大小 */
    vm_prot_t   maxprot;    /* 页面所需要的最高内存保护 */
    vm_prot_t   initprot;   /* 页面初始的内存保护 */
    uint32_t    nsects;     /* 段所包含的节区(section) */
    uint32_t    flags;      /* 段的标志信息 */
};

</br>
一个程序编译后,可执行的程序分成了多个段,不同的类型的数据放入了不同的段中,如代码段放入__TEXTsegment_command有几个常用的段,分别起到不同的作用,定义如下:

宏定义 名字 用途 内存保护
SEG_PAGEZERO __PAGEZERO 捕捉NULL指针的引用,映射到虚拟内存的第一页 0
SEG_TEXT __TEXT 包含了执行代码以及其他只读数据 5
SEG_DATA __DATA 程序数据 3
SEG_LINKEDIT __LINKEDIT 链接器使用的符号以及其他表 1

</br>

segment_command中的maxprotinitprot是内存保护相关的,分别有:

  • VM_PROT_READ(可读 1)
  • VM_PROT_WRITE(可写 2)
  • VM_PROT_EXECUTE(可执行 4)

每个段具体的值都是由他们组合相加得到的,但LC_SEGMENT_64(__PAGEZERO)段比较特殊,2个字段的值都是VM_PROT_NONE(0)。

section

通过注释A segment is made up of zero or more sections可知,segment里面还可以包含sections,下面先看下sections的数据结构:

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 的内存对齐边界 (2 的次幂) */
    uint32_t    reloff;     /* 重定位入口的文件偏移 */
    uint32_t    nreloc;     /* 需要重定位的入口数量 */
    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 */
};

segment_command结构中nsects字段标识含有多少个section,它是具体有用的数据存放的地方。下面列举几个常见的section

segment Section 用户
__TEXT __text 主程序代码
__TEXT __cstring C 语言字符串
__TEXT __const const 关键字修饰的常量
__TEXT __stubs 用于 Stub 占位代码,很多地方称之为桩代码。
__TEXT __stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
__TEXT __objc_methname Objective-C 方法名称
__TEXT __objc_methtype Objective-C 方法类型
__TEXT __objc_classname Objective-C 类名称
__DATA __data 初始化过的可变数据
__DATA __la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA __nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA __const 没有初始化过的常量
__DATA __cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA __bss BSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA __common 没有初始化过的符号声明
__DATA __objc_classlist Objective-C 类列表
__DATA __objc_protolist Objective-C 原型
__DATA __objc_imginfo Objective-C 镜像信息
__DATA __objc_selfrefs Objective-C self 引用
__DATA __objc_protorefs Objective-C 原型引用
__DATA __objc_superrefs Objective-C 超类引用
__DATA __objc_ivar Objective-C 成员变量
__DATA __objc_catlist Objective-C 分类
__DATA __objc_nlclslist Objective-C 重写+load方法的类
__DATA __objc_nlcatlist Objective-C 重写+load方法的分类

推荐阅读更多精彩内容

  • Mach-O 概述 和 部分命令介绍 我们知道Windows下的文件都是PE文件,同样在OS X和iOS中可执行文...
    青花瓷的平方阅读 14,413评论 2 52
  • 1 dyld 1.1 dyld简介 在iOS系统中,几乎所有的程序都会用到动态库,而动态库在加载的时候都需要用d...
    Kevin_Junbaozi阅读 10,165评论 4 44
  • 组成 Mach-O通常有三部分组成头部 (Header): Mach-O文件的架构 比如Mac的 PPC, PPC...
    充满活力的早晨阅读 1,214评论 0 5
  • Mach-O类型的文件 Mach-O是一种文件的格式; 是iOS/Mac OS上存储程序以及库的标准格式Mach ...
    其字德安阅读 5,013评论 0 12
  • 有时被放弃 像人类无视自己的影子 有时被研究 如何在这个三维的笼子里来来去去 有时被疏解 学会盖印和滤色 有时被存...
    筛筛阅读 274评论 0 1
  • 上一章 回目录 下一章第九章 有外星人 现在是早上5点。严格说起来,天应该还没有亮,...
    非谁莫属阅读 158评论 0 0
  • 你嘴角牵起的笑 如绽放的花朵 鲜艳而又明亮 如冬日的暖阳 温暖而且阳光
    在人境阅读 207评论 5 5