前言
本文将提供一种静态分析的方式,用于查找可执行文件mach-o中未使用的类,源码链接:xuezhulian/classunref 。
mach-o文件中__data __objc_classrefs段记录了引用类的地址,__data __objc_classlist段记录了所有类的地址,取差集可以得到未使用的类的地址,然后进行符号化,就可以得到未被引用的类信息。
引用类地址
可以通过mac自带的工具otool打印mach-o中的段信息,需要注意的是模拟器和真机对应的可执行文件,数据的存储方式不同需要加以区分。
可以通过file命令获取到arch。
1
2
3
|
#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() |
在取类地址的时候区分x86_64和arm。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
def pointers_from_binary(line, binary_file_arch): 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 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 pointers.add(line[1] + line[0]) pointers.add(line[3] + line[2]) return pointers return none |
通过otool -v -s __data __objc_classrefs获取到引用类的地址。
1
2
3
4
5
6
7
|
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 |
所有类地址
通过otool -v -s __data __objc_classlist获取所有类的地址。
1
2
3
4
5
6
7
|
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 |
取差集
用所有类信息减去引用类的信息,此时我们可以拿到未使用类的地址信息。
1
|
unref_pointers = class_list_pointers(path, binary_file_arch) - class_ref_pointers(path, binary_file_arch) |
符号化
通过nm -nm命令可以得到地址和对应的类名字。
1
2
3
4
5
6
7
8
9
10
11
|
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 |
过滤
在实际分析的过程中发现,如果一个类的子类被实例化,父类未被实例化,此时父类不会出现在__objc_classrefs这个段里,在未使用的类中需要将这一部分父类过滤出去。使用otool -ov可以获取到类的继承关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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 |
为了防止一些三方库的误伤,还可以去过滤一些前缀,或者是是仅保留带有某些前缀的类。
1
2
3
4
5
6
7
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) |
最终结果保存在脚本目录下。
1
2
3
4
5
6
7
|
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() |
这个思路在一定程度上能够减少代码的冗余,减小包的体积。因为是静态分析,不能包括动态调用的情况,对于需要删除的类需要进一步的确认。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对服务器之家的支持。
原文链接:https://juejin.im/post/5d6482b0e51d456209238885