美文网首页iOS 收藏篇iOS开发技术技术关注
分析Mach-o文件获取无用代码和类

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

作者: Rathen | 来源:发表于2019-12-23 10:24 被阅读0次

    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的崩溃信息)
    • 目标文件是代码文件和可执行文件的中间产物.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

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

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

    主意需要人工二次确认

    • JSONModel 里定义了未使用的协议会被判定为无用协议;
    • 如果子类使用了父类的方法,父类的这个方法不会被认为使用了;
    • 通过点的方式使用属性,该属性会被认为没有使用;* 使用 performSelector 方式调用的方法也检查不出来,比如 self performSelector:@selector(arrivalRefreshTime);
    • 运行时声明类的情况检查不出来。比如通过 NSClassFromString 方式调用的类会被查出为没有使用的类,比如 layerClass = NSClassFromString(@“SMFloatLayer”)。还有以 [[self class] accessToken] 这样不指定类名的方式使用的类,会被认为该类没有被使用。像 UITableView 的自定义的 Cell 使用 registerClass,这样的情况也会认为这个 Cell 没有被使用。

    相关文章

      网友评论

        本文标题:分析Mach-o文件获取无用代码和类

        本文链接:https://www.haomeiwen.com/subject/suainctx.html