mach-o文件分析多余的类和方法.md
背景
最近做包大小优化,在做项目代码优化时,其中有一个过程是分析Mach-O文件,看网上很多文章都说通过otool分析Mach-O,获取__objc_classrefs、__objc_classlist等,然后找出无用类和无用方法。
比如:无用类通过 otool 逆向Mach-O文件 __DATA.__objc_classlist段和__DATA.__objc_classrefs 段获取所有 OC 类和被引用的类,两个集合差值为无用类集合,结合 nm -nm 得到地址和对应类名符号化无用类类名
来自干货!京东商城iOS App瘦身实践
又或者结合LinkMap文件的__TEXT.__text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)
来自iOS微信安装包瘦身
上面那些话看起来简简单单的,但是笔者操作起来确遇到了很多困难,首先otool是什么?然后__DATA.__objc_classlist是什么?哪里来的?怎么跟otool命令结合起来使用?怎么获取差值?怎么结合使用正则表达式,等等?笔者在没有大佬带领的情况下,只能是一步步趟过来。
于是笔者这两天就自己小马过河,实践了一下,做成了一个类似LinkMap分析的工具——OtoolAnalyse,分享一下具体的实现过程和原理。
主要涉及到otool命令的简单使用、OtoolAnalyse的实现原理两部分。
原理
首先来看Mach-O
是什么,Mach-O
是Mach Object
文件格式的缩写,是一种记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。
- Mach Header: 描述 Mach-O 的CPU架构、文件类型、加载命令等信息
- Load Command: 描述文件中数据等具体组织结构,不同数据类型使用不同等加载命令表示
- Data: Data中每一个段(Segment)的数据保存在此,段用来存放数据和代码
列举Data常见的Section,来自Mach-O 文件格式探索
表头 | 表头 |
---|---|
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 类名称 |
__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_selrefs | Objective-C 方法引用 |
__DATA.__objc_protorefs | Objective-C 原型引用 |
__DATA.__objc_superrefs | Objective-C 超类引用 |
实现
Mach-O文件获取:Xcode打包好的iPA,改后缀名为.zip,然后解压缩得到payload文件夹,其中有xxx.app,右键显示包内容,其中有xxx的exec文件,即是Mach-O文件。
otool命令简单使用
比如项目名字为TestClass,进入TestClass exec所在的文件夹
- otool符号格式化,输出项目的类结构及定义的方法
1 |
|
- 查看链接了哪些库
1 |
|
- 筛选是否链接了某个指定的库,比如CoreFoundation
1 |
|
- 查看Mach-O所有类集合
1 |
|
- 查看Mach-O所有使用类的集合
1 |
|
- 查看Mach-O所有使用方法的集合
1 |
|
- 查看c语言字符串
1 |
|
到这里为止,otool是什么?__DATA.__objc_classlist是什么?哪里来的?怎么跟otool命令结合起来使用?这几个问题解决了。但是接下来的,怎么获取差值?怎么结合使用正则表达式?要怎么解决呢?
iOS代码瘦身实践:删除无用的类这篇文章里使用python代码有实现的过程。但是笔者走了另一条路,这里分享一下,希望大家多多指点。
OtoolAnalyse的实现原理
首先,参考otool的命令otool -arch arm64 -ov TestClass > otool.txt
,生成otool.txt
打开otool.txt,搜索Contents of (__DATA
,会发现
Contents of (__DATA_CONST,__objc_classlist) section
或者Contents of (__DATA,__objc_classlist) section
Contents of (__DATA,__objc_classrefs) section
Contents of (__DATA,__objc_superrefs) section
Contents of (__DATA,__objc_catlist) section
Contents of (__DATA_CONST,__objc_protolist) section
或者Contents of (__DATA,__objc_protolist) section
Contents of (__DATA,__objc_selrefs) section
Contents of (__DATA_CONST,__objc_imageinfo) section
结合下面的表格来看,就能知道每个section代表的含义是什么了。
表头 | 表头 |
---|---|
Section | 用途 |
__DATA.__objc_classlist | Objective-C 类列表 |
__DATA.__objc_classrefs | Objective-C 类引用 |
__DATA.__objc_superrefs | Objective-C 超类引用 |
__DATA.__objc_catlist | Objective-C category列表 |
__DATA.__objc_protolist | Objective-C 原型 |
__DATA.__objc_selrefs | Objective-C 方法引用 |
__DATA.__objc_imginfo | Objective-C 镜像信息 |
分析无用类
获取__objc_classlist
来看__objc_classlist
所在的section
1 |
|
这里我们通过单个类的信息结构,可以看出其中包含类的地址、类的名字、父类的地址,而笔者想做的是通过固定的代码获取类的信息,然后放到字典中,直到__objc_classlis
这个section结束,然后就获取了所有类名字和地址。
那要怎么做呢?由于文件不是固定的json格式,所以这里难住了,没办法取对应的信息。笔者对比多个类结构,希望能总结出来固定的规律。
参考LinkMap项目的symbolMapFromContent方法实现,笔者发现,它的匹配是读取文件,然后单行单行,匹配文案,设置标记位,从而解析对应信息。代码如下
1 |
|
所以,笔者发现,如果按照同样的逻辑,单行读取+标记位时,同样的逻辑也可以使用,即每次000000010
开头时,说明是一个新类的开始,存储对应的地址,设置可以存储名字标记位,然后读取到name时,就用{ classAddress: className }
的格式存储下来,并把标识位清除,直到下一行包含000000010
时,再重置标识位为YES。代码如下:
1 |
|
然后怎么调试这个代码的正确与否?
笔者这时候想到了借助LinkMap的UI,因为同样都是需要选择文件,读取文件,而且笔者也想做分析之后结果显示,外加最后输出结果到文件,一整套的逻辑。所以,笔者就想到了把LinkMap的内部实现改掉。
首先第一步,注释掉checkContent:
的判断,然后analyze:
方法中把调用symbolMapFromContent:
的地方改为调用classListFromContent:
,断点调试看classListFromContent:
方法是否正确?那如何判断这个方法是否正确呢?最简单的方法根据个数来,经过classListFromContent:
得到的NSMutableDiction的数据的个数,和直接搜索otool.txt
文件中Contents of (__DATA_CONST,__objc_classlist) section
部分000000010
的个数一致,就说明没有问题。具体如下:
- 笔者把
otool.txt
文件中除去Contents of (__DATA_CONST,__objc_classlist) section
部分删掉,然后搜索000000010
看有多少个。
- 笔者把
- 运行LinkMap项目,选择otool.txt,然后断点看
classListFromContent:
方法的输出
- 运行LinkMap项目,选择otool.txt,然后断点看
- 两个结果个数一致,笔者认为代码运行正确。
获取__objc_classrefs
来看__objc_classrefs
所在的section
1 |
|
同样,先来分析上述代码,可以看到单行信息中,后面的部分要不是系统信息,要不是类地址。如下:
所以,笔者采取同样的处理逻辑,读取Contents of (__DATA,__objc_classrefs) section
的内容,单行单行读取,判断如果包含0x100
,说明是类地址,存储到数组里。实现如下
1 |
|
然后校验上面方法的正确与否,去除除了Contents of (__DATA,__objc_classrefs) section
的之外的内容,然后搜索0x100
的个数,与classRefsFromContent:
方法返回的个数对比,相同则说明方法无错误。
取差值,获取无用类
在LinkMap中的analyze:
方法中,调用classListFromContent:
和classRefsFromContent:
,获取到了所有类和已引用类后,所有类存储是{ classAddress: className }
,已引用类存储的是[classAddress]
,去重后,遍历去重后的已引用类,然后把所有在已引用的地址从所有类中移除。最后所有类中剩下的就是无用的类。代码如下
1 |
|
最后测试输出结果如下,可以看到输出结果的结构,但是其中ViewController是Storyboard引用的,SceneDelegate是Info.plist文件中配置的,但是都被识别为无使用类。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的类。
分析无用方法
无用方法的分析与类稍有不同,因为没有直接获取所有方法的地方,__objc_selrefs
是所有引用到的方法,因此笔者想到的是,用__objc_classlist
中的BaseMethods、InstanceMethods以及ClassMethods中的数据,作为所有方法的集合,然后和引用的方法做差值,最终得到无用方法。
获取__objc_selrefs
来看__objc_selrefs
所在的section
1 |
|
可以看到,这部分的数据比较简单,前面是地址,后面是方法名字,这里遍历每一行数据,然后直接以{ methodAddress: methodName }
的方式存起来。代码如下:
1 |
|
获取所有方法列表
这部分稍有麻烦,笔者想的是用__objc_classlist
中的BaseMethods、InstanceMethods以及ClassMethods中的数据,作为所有方法的集合,所以先来看文件结构,总结出来规律
1 |
|
上面的文件能看出来什么规律?脑壳疼,笔者想获取的是BaseMethods后面的name行的数据,而且笔者还希望能把这个方法跟类关联起来,这样最后输出查找的时候也比较方便。
笔者总结出来的规律如下
- 按照一行行的读取逻辑来,读到了data,然后读到了name,这时候name是类名字。
- 再接着往下读,读到了baseMethods或者InstanceMethods或者Class Methods,再然后读到了name,这时候name中是方法名字和方法地址。
- 再接着往下读,读到了data,重复步骤1
用代码逻辑实现就是,设置两个标志位,一个标记是类名,一个标记是方法;读到了data之后,把第一个标记置为YES,然后判断第一个标记位YES时,读到了name就更新类名;读到了包含Methods之后,把第一个标记置为NO,第二个标记置为YES,然后判断是第二个标记位YES时,就存储方法名和方法地址。最终数据以{ className:{ address: methodName } }
存储。代码如下
1 |
|
取差值,获取无用方法
在LinkMap中的analyze:
方法中,调用allSelRefsFromContent:
和selRefsFromContent:
,获取到了所有方法和已引用方法后,所有方法存储是{ className:{ address: methodName } }
,已引用方法存储的是{ methodAddress: methodName }
,遍历去重后的已引用方法,然后把所有在已引用的地址从所有方法中移除。最后所有方法中剩下的就是无用的方法。代码如下
1 |
|
最后测试输出结果如下,可以看到输出结果的结构,其中AppDelegate和SceneDelegate的代理方法被识别为了多余方法。所以结果打印出来后,删除前需要确认。也可以在上面的获取差值代码中过滤指定的代理方法。
最后
完整的项目地址OtoolAnalyse,笔者用这样方法,分析出来了项目中无用的类、无用的方法,删除前要注意先确认。项目还有待完善的地方,比如系统方法的过滤,基类的判断逻辑,等等,留待后续补充。但整体分析的逻辑如上,笔者趟过了河,先分享为敬,😄。