分析Mach-o文件获取无用代码和类
Mach-O
Mach-O是Mach object的缩写,是Mac\iOS上用于存储程序、库的标准格式
属于Mach-O格式的文件类型有
常见的Mach-O文件类型
Mach-O是Mach object的缩写,是Mac\iOS上用于存储程序、库的标准格式
属于Mach-O格式的文件类型有
MH_OBJECT | MH_EXECUTE | MH_DYLIB | MH_DYLINKER | MH_DSYM |
---|---|---|---|---|
目标文件(.o) | 可执行文件 | 动态库文件 | 动态链接编辑器 | 存储着二进制文件符号信息的文件 |
静态库文件(.a),静态库其实就是N个.o合并在一起 | .app/xx | .dylib .framework/xx | /usr/lib/dyld | .dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息) |
- 目标文件是代码文件和可执行文件的中间产物.C -> .O -> 可执行文件(clang -c 文件名)
- clang -o 生成文件名 代码文件名 直接生成可执行文件
- cd usr/bin 查找动态库
- file 文件名 查看文件类型
Mach-O的基本结构
官方描述https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/MachOTopics/0-Introduction/introduction.html
一个Mach-O文件包含3个主要区域:
-
Header
-
文件类型、目标架构类型等
-
Load commands
-
描述文件在虚拟内存中的逻辑结构、布局
-
Raw segment data
-
在Load commands中定义的Segment的原始数据
image
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 | objc类列表 |
__DATA.__objc_protolist | objc协议列表 |
__DATA.__objc_imginfo | objc 镜像信息 |
__DATA.__objc_selfrefs | 引用到的objc方法 |
__DATA.__objc_protorefs | 引用到的objc协议 |
__DATA.__objc_superrefs | objc超类引用 |
窥探Mach-O的结构
命令行工具
file:查看Mach-O的文件类型
file 文件路径
otool:查看Mach-O特定部分和段的内容
lipo:常用于多架构Mach-O文件的处理
查看架构信息:lipo -info 文件路径
导出某种特定架构:lipo 文件路径 -thin 架构类型 -output 输出文件路径
合并多种架构:lipo 文件路径1 文件路径2 -output 输出文件路径
GUI工具
MachOView(https://github.com/gdbinit/MachOView)
Universal Binary(通用二进制文件)
- 通用二进制文件
- 同时适用于多种架构的二进制文件
- 包含了多种不同架构的独立的二进制文件
- 因为需要储存多种架构的代码,通用二进制文件通常比单一平台二进制的程序要大
- 由于两种架构有共同的一些资源,所以并不会达到
otool
image.png- -f print the fat headers 查找通用二进制文件header
- -h print the mach header 打印armv7,arm64里面的头信息
- -l print the load commands 打印段信息
- -L print shared libraries used 打印引用动态库
查找无用类
Mach-o文件中 __DATA __objc_classrefs
段记录了引用类的地址,__DATA __objc_classlist
段记录了所有类的地址,取差集可以得到未使用的类的地址,然后进行符号化,就可以得到未被引用的类信息。
1、通过file命令获取到arch。
#binary_file_arch: distinguish Big-Endian and Little-Endian
#file -b output example: Mach-O 64-bit executable arm64
binary_file_arch = os.popen('file -b ' + path).read().split(' ')[-1].strip()
2、在取类地址的时候区分x86_64和arm
def pointers_from_binary(line, binary_file_arch):
if len(line) < 16:
return None
line = line[16:].strip().split(' ')
pointers = set()
if binary_file_arch == 'x86_64':
#untreated line example:00000001030cec80 d8 75 15 03 01 00 00 00 68 77 15 03 01 00 00 00
if len(line) != 16:
return None
pointers.add(''.join(line[4:8][::-1] + line[0:4][::-1]))
pointers.add(''.join(line[12:16][::-1] + line[8:12][::-1]))
return pointers
#arm64 confirmed,armv7 arm7s unconfirmed
if binary_file_arch.startswith('arm'):
#untreated line example:00000001030bcd20 03138580 00000001 03138878 00000001
if len(line) != 4:
return None
pointers.add(line[1] + line[0])
pointers.add(line[3] + line[2])
return pointers
return None
3、通过otool -v -s __DATA __objc_classrefs获取到引用类的地址
def class_ref_pointers(path, binary_file_arch):
ref_pointers = set()
lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classrefs %s' % path).readlines()
for line in lines:
pointers = pointers_from_binary(line, binary_file_arch)
ref_pointers = ref_pointers.union(pointers)
return ref_pointers
4、获取所有的类
def class_list_pointers(path, binary_file_arch):
list_pointers = set()
lines = os.popen('/usr/bin/otool -v -s __DATA __objc_classlist %s' % path).readlines()
for line in lines:
pointers = pointers_from_binary(line, binary_file_arch)
list_pointers = list_pointers.union(pointers)
return list_pointers
5、取差集
用所有类信息减去引用类的信息,此时我们可以拿到未使用类的地址信息。
unref_pointers = class_list_pointers(path, binary_file_arch) - class_ref_pointers(path, binary_file_arch)
6、符号化
通过nm -nm命令可以得到地址和对应的类名字
def class_symbols(path):
symbols = {}
#class symbol format from nm: 0000000103113f68 (__DATA,__objc_data) external _OBJC_CLASS_$_EpisodeStatusDetailItemView
re_class_name = re.compile('(\w{16}) .* _OBJC_CLASS_\$_(.+)')
lines = os.popen('nm -nm %s' % path).readlines()
for line in lines:
result = re_class_name.findall(line)
if result:
(address, symbol) = result[0]
symbols[address] = symbol
return symbols
7、过滤
在实际分析的过程中发现,如果一个类的子类被实例化,父类未被实例化,此时父类不会出现在__objc_classrefs这个段里,在未使用的类中需要将这一部分父类过滤出去。使用otool -oV可以获取到类的继承关系。
def filter_super_class(unref_symbols):
re_subclass_name = re.compile("\w{16} 0x\w{9} _OBJC_CLASS_\$_(.+)")
re_superclass_name = re.compile("\s*superclass 0x\w{9} _OBJC_CLASS_\$_(.+)")
#subclass example: 0000000102bd8070 0x103113f68 _OBJC_CLASS_$_TTEpisodeStatusDetailItemView
#superclass example: superclass 0x10313bb80 _OBJC_CLASS_$_TTBaseControl
lines = os.popen("/usr/bin/otool -oV %s" % path).readlines()
subclass_name = ""
superclass_name = ""
for line in lines:
subclass_match_result = re_subclass_name.findall(line)
if subclass_match_result:
subclass_name = subclass_match_result[0]
superclass_match_result = re_superclass_name.findall(line)
if superclass_match_result:
superclass_name = superclass_match_result[0]
if len(subclass_name) > 0 and len(superclass_name) > 0:
if superclass_name in unref_symbols and subclass_name not in unref_symbols:
unref_symbols.remove(superclass_name)
superclass_name = ""
subclass_name = ""
return unref_symbols
8、过滤
为了防止一些三方库的误伤,还可以去过滤一些前缀,或者是是仅保留带有某些前缀的类
for unref_pointer in unref_pointers:
if unref_pointer in symbols:
unref_symbol = symbols[unref_pointer]
if len(reserved_prefix) > 0 and not unref_symbol.startswith(reserved_prefix):
continue
if len(filter_prefix) > 0 and unref_symbol.startswith(filter_prefix):
continue
unref_symbols.add(unref_symbol)
9、保存
最终结果保存在脚本目录下
script_path = sys.path[0].strip()
f = open(script_path+"/result.txt","w")
f.write( "unref class number: %d\n" % len(unref_symbles))
f.write("\n")
for unref_symble in unref_symbles:
f.write(unref_symble+"\n")
f.close()
LinkMap使用
1、XCode开启编译选项Write Link Map File
XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为YES,并指定好linkMap的存储位置
特别提醒:打包发布前记得还原为NO
2、编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File位于
~/Library/Developer/Xcode/DerivedData/XXX-XXXXXXXXXXXX/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/
这个LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情。
LinkMap结构
image.png- Object File:包含了代码工程的所有文件
- Section:描述了代码段在生成的 Mach-O 里的偏移位置和大小
- Symbols:会列出每个方法、类、Block,以及它们的大小
1、首先列出来的是目标文件列表(中括号内为文件编号):
# Path: /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Products/Debug-iphoneos/JRAPP.app/JRAPP
# Arch: arm64
# Object files:
[ 0] linker synthesized
[ 1] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRRepaymentBankBindingPTwoPView.o
[ 2] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRApplyAddMountTypeSelectViewController.o
[ 3] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/MQTTSSLSecurityPolicyEncoder.o
[ 4] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRICBCFaceSignedShowModel.o
[ 5] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRUsersSignUpViewController.o
[ 6] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRUrgentInfoManager.o
[ 7] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRPersonInfoCityModel.o
[ 8] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRPayDownPaymentInputPayMoneyCell.o
[ 9] /Users/zhaoruisheng/Library/Developer/Xcode/DerivedData/JRAPP-diwjmzrfywmbpjbotgfzkcziuekb/Build/Intermediates.noindex/JRAPP.build/Debug-iphoneos/JRAPP.build/Objects-normal/arm64/JRDigitalCompassSearchRangeSelectTableViewCell.o
2、接着是一个段表,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值)
# Sections:
# Address Size Segment Section
0x100004550 0x014CA9F4 __TEXT __text
0x1014CEF44 0x00004038 __TEXT __stubs
0x1014D2F7C 0x00003798 __TEXT __stub_helper
0x1014D6714 0x0008D528 __TEXT __gcc_except_tab
0x101563C40 0x0004C9B8 __TEXT __const
0x1015B05F8 0x00091381 __TEXT __objc_methname
0x10164197A 0x0000C592 __TEXT __ustring
0x10164DF10 0x000A9D09 __TEXT __cstring
0x1016F7C19 0x0000EF1E __TEXT __objc_classname
0x101706B37 0x00015537 __TEXT __objc_methtype
0x10171C070 0x00037C88 __TEXT __unwind_info
0x101753CF8 0x0001C2FC __TEXT __eh_frame
0x101770000 0x00001838 __DATA __got
0x101771838 0x00002AD0 __DATA __la_symbol_ptr
0x101774308 0x00000128 __DATA __mod_init_func
0x101774430 0x00000008 __DATA __mod_term_func
0x101774440 0x0004D238 __DATA __const
0x1017C1678 0x0004DB80 __DATA __cfstring
0x10180F1F8 0x000045E0 __DATA __objc_classlist
首列是数据在文件的偏移位置,第二列是这一段占用大小,第三列是段类型,代码段和数据段,第四列是段名称。
每一行的数据都紧跟在上一行后面,如第二行__stubs的地址0x10304FD9C就是第一行__text的地址0x100005B00加上大小0x0304A29C,整个可执行文件大致数据分布就是这样。
这里可以清楚看到各种类型的数据在最终可执行文件里占的比例,例如__text表示编译后的程序执行语句,__data表示已初始化的全局变量和局部静态变量,__bss表示未初始化的全局变量和局部静态变量,__cstring表示代码里的字符串常量,等等。
3、接着就是按上表顺序,列出具体的按每个文件列出每个对应字段的位置和占用空间
# Symbols:
# Address Size File Name
0x100004550 0x00000080 [ 1] +[JRRepaymentBankBindingPTwoPView initRepaymentBankBindingPTwoPViewFrame:]
0x1000045D0 0x000000DC [ 1] -[JRRepaymentBankBindingPTwoPView initWithFrame:]
0x1000046AC 0x00000094 [ 1] -[JRRepaymentBankBindingPTwoPView cancelBtnAction]
0x100004740 0x00000234 [ 1] -[JRRepaymentBankBindingPTwoPView loadTime]
0x100004974 0x000001E8 [ 1] ___43-[JRRepaymentBankBindingPTwoPView loadTime]_block_invoke
0x100004B5C 0x000000C8 [ 1] ___43-[JRRepaymentBankBindingPTwoPView loadTime]_block_invoke_2
0x100004C24 0x0000006C [ 1] ___copy_helper_block_e8_32s40r
0x100004C90 0x0000004C [ 1] ___destroy_helper_block_e8_32s40r
0x100004CDC 0x00000044 [ 1] ___43-[JRRepaymentBankBindingPTwoPView loadTime]_block_invoke.17
0x100004D20 0x0000004C [ 1] ___copy_helper_block_e8_32s
0x100004D6C 0x00000030 [ 1] ___destroy_helper_block_e8_32s
0x100004D9C 0x000004A8 [ 1] -[JRRepaymentBankBindingPTwoPView isHaveSendMessage:]
0x100005244 0x000001E0 [ 1] -[JRRepaymentBankBindingPTwoPView loadPhone:title:message:]
0x100005424 0x00000094 [ 1] -[JRRepaymentBankBindingPTwoPView bottomButtonAction]
0x1000054B8 0x000001E0 [ 1] -[JRRepaymentBankBindingPTwoPView dissMissAlertView]
0x100005698 0x00000094 [ 1] -[JRRepaymentBankBindingPTwoPView topButtonAction]
0x10000572C 0x00000EE0 [ 1] -[JRRepaymentBankBindingPTwoPView creatSubviews]
0x10000660C 0x0000004C [ 1] _CGRectMake
0x100006658 0x00000190 [ 1] -[JRRepaymentBankBindingPTwoPView creatButtonFrame:]
0x1000067E8 0x0000004C [ 1] -[JRRepaymentBankBindingPTwoPView dealloc]
0x100006834 0x0000003C [ 1] -[JRRepaymentBankBindingPTwoPView cancelBtnBlock]
0x100006870 0x00000044 [ 1] -[JRRepaymentBankBindingPTwoPView setCancelBtnBlock:]
0x1000068B4 0x0000003C [ 1] -[JRRepaymentBankBindingPTwoPView bottomBtnBlock]
0x1000068F0 0x00000044 [ 1] -[JRRepaymentBankBindingPTwoPView setBottomBtnBlock:]
0x100006934 0x0000003C [ 1] -[JRRepaymentBankBindingPTwoPView topBtnBlock]
0x100006970 0x00000044 [ 1] -[JRRepaymentBankBindingPTwoPView setTopBtnBlock:]
同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。
4、已废弃&多余重复的字段
# Dead Stripped Symbols:
# Size File Name
<<dead>> 0x0000000B [ 2] literal string: whiteColor
<<dead>> 0x00000014 [ 2] literal string: setBackgroundColor:
<<dead>> 0x00000012 [ 2] literal string: stringWithFormat:
<<dead>> 0x00000009 [ 2] literal string: setText:
<<dead>> 0x00000007 [ 2] literal string: length
<<dead>> 0x0000000C [ 2] literal string: addSubview:
<<dead>> 0x0000000F [ 2] literal string: initWithFrame:
<<dead>> 0x0000000E [ 2] literal string: setTextColor:
<<dead>> 0x00000016 [ 2] literal string: boldSystemFontOfSize:
<<dead>> 0x00000009 [ 2] literal string: setFont:
<<dead>> 0x0000000E [ 2] literal string: .cxx_destruct
<<dead>> 0x00000001 [ 2] literal string:
<<dead>> 0x00000005 [ 2] literal string: %@%@
<<dead>> 0x00000008 [ 2] literal string: orderId
<<dead>> 0x00000008 [ 2] literal string: @16@0:8
<<dead>> 0x0000000B [ 2] literal string: v24@0:8@16
<<dead>> 0x00000011 [ 2] literal string: v40@0:8@16@24@32`
得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。
Objective-C 中的方法都会通过 objc_msgSend 来调用,而 objc_msgSend 在 Mach-O 文件里是通过 _objc_selrefs 这个 section 来获取 selector 这个参数的。
所以,_objc_selrefs 里的方法一定是被调用了的。_objc_classrefs 里是被调用过的类, objc_superrefs 是调用过 super 的类(继承关系)。通过 _objc_classrefs 和 _objc_superrefs,我们就可以找出使用过的类和子类。
APPCode
通过 AppCode 查找无用代码
AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。
说明:AppCode检测出了实际上需要的大部分场景的问题,但是由于 Objective-C 是一门动态性语言,所以 AppCode 检测出无用的方法等都需要工程师自己再次确认后删除。(在我们的工程中有一些和 H5 交互的桥接方法,因此 AppCode 视为 Unused Method,但是你删除的话,那就自己哭去吧 😭)。使用 AppCode 的时候如果工程比较大,则整个 code inspect 会非常耗时
无用类:Unused class 是无用类,Unused import statement 是无用类引入声明,Unused property 是无用的属性;
无用方法:Unused method 是无用的方法,Unused parameter 是无用参数,Unused instance variable 是无用的实例变量,Unused local variable 是无用的局部变量,Unused value 是无用的值;
无用宏:Unused macro 是无用的宏。
无用全局:Unused global declaration 是无用全局声明。
主意需要人工二次确认
- JSONModel 里定义了未使用的协议会被判定为无用协议;
- 如果子类使用了父类的方法,父类的这个方法不会被认为使用了;
- 通过点的方式使用属性,该属性会被认为没有使用;* 使用 performSelector 方式调用的方法也检查不出来,比如 self performSelector:@selector(arrivalRefreshTime);
- 运行时声明类的情况检查不出来。比如通过 NSClassFromString 方式调用的类会被查出为没有使用的类,比如 layerClass = NSClassFromString(@“SMFloatLayer”)。还有以 [[self class] accessToken] 这样不指定类名的方式使用的类,会被认为该类没有被使用。像 UITableView 的自定义的 Cell 使用 registerClass,这样的情况也会认为这个 Cell 没有被使用。