iOS 收藏篇iOS开发技术技术关注

分析Mach-o文件获取无用代码和类

2019-12-23  本文已影响0人  Rathen

Mach-O

Mach-O是Mach object的缩写,是Mac\iOS上用于存储程序、库的标准格式
属于Mach-O格式的文件类型有

image

常见的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的崩溃信息)

Mach-O的基本结构

官方描述https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/MachOTopics/0-Introduction/introduction.html
一个Mach-O文件包含3个主要区域:

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

查找无用类

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

image.png
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

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 ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。

image.png

说明: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 是无用全局声明。

主意需要人工二次确认

上一篇下一篇

猜你喜欢

热点阅读