Mach-O文件周边二三事
2020/4/1 23:01:28
本文主要是介绍Mach-O文件周边二三事,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一、iOS App大小限制变化
1、App下载大小的限制变化
-
Apple不断放宽在蜂窝网络下,从AppStore下载App的大小限制,大App们的大小都逼近或超过200MB,甚至突破250MB;
-
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下载大小的限制,主要流量够用,随便下;
-
2020年4月1号了,我们日常用得比较多的App中,微信/手淘/美团App大小突破250MB;滴滴/抖音/快手App大小突破200MB;支付宝App逼近200MB(190MB+),美团外卖App一股清流,才115MB+。
2、可执行文件大小的限制变化
-
根据Apple的审核要求,上传App Store的iap的可执行文件有大小限制,这里的可执行文件大小不是指二进制(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 保留字段 */ }; 复制代码
-
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_64
和
LC_SEGMENT` 是加载的主要命令, 他们指导内核来设置进程的内存空间;
- 加载命令告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的;LC_SEGMENT_64
-
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的指令集。
三、分析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安装包瘦身小记 -- 此文写的比较早,缺失了二进制瘦身这个大内容,后面补充下。
文档参考
Apple 将 iOS AppStore 下载限制从 150M 提高至 200M
iOS 指令集架构 armv6、armv7、armv7s、arm64、arm64e、x86_64、i386
这篇关于Mach-O文件周边二三事的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值