Mach-O文件周边二三事

4,277 阅读15分钟

一、iOS App大小限制变化

1、App下载大小的限制变化

  • Apple不断放宽在蜂窝网络下,从AppStore下载App的大小限制,2013年9月,iOS 7正式版后,蜂窝网络下App下载大小的限制,从 50 MB 提升至 100 MB;

  • 到了2017年9月,iOS11正式版本后,限制从 100 MB 提升至 150 MB,并在2019年5月下旬,将 150 MB"默默"放宽到200MB;

  • 2019年9月,iOS13正式版本后,直接放开了蜂窝网络下App下载大小的限制,主要流量够用,随便下。

  • 下载大小是App 压缩包(.ipa 文件)大小,目前AppStore上见到的是安装大小,它是压缩包解压后的大小。

2、可执行文件大小的限制变化

  • 根据Apple的审核要求,上传App Store的ipa的可执行文件有大小限制,这里的可执行文件大小不是指二进制(Mach-O)文件大小,而是指二进制(Mach-O)文件中__TEXT部分的大小。

  • IOS 7版本之前, 二进制文件中所有__TEXT部分总和不得超过80MB;

  • iOS 7.X 至 iOS 8.X,二进制文件中,每个 Architecture Slice(架构片段)中的__TEXT部分不得超过60MB

    • Architecture Slice是针对特定架构的胖二进制布局文件的一部分。例如,一个胖二进制文件可能会包含针对 32 位和 64 位架构的片段。
  • iOS 9.0之后,二进制文件中所有__TEXT部分的总和不超过500 MB;具体可参考最大构建版本文件大小

  • 2020年4月1号了,几乎所有的iOS App兼容的最低版本都是iOS 9起步,如:微信/美团/美团外卖iOS App最低支持iOS10,支付宝/手淘/滴滴/抖音/快手iOS App最低支持iOS 9。

3、总结

  • 随着4G普及,5G到来,流量费用大大降低、Apple放开了对App大小方面的限制、iOS用户升级系统意愿高等因素,iOS开发者对包大小可以松口气,如不必很担心超过二进制__TEXT部分的限制,可以优先业务迭代,有人力的情况下,再去做包瘦身;
  • 如果追求App的更高品质,在竞品中拔得头筹,还是需要在包大小方面花很大功夫;一般来说,ROI最高的是无用资源(主要是图片)的清理,其次是二进制文件大小的优化;二进制文件大小的优化一个是靠优化编译器选项,一个是清理无用的类、函数和代码块等。
  • 网络上有很多类似的包优化的博客可以参考,本文就不说了,本文主要介绍Mach-O文件周边的知识:Mach-O文件本身、 分析工具和Link Map File等。

二、Mach-O文件简介

1、概述

  • Mach-O格式全称为Mach Object文件格式的缩写,是MacOS或者iOS上可执行的程序格式,类似于Windows上的PE格式 (Portable Executable),linux上的ELF格式 (Executable and Linking Format)。
  • Mach-O文件的分类有如下5类:
    • Executable:应用的可执行文件
    • Dylib Library:动态链接库(又称DSO或DLL)
    • Static Library:静态链接库
    • Bundle:不能被链接的Dylib,只能在运行时使用dlopen( )加载,可当做macOS的插件
    • Relocatable Object File :可重定向文件类型

2、Mach-O文件的组成

Mach-O文件主要包括三部分内容: Header(头部)、Load Commands(加载命令)、Data(数据区)

  • Header(头部),指明了 CPU 架构、大小端序、文件类型、Load Commands 个数等一些基本信息,Headers 能帮助校验 Mach-O 合法性和定位文件的运行环境,64位架构为例,Header结构定义如下:

    struct mach_header_64 {
        uint32_t    magic;        /* mach magic number identifier 魔数,用于快速确认该文件用于64位还是32位 */
        cpu_type_t    cputype;    /* cpu specifier,CPU**类型,比如 arm */
        cpu_subtype_t    cpusubtype;    /* machine specifier,对应的具体类型,比如arm64、armv7 */
        uint32_t    filetype;    /* type of file,文件类型,比如可执行文件、库文件、Dsym文件,demo中是2 `MH_EXECUTE`,代表可执行文件*/
        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  保留字段 */
    };
    

    文件类型filetype取值如下:

    • OBJECT,指的是 .o 文件或者 .a 文件;
    • EXECUTE,指的是 IPA 拆包后的文件;
    • DYLIB,指的是 .dylib 或 .framework 文件;
    • DYLINKER,指的是动态链接器;
    • DSYM,指的是保存有符号信息用于分析闪退信息的文件。
  • Load Commands(加载命令),包含 Mach-O 里命令类型信息,名称和二进制文件的位置;以64位架构为例,Load Commands结构定义如下:

    struct segment_command_64 { /* for 64-bit architectures */
        uint32_t    cmd;        /* cmd是Load commands的类型,LC_SEGMENT_64代表将文件中64位的段映射到进程的地址空间*/
        uint32_t    cmdsize;    /* includes sizeof section_64 structs 代表load command的大小 */
        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 标示了Segment中有多少secetion */
        uint32_t    flags;        /* flags */
    };
    
    • 加载命令告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的;LC_SEGMENT_64LC_SEGMENT` 是加载的主要命令, 他们指导内核来设置进程的内存空间;
  • Data(数据区)由Segment 的数据组成,是 Mach-O中 占比最多的部分,有代码有数据,比如符号表。Data 共三个 Segment:__TEXT(包含执行代码以及其他只读数据)、__DATA(程序数据,该段可写)、__LINKEDIT(包含链接器使用的符号以及其他表)。

    • 其中, __TEXT 和 __DATA 对应一个或多个 Section,__LINKEDIT 没有 Section,需要配合 LC_SYMTAB 来解析 symbol table 和 string table。这些里面是 Mach-O 的主要数据。

    • 以64位架构为例,Section的结构定义如下:

      struct section_64 { /* for 64-bit architectures */
      	char		sectname[16];	/* name of this section  比如_text、stubs */
      	char		segname[16];	/* segment this section goes in 该section所属的segment,比如__TEXT*/
      	uint64_t	addr;		/* memory address of this section 该section在内存的起始位置 */
      	uint64_t	size;		/* size in bytes of this section 该section的大小*/
      	uint32_t	offset;		/* file offset of this section 该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) 包含section的type和attributes*/
      	uint32_t	reserved1;	/* reserved (for offset or index) */
      	uint32_t	reserved2;	/* reserved (for count or sizeof) */
      	uint32_t	reserved3;	/* reserved */
      };
      
    • 备注:__TEXT代表的是Segment,小写的__text代表 Section

3、FatFile/FatBinary

  • FatFile/FatBinary直译“胖二进制”,是一个由不同的编译架构的Mach-O产物合成的集合体。一个架构的Mach-O只能在相同架构的机器或者模拟器上用,为了支持不同架构需要一个集合体。
  • 这里的架构是指CPU的指令集,iOS设备使用的是ARM处理器,ARM支持的指令集有两类:32位ARM指令集(armv7|armv7s)和 64位ARM指令集(arm64和arm64e)
  • 此外,还有i386(32位)和x86_64(64位)这两个Mac处理器的指令集,iOS模拟器没有运行ARM指令集,运行在iOS模拟器上的App需要支持i386 or x86_64的指令集。

4、XNU加载App Mach-O

  • iOS 系统是基于 ARM 架构的,从上到下可以分为四层:用户体验层(含SpringBoard、Spotlight等)、应用框架层(Cocoa等框架)、核心框架层(Metal等图形和媒体核心框架)和 Darwin 层(操作系统核心,含了系统内核 XNU、驱动等)。

  • XNUDarwin的内核;主要有MachBSDIOKit;Mach是内环,负责操作系统最基础的工作,包括进程和线程抽象、处理器调度、IPC(进程间通信)、消息机制、虚拟内存管理、内存保护等;而BSD层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能;

  • App的二进制文件由iOS系统内核XNU加载的;XNU 加载就是为 Mach-O 创建一个新进程,建立虚拟内存空间,解析 Mach-O 文件,最后映射到内存空间;

  • 具体加载流程概括为:fork 新进程;为 Mach-O 分配内存;解析 Mach-O;读取 Mach-O 头信息;遍历 load command信息,将 Mach-O 映射到内存;启动 dyld( 用户态进程,不在 XNU内,加载动态库等)。

    Mach 中,所有的东西都是通过自己的对象实现的;进程、线程和虚拟内存都被称为"对象";Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

三、分析Mach-O的基础命令

1、lipo命令

  • 管理Fat File的工具, 可以查看CPU架构, 提取特定架构,整合和拆分库文件

  • 常用的方法如下:

    # 【查】看胖二进制支持的CPU架构列表
    lipo -info xxxx.a/xxxx.framework/xxxx
    
    # 【拆】从胖二进制中提取特定CPU架构的二进制
    lipo lxx.a -thin cpu_type(armv7s/arm64等)  -output xx_cpu_type.a
    
    # 【合】整合成Fat文件
    lipo -create xxxx1  xxxx2  -output xxxxfat
    
    #【删】移除掉特定的cpu架构的文件
    lipo -remove cpu_type(armv7s/arm64等) xxxx -output  xxxx
    

2、ar命令

  • 常用来创建、修改库,从库中提出单个模块。

  • 常使用ar命令解压.a文件,但是如果直接解压第三方SDK的.a文件(如微信SDK),会遇到xxx.a is a fat file (use libtool(1) or lipo(1) and ar(1) on it)的错误。

  • 这是因为这类.a文件是一个胖二进制,包含了多个CPU架构,需要先使用lipo文件来提取特定的CPU架构的二进制文件,使用如下:

    # 拆分出个arm64架构的二进制
    lipo xx. a -thin arm64 -output xx_arm64.a
    # 解压.a文件
    ar -x xx_arm64.a
    

3、nm命令

  • 被用于显示二进制目标文件的符号表(display name list (symbol table))

  • 常用的方法如下:

    # 得到Mach-O中的程序符号表
    nm path
    # 目标文件的所有符号
    nm -nm path 
    

4、grep命令

  • 用来判断是否包含字符串

  • 常用的方法如下:

    # 检查是否包含xxx字符串:
    grep -r "xxx” path
    

四、otool工具使用简介

otool(object file displaying tool),可以对指定目标文件或者库文件以特定的方法解析显示,是分析Mach-O文件的利器。(一般安装了Xcode,默认安装了otool)

1、查看Mach-O的header

otool -h app_name.app/app_name
  • header信息包括:magic、cputype、cpusubtype、caps、filetype、ncmds、sizeofcmds和flags

2、查看Mach-O的load commands

otool -l app_name.app/app_name
  • 信息主要包括Mach-O 里命令类型信息,名称和二进制文件的位置。

3、查看Mach-O依赖的动态库

otool -L app_name.app/app_name
  • 动态库信息包括:动态库名称、当前版本号、兼容版本号

4、查看Mach-O文件的加密信息

otool -l app_name.app/app_name | grep crypt
  • 执行结果中cryptid有 0(未加密)和1(加密) 两个取值

5、查看Mach-O文件中所有类和引用类(地址)

# 获取所有类的地址
otool -v -s __DATA __objc_classlist app_name.app/app_name
# 获取所有引用类的地址
otool -v -s __DATA __objc_classrefs app_name.app/app_name 
  • 可以利用这两个结果的差值,然后进行符号化,就可以得到未被引用的类信息。不过,需要注意的是:未引用的类不等于未使用的类,一些实际使用(动态调用等)也可能被误认为是未使用的类。

6、扩展:MachOView工具

  • 使用otool固然方便,但是也可以使用MachOView工具来查看Mach-O文件,更加直观,很方便看到 Mach-O文件header、 load commands等信息,具体使用见Mach-O文件浏览器---MachOView
  • MachOView的工具界面左上角有一个 RAW、RVA 的选项。
    • RAW 就是指该字节相对于文件开始部分的绝对偏移,文件头部的地址是从0x000开始的。
    • RVA 是相对于某个基地址的偏移,也就是整体的绝对偏移值再加上某个基地址,文件头部的地址是从某个值(基地址)开始的。

五、class-dump工具使用简介

1、概述

  • class-dump用来dump Mach-O文件的class信息;它利用OC语言的Runtime特性,将存储在Mach-O文件中的头文件信息提取出来,并生成对应的.h文件。
  • 逆向中也常用到class-dump这个工具

2、下载和安装

  • Class-dump地址 下载最新的dmg文件
  • 打开dmg文件,将其中的class-dump拷贝到目录中,比如$HOME/custom-tool/bin目录下
  • 打开~/.bash_profile文件:vi ~/.bash_profile,在文件最上方加一行:export PATH=$HOME/custom-tool/bin/:$PATH,然后保存并退出
  • 执行source ~/.bash_profile
  • 至此,class-dump工具生效。

3、使用

  • 获取ipa文件,修改后缀名为.zip,解压后,获取Payload文件中的app文件;

  • 需要注意的是,从App Store下载的app文件都是经过加密的,可执行文件被加上了一层外壳,class-dump无法直接作用于这样的文件。需要使用其它方式将外壳破坏才可以。

  • 将app文件放到指定目录下,进入该目录,执行如下命令

    # 导出Mach-O头文件(头文件内容按名字排序)
    class-dump -H Mach-O文件路径 -o 头文件存放目录
    
    • -H 表示要生成头文件
    • -o用于制定头文件的存放目录
  • 补充统计文件和文件夹数的命令

    # 查看某个文件下的文件个数,包括子文件里的
    ls -lR|grep "^-"|wc -l
    
    # 查看某文件下的文件夹的个数,包括子文件夹里的
    ls -lR|grep "^d"|wc -l
    

六、Link Map File

1、概述

  • 源码经过编译阶段,每个类会生成对应的.o文件(目标文件);然后在链接阶段,把.o文件和动态库链接在一起,最终生成可执行文件;
  • Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,里面记录了可执行文件的路径、CPU架构、目标文件、符号等信息。
  • 通过Link Map File可以了解内存分段、分区、分析可执行文件中类或库占用空间(可以知道App瘦身)
  • Link Map File可以设置 工程->Build Setting->Write Link Map File为YES,Build后生成Link Map File文件的功能;还可以通过设置Path to Link Map File,指定Link Map File存放的路径。

2、Link Map File的重要组成

  • Path & Arch:Path是可执行文件的路径,Arch是架构类型。

    # Path: /Users/xxx/Library/Developer/Xcode/DerivedData/..../app_name.app/app_name
    # Arch: arm64
    
  • Object Files:生成二进制用到的link单元(包括.o文件和dylib库)的路径和文件编号;通过类编号可以对应到具体的类。在后面的Symbols部分,我们会用到类编号。

    # Object files:
    [  0] linker synthesized
    [  1] /Users/xxxx/Library/Developer/Xcode/DerivedData/..../AppDelegate.o
    [  2] /Users/xxxx/Library/Developer/Xcode/DerivedData/..../main.o
    # ...
    
  • Sections: 记录Mach-O中每个Segment/section的地址范围。Mach-O中有三类的Segement,Segement划分成了不同的Section,不同的Section存储着不同的信息:Segement主要有三类:__TEXT__DATA __LINKEDIT

    • __TEXT包含 Mach header,被执行的代码和只读常量(如C 字符串),只读可执行
    • __DATA 包含全局变量,静态变量等,可读写
    • __LINKEDIT 包含包含了加载程序的『元数据』,比如函数的名称和地址,只读。
    # 第一列是Section起始位置,第二列是Section占用内存大小,第三列是Segment类型,第四列是Section类型。
    # Sections:
    # Address	Size    	Segment	Section
    0x100002780 0x0129617D  __TEXT  __text
    0x1012988FE 0x000015E4  __TEXT  __stubs
    # ...
    
  • Symbols: 按顺序记录每个符号的地址范围

    # Symbols:
    // __text代码区
    # Address   Size        File  Name
    0x100002780 0x00000450  [  2] -[UIButton(SSEdgeInsets) setImageUpTitleDownWithSpacing:]
    0x100002BD0 0x00000070  [  2] _UIEdgeInsetsMake
    # ...
    
    • 根据Address确定分布的区域,如__TEXT段的__text区(存储着代码),__TEXT段的__objc_methname区(存储着方法名)、__DATA的__objc_classlist区(存储所有的类)等;
    • 根据Address ,还可以通过符号表找到对应出具体的方法名Name(方法名越长,最终占用的内存也越大)
    • 根据File编号找到代码属于哪个类;
    • __objc_classlist区的size值都是8,区域里存储的值都是一个指针,指向了类的虚拟地址。

3、功能

  • 分析二进制中类和库大小:在Symbols部分,我们可以把类编号相同的size加起来,可以计算出类的大小;将同一个库中类大小统计在一起,可以计算库的大小。现成分析工具LinkMap
  • 找到未引用的类:利用_objc_classname (所有类名)和__objc_classrefs(引用到的类)的差集找到未引用的类(未引用的类未必是未使用的类)
  • 找到未引用的方法:_objc_methname(所有的方法)和__objc_selrefs(引用的方法)的差别,找到未引用的方法(未引用的方法未必是未使用的方法)
  • Link Map File还有很多可挖掘的用处

历史文章

iOS App瘦身小记 -- 基本给出了App瘦身一些建议

PNG图片原理二三事 -- 基本介绍了PNG原理,然后就对App瘦身中图片压缩佛系了

文档参考

Apple 将 iOS AppStore 下载限制从 150M 提高至 200M

iOS逆向 class-dump

iOS代码瘦身实践:删除无用的类

当我们谈论iOS瘦身的时候,我们到底在谈论些什么

Mac查看文件内容常用的命令小结

分析Mach-O文件

iOS中的可执行文件

iOS 指令集架构 armv6、armv7、armv7s、arm64、arm64e、x86_64、i386

解读 Mach-O 文件格式

趣探 Mach-O:文件格式分析