[iOS] Mach-O文件解析

1. 介绍

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

熟悉Mach-O文件格式,有助于了解苹果底层的软件运行机制,更好的掌握dyld(dyld是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作) 加载Mach-O的步骤。

比如,在我们项目下的Products下的xxx.app文件,其实就是一个文件夹,我们在其右键显示包内容,可以看到一个xxxUnix可执行文件,这个就是iOS可执行文件,符合Mach-O格式的。

截屏2020-09-09 下午5.35.01.png

2. Mach-O文件类型

对于OS XiOS来说,Mach-O是其可执行文件的格式,主要包括以下几种文件类型:

  • Executable 可执行文件
  • Dylib 动态链接库
  • Bundle 无法被链接的动态库,只能在运行时使用dlopen加载
  • Image 指的是Executable、Dylib和Bundle的一种
  • Framework 包含Dylib、资源文件和头文件的集合
2.1 使用 file 命令查看文件类型
  • 以导出来的 developer 包为例,我们看一下可执行文件的类型,把 .ipa 改为.zip,直接解压拿到 Payload 文件:

    截屏2020-07-30 下午9.12.05.png

  • 然后右键Payload里面的文件,“显示包内容”找到可执行文件:

    截屏2020-07-30 下午9.13.10.png

  • 使用 file 命令查看:

    截屏2020-07-30 下午9.16.19.png

我们可以看到是一个 64 位的 arm64 架构的可执行文件。

3. 通用二进制文件(Universal binary)

包含了支持多架构的 Mach-O ececutable 可执行文件被称为 : 通用二进制文件 , 即多种架构都可读取运行。
通常也被称为 Universal binary , 在 MachOView(用于查看Mach-O文件) 等中叫做 Fat binary ,这种二进制文件是可以完全拆分开来 , 或者重新组合的。

3.1 介绍
  • 苹果公司提出的一种程序代码,能同时适用多种架构的二进制文件
  • 同一个程序包中同时为多种架构提供最理想的性能
  • 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大
  • 由于两种架构之间有共同的非执行资源,所以并不会达到单一版本的两倍至多
  • 而且由于执行中只调用一部分代码,运行起来也不需要额外的内存

我们还是通过 file 命令,去查看一个通用二进制文件:

截屏2020-07-30 下午9.24.50.png

可以看到,上面👆该文件包含了 2 个架构:armv7 arm64

3.2 Fat binary的组合和拆分
3.2.1 使用 lipo -info 命令

还可以使用 lipo -info 查看 mach-o文件支持的架构

截屏2020-07-30 下午9.30.02.png

3.2.2 拆分 Fat binary
lipo macho文件名称 -thin 要拆分哪个架构 -output 拆分出来文件名

例:


截屏2020-07-30 下午9.33.00.png

我们可以看到文件夹下面多了一个 Test_arm64的文件,使用 lipo -info 命令再查看一下:

截屏2020-07-30 下午9.34.02.png

这样,我们就拆分出来 arm64 架构的 mach-o文件了,另外,拆分后源文件并不会改变的,自己可以去验证一下。

3.2.3 合并 Fat binary

我们还用上面提到的方法,把源文件的 armv7 架构拆分出来,名字为:Test_armv7,然后,使用下面的合并命令:

lipo -create [arm64架构文件名] [armv7架构文件名] -output [新的 mach-o文件名]

例:

lipo -create Test_arm64 Test_armv7 -output Test_new

然后就生成了一个新的通用二进制文件,名字为:Test_new

我们再来看下新文件和源文件的哈希值,是一模一样的:


截屏2020-07-30 下午9.39.55.png

注意:上面这种方式在我们合并静态库的时候会经常用到的,因为静态库本身就是 Mach-O文件嘛,另外,我们在分析 Mach-O文件的时候,只需要分析单一的一种架构即可。

4. Mach-O文件结构

4.1 Mach-O文件结构图解

我们可以看下面这张 Mach-O 镜像文件格式:

640.jpg

通过上图,可以看出Mach-O主要由以下三部分组成:

  1. Mach-O 头部(Mach Header)
    Mach-O的概要说明,主要是描述了Mach-OCPU架构,文件类型,以及加载命令等信息。它能帮助校验Mach-O合法性和定位文件的运行环境。

  2. 加载命令 (Load Commands)
    用来描述文件在虚拟内存中的逻辑结构,不同的数据类型会使用不同的加载命令表示,其占用的内存和加载命令的总数在Headers中已经指出。通俗来讲,就是存储着各段数据的大小、分段、地址等信息。
    通过图中看出,应该就是是Data中不同的Segment的加载命令。`

  3. Data
    Data中每个段(Segment)的具体数据都保存在这里,每个段都有一个或多个Section,它们存放了具体的数据和代码,主要包含代码,数据,例如符号表,动态符号表等等,Segment根据对应的Load Commanddyld加载入内存中。

那么Data部分包含的Segment都有哪些呢?
绝大多数Mach-O包括以下三个段(也支持用户自定义Segment,但是很少见)

  • __TEXT 代码段,只读,包括函数,和只读的字符串
  • __DATA 数据段,读写,包括可读写的全局变量等
  • __LINKEDIT __LINKEDIT包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。
4.2 使用 MachOView

下载地址:https://sourceforge.net/projects/machoview/,down 下来直接运行,就可以,然后 open 我们的 Mach-O文件:

截屏2020-07-30 下午9.55.18.png

下面我们就来看看,每一部分都是什么内容,下面提到的一些代码,在这里:mach-o源代码

4.3 Mach Header
截屏2020-07-30 下午9.57.00.png

Header 中存储的内容大致如上图所示,存储的是Mach-O文件格式有关的结构体,针对32位和64位架构的cpu,分别使用了mach_headermach_header_64结构体来描述Mach-O头部。

mach_header64结构体的定义如下:

struct mach_header_64 {
    uint32_t    magic;      /* 标志符,用来标识Mach-O的平台属性,确认文件的类型。快速定位 64 位/32 位*/
    cpu_type_t  cputype;    /* cpu 类型 比如 ARM */
    cpu_subtype_t   cpusubtype; /* cpu 具体类型 比如arm64 , armv7 */
    uint32_t    filetype;   /* 标识Mach-O文件的具体类型,比如 MH_EXECUTE,代表可执行文件,MH_DYLINKER,表明该文件是动态链接器程序文件,符号文件(DYSM) */
    uint32_t    ncmds;      /* 加载命令 load commands 的个数 */
    uint32_t    sizeofcmds; /* 加载命令 load commands 的大小 */
    uint32_t    flags;      /* 标志位标识二进制文件支持的功能 , 主要是和系统加载、链接有关 */
    uint32_t    reserved;   /* reserved , 保留字段 */
};

mach_header_64 相较于 mach_header ,也就是 32 位头文件,只是多了一个reserved保留字段,mach_header 是链接器加载时最先读取的内容,它决定了一些基础架构、系统类型、指令条数等信息。

4.3.1 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 内核扩展 */
4.3.2 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
    随机地址空间,标明使用了ASLR
4.4 Load Commands

在内存中,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 */
};
  1. cmd: 具体的加载类型
  2. cmdsize: 具体的load_command结构所占内存的大小

苹果为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

来看下都有哪些类型呢:


image.png
4.4.1 Segment load command

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

  • segname
    segment的名称
  • vmaddr
    该段被加载后在进程地址空间中的虚拟地址
  • vmsize
    段的虚拟内存大小
  • fileoff
    如果该段存在于文件中,则表示该段在文件中的偏移,否则无意义
  • filesize
    段在文件中的大小
  • maxprot
    段页面所需要的最高内存保护(可读 可写 可执行)
  • initprot
    段页面初始的内存保护
  • nsects
    段中包含section的数量
  • flags
    其他杂项标志位。

可以结合下面这张图去理解:


屏幕快照 2019-09-05 上午11.14.37.png
4.4.2 __PAGEZERO

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

__PAGEZERO是Mach-O加载进内存之后附加的一块区域,它不可读,不可写,主要用来捕捉NULL指针的引用。如果访问__PAGEZERO段,会引起程序崩溃。

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


截屏2020-09-10 下午3.38.02.png
4.4.3 Section header

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

Mach-O文件中,每一个Segment load command下面,都会包含对应Segment 下所有sectionheader

定义如下:

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) */
};
  • sectname 就是这个section的名称
  • segname 该section所属的segment名
  • addr 该section在内存的起始位置
  • size 该section的大小
  • offset 该section的文件偏移
  • align 字节大小对齐
  • reloff 重定位入口的文件偏移
  • nreloc 需要重定位的入口数量
  • flags包含section的type和attributes

可以对比下图去理解一下:


屏幕快照 2019-09-05 上午11.26.01.png
4.5 Data部分

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

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

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

先来看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" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */

其中_TEXT段和_DATA段,是我们经常需要研究的,MachOView下面也有详细列出:

截屏2020-07-30 下午10.06.04.png
4.5.1 _TEXT段

我们来看看_TEXT段里都存放了什么,其实真正读取是从_TEXT段开始读取的:

名称 内容
__TEXT.__text 主程序代码
__TEXT.__const const 关键字修饰的常量
__TEXT.__stubs 用于 Stub 的占位代码,很多地方称之为桩代码
__TEXT.__stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methname 方法名称
__TEXT.__objc_classname 类名称
__TEXT.__objc_methtype 方法类型 ( v@: )
__TEXT.__cstring 静态字符串常量
4.5.2 _DATA段

_DATA 在内存中是紧跟在_TEXT 段之后的,__DATA段用于存储程序中所定义的数据,可读写。__DATA段下常见的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的加载有关的:


截屏2020-09-10 下午3.52.22.png

5. 和runtime加载有关的section

5.1 OC之源起

我们知道,程序的入口在iOS中被称为main函数:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

我们所写的所有代码,它的执行的第一步,均是由main函数开始的。

但其实,在程序进入main函数之前,内核已经为我们的程序加载和运行做了许多的事情。

当我们设置符号断点_objc_init,可以看到如下调用堆栈信息,这些函数都是在main函数调用前,被系统调用的:


截屏2020-09-11 下午2.49.44.png

_objc_initOC runtime的入口函数,在这里面,主要功能是读取Mach-O文件中OC对应的Segment section,并根据其中的数据代码信息,完成OC的内存布局,以及初始化runtime相关的数据结构。

我们可以看到,_objc_init 是被_dyld_start 所调用起来的,_dyld_startdyldbootstrap方法,最终调用到了_objc_init

当程序启动时,系统内核首先会加载dyld, 而dyld会将我们APP所依赖的各种库加载到内存空间中,其中就包括libobjc库(OCruntime), 这些工作,是在APPmain函数执行前完成的。

在_objc_init 方法中,会注册监听来自dlyd的以下事件:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容