Objective-C runtime机制(前传)——Mach-O格式

Mach-O

Mach-O是Mach Object文件格式的缩写。它是用于可执行文件,动态库,目标代码的文件格式。作为a.out格式的替代,Mach-O格式提供了更强的扩展性,以及更快的符号表信息访问速度。

Mach-O格式为大部分基于Mach内核的操作系统所使用的,包括NeXTSTEP, Mac OS X和iOS,它们都以Mach-O格式作为其可执行文件,动态库,目标代码的文件格式。

具体到我们的iOS程序,当用XCode打包后,会生成一个.app为扩展名的文件(位于工程目录/Products文件夹下),其实.app是一个文件夹,我们用鼠标右键选择‘Show Package contents’,就可以查看文件夹的内容,其中会发现有一个和我们工程同名的unix 可执行文件,这个就是iOS可执行文件,它是符合Mach-O格式的。


Objective-C runtime机制(前传)——Mach-O格式_第1张图片
image

Mach-O文件结构

关于Mach-O的文件格式,在苹果官网已经找不到相关说明了,但是你可以通过下面链接获取PDF版说明:
Mach-O File Format Reference

Mach-O格式如下图所示,它被分为headerload commandsdata三大部分:

Objective-C runtime机制(前传)——Mach-O格式_第2张图片
image

header:对Mach-O文件的一个概要说明,包括Magic Number, 支持的CUP类型等。

load commands: 当系统加载Mach-O文件时,load command会指导苹果的动态加载器(dyld)h或内核,该如何加载文件的Data数据。

data: Mach-O文件的数据区,包含代码和数据。其中包含若干Segment块(注意,除了Segment块之外,还有别的内容,包括code signature,符号表之类,不要被苹果的图所误导!),每个Segment块中包含0个或多个seciton。Segment根据对应的load command被dyld加载入内存中。

我们可以使用MachOView(一个查看MachO 格式文件信息的开源工具)工具来查看一个具体的文件的Mach-O格式。

header

我们以一个普通的iOS APP为例,看看Mach-O文件header部分的具体内容。通过MachOView打开可执行文件,可以看到header的结构:

Objective-C runtime机制(前传)——Mach-O格式_第3张图片
image

是不是有些懵?下面我们就结合Darwin内核源码,来了解下Mach header的定义。

Mach header的定义位于Darwin源码中的 EXTERNAL_HEADERS/mach-o/loader.h 中:

32位:

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 */
};

64位:

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;   /* type of file */
    uint32_t    ncmds;      /* number of load commands */
    uint32_t    sizeofcmds; /* the size of all the load commands */
    uint32_t    flags;      /* flags */
    uint32_t    reserved;   /* reserved */
};

可以看到,32位和64位的Mach header基本相同,只不过64位header中多了一个保留参数reserved。

  • magic:魔数,用来标识这是一个Mach-O文件,有32位和64位两个版本:
#define MH_MAGIC    0xfeedface  /* the mach magic number */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
  • cputype:支持的CUP架构类型,如arm。
  • cpusubtype:在支持的CUP架构类型下,所支持的具体机器型号。在我们的例子中,APP是支持所有arm64的机型的:CUP_SUBTYPE_ARM64_ALL
  • filetype: Mach-O的文件类型。包括:
#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 内核扩展 */
  • ncmds:load command的数量
  • sizeofcmds: 所有load command的大小
  • flags: Mach-O文件的标志位。主要作用是告诉系统该如何加载这个Mach-O文件以及该文件的一些特性。有很多值,我们取常见的几种:
#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  /* 加载程序在随机的地址空间,只在 MH_EXECUTE中使用 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */

MH_PIE 随机地址空间

每次系统加载进程后,都会为其随机分配一个虚拟内存空间。
在传统系统中,进程每次加载的虚拟内存是相同的。这就让黑客有可能篡改内存来破解软件。

dyld

dyld是苹果公司的动态链接库,用来把Mach-O文件加载入内存。

二级命名空间

表示其符号空间中还会包含所在库的信息。这样可以使得不同的库导出通用的符号。与其相对的是扁平命名空间。

Load commands

load commands 紧跟在header之后,用来告诉内核和dyld,如何将各个Segment加载入内存中。
load command被源码表示为struct,有若干种load command,但是共同的特点是,在其结构的开头处,必须是如下两个属性:

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

苹果为cmd定义了若干的宏,用来表示cmd的类型,下面列举出几种:

// 描述该如何将32或64位的segment 加载入内存,对应segment command类型
#define LC_SEGMENT  0x1
#define LC_SEGMENT_64   0x19    
// UUID, 2进制文件的唯一标识符
#define LC_UUID     0x1b
// 启动动态加载器dyld
#define LC_LOAD_DYLINKER 0xe

Segment load command

在这么多的load command中,需要我们重点关注的是segment load command。segment command解释了该如何将Data中的各个Segment加载入内存中,而和我们APP相关的逻辑及数据,则大部分位于各个Segment中。

而和我们的Run time相关的Segment,则位于__DATA类型Segment下。

Segment load command分为32位和64位:

struct segment_command { /* for 32-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT */
    uint32_t    cmdsize;    /* includes sizeof section structs */
    char        segname[16];    /* segment name */
    uint32_t    vmaddr;     /* memory address of this segment */
    uint32_t    vmsize;     /* memory size of this segment */
    uint32_t    fileoff;    /* file offset of this segment */
    uint32_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};

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;     /* memory address of this segment */
    uint64_t    vmsize;     /* memory size of this segment */
    uint64_t    fileoff;    /* file offset of this segment */
    uint64_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};

32位和64位的Segment load command基本类似,只不过在64位的结构中,把和寻址相关的数据类型,由32位的uint32_t改为了64位的uint64_t类型。

结构体的定义,看注释基本能够看懂,就是maxprot, initprot不太明白啥意思。

这里介绍一个特殊的‘Segment’,叫做__PAGEZERO Segment。 这里说它特殊,是因为这个Segment其实是苹果虚拟出来的,只是一个逻辑上的段,而在Data中,根本没有对应的内容,也没有占用任何硬盘空间。

__PAGEZERO Segment在VM中被置为Read only,逻辑上占用APP最开始的4GB空间,用来处理空指针。

我们用MachOV点开__PAGEZERO Segment所对应的Segment load command,LC_SEGMENT_64(__PAGEZERO):


Objective-C runtime机制(前传)——Mach-O格式_第4张图片
image

可以看到其vm size是4GB,但其真正的物理地址File size和offset都是0。

Section header

在Data中,程序的逻辑和数据是按照Segment(段)存储,在Segment中,又分为0或多个section,每个section中在存储实际的内容。而之所以这么做的原因在于,在section中,可以不用内存对齐达到节约内存的作用,而所有的section作为整体的Segment,又可以整体的内存对齐。

在Mach-O文件中,每一个Segment load command下面,都会包含对应Segment 下所有section的header。
section header的定义如下:

struct section { /* for 32-bit architectures */
    char        sectname[16];   /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint32_t    addr;       /* memory address of this section */
    uint32_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) */
};

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 */
};

这样,关于load commonds部分,其真正的结构其实和苹果提供的图片有些许的差异:

Objective-C runtime机制(前传)——Mach-O格式_第5张图片
image

Data

Mach-O的Data部分,其实是真正存储APP 二进制数据的地方,前面的header和load command,仅是提供文件的说明以及加载信息的功能。

Data部分也被分为若干的部分,除了我们前面提到的Segment外,还包括符号表,代码签名,动态加载器信息等。

而程序的逻辑和数据,则是放在以Segment分割的Data部分中的。我们在这里,仅关心Data中的Segment的部分。

Segment根据内容的不同,分为若干类型,类型名称均是以“双下划线+大写英文”表示,有的Segment下面还会包含若干的section,section的命名是以"双下划线+小写英文"表示。

先来看Segment,Mach-O中有如下几种Segment:

#define SEG_PAGEZERO    "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,表示空指针区域 */
#define SEG_TEXT    "__TEXT" /* 代码/只读数据段 */
#define SEG_DATA    "__DATA" /* 数据段 */
#define SEG_OBJC    "__OBJC" /* Objective-C runtime 段 */
#define SEG_LINKEDIT    "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */

这里面注意到到SEG_OBJC,是和OC的runtime相关的。但是根据这篇文章中说所,在OC 2.0中已经废弃掉__OBJC段,而是将其放入到了__DATA段中以__objc开头的section中。这些和runtime相关的sections是本文的重点,我们稍后再分析。我们先看看其他的段。

__TEXT段

__TEXT是程序的只读段,用于保存我们所写的代码和字符串常量,const修饰常量等。
下面是__TEXT段下常见的section:
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 类名称

例如,我们点击__TEXT.__objc_classname, 会看到我们程序中所使用到的类的名称:

Objective-C runtime机制(前传)——Mach-O格式_第6张图片
image

而在__TEXT.__cstring section中,则看到我们定义的字符串常量(如@"I'm a cat!! miao miao"):

Objective-C runtime机制(前传)——Mach-O格式_第7张图片
image

值得注意的是,这些都是以明文形式展现的。如果我们将加密key用字符串常量或宏定义的形式存储在程序中,可以想象其安全性是得不到保障的。

__DATA段

__DATA段用于存储程序中所定义的数据,可读写。__DATA段下常见的sectin有:

Section 用途
__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开头的section,而这些section,均是和runtime的加载有关的。


Objective-C runtime机制(前传)——Mach-O格式_第8张图片
image

我们将在后续的文章中,继续探讨这些section和runtime的关系。

总结

这次我们一起了解了XNU内核下的二进制文件格式Mach-O。它由header,load command以及data三部分组成:


Objective-C runtime机制(前传)——Mach-O格式_第9张图片
image

我们重点应该了解的应该是data部分,因为这里存储着我们程序真正的数据和代码。
在data部分中,又区分为以Segment划分的部分以及代码签名等其他部分。
在Segment下,有区分有若干的section。
常用的Segment有__PAGE_ZERO, __TEXT, __DATA(注意区分Mach-O的data和这里的__DATA段名称)。

参考资料

趣探 Mach-O:文件格式分析
深入理解Macho文件(二)- 消失的__OBJC段与新生的__DATA段
mach-o格式分析
Mach-O 文件格式探索
Mach-O 维基百科

你可能感兴趣的:(Objective-C runtime机制(前传)——Mach-O格式)