美文网首页
[iOS] Mach-O文件解析

[iOS] Mach-O文件解析

作者: 沉江小鱼 | 来源:发表于2019-11-18 15:58 被阅读0次

    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 Segment在VM中被置为Read only,逻辑上占用APP最开始的4GB空间,用来处理空指针。

    如下图,可以看到其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

    相关文章

      网友评论

          本文标题:[iOS] Mach-O文件解析

          本文链接:https://www.haomeiwen.com/subject/wtdbictx.html