Mach-O文件周邊二三事

1、iOS App大小限制變化

一、App下載大小的限制變化

  • Apple不斷放寬在蜂窩網絡下,從AppStore下載App的大小限制,大App們的大小都逼近或超過200MB,甚至突破250MB;html

  • 2013年9月,iOS 7正式版後,蜂窩網絡下App下載大小的限制,從 50 MB 提高至 100 MB;linux

  • 2017年9月,iOS11正式版本後,限制從 100 MB 提高至 150 MB,並在2019年5月下旬,將 150 MB"默默"放寬到200MB;git

  • 2019年9月,iOS13正式版本後,直接放開了蜂窩網絡下App下載大小的限制,主要流量夠用,隨便下;github

  • 2020年4月1號了,咱們平常用得比較多的App中,微信/手淘/美團App大小突破250MB;滴滴/抖音/快手App大小突破200MB;支付寶App逼近200MB(190MB+),美團外賣App一股清流,才115MB+。objective-c

二、可執行文件大小的限制變化

  • 根據Apple的審覈要求,上傳App Store的ipa的可執行文件有大小限制,這裏的可執行文件大小不是指二進制(Mach-O)文件大小,而是指二進制(Mach-O)文件中__TEXT部分的大小。shell

  • IOS 7版本以前, 二進制文件中全部__TEXT部分總和不得超過80MB;瀏覽器

  • iOS 7.X 至 iOS 8.X,二進制文件中,每一個 Architecture Slice(架構片斷)中的__TEXT部分不得超過60MBbash

    • Architecture Slice是針對特定架構的胖二進制佈局文件的一部分。例如,一個胖二進制文件可能會包含針對 32 位和 64 位架構的片斷。
  • iOS 9.0以後,二進制文件中全部__TEXT部分的總和不超過500 MB;具體可參考最大構建版本文件大小微信

  • 2020年4月1號了,幾乎全部的iOS App兼容的最低版本都是iOS 9起步,如:微信/美團/美團外賣iOS App最低支持iOS10,支付寶/手淘/滴滴/抖音/快手iOS App最低支持iOS 9。網絡

三、總結

  • 隨着4G普及,5G到來,流量費用大大下降、Apple放開了對App大小方面的限制、iOS用戶升級系統意願高等因素,iOS開發者對包大小能夠鬆口氣,如沒必要很擔憂超過二進制__TEXT部分的限制,能夠優先業務迭代,有人力的狀況下,再去作包瘦身;
  • 若是追求App的更高品質,在競品中拔得頭籌,仍是須要在包大小方面花很大功夫;通常來講,ROI最高的是無用資源(主要是圖片)的清理,其次是二進制文件大小的優化;二進制文件大小的優化一個是靠優化編譯器選項,一個是清理無用的類、函數和代碼塊等。
  • 網絡上有不少相似的包優化的博客能夠參考,本文就不說了,本文主要介紹Mach-O文件周邊的知識:Mach-O文件自己、 分析工具和Link Map File等。

2、Mach-O文件簡介

一、概述

  • Mach-O格式全稱爲Mach Object文件格式的縮寫,是MacOS或者iOS上可執行的程序格式,相似於Windows上的PE格式 (Portable Executable),linux上的ELF格式 (Executable and Linking Format)。
  • Mach-O文件的分類有以下5類:
    • Executable:應用的可執行文件
    • Dylib Library:動態連接庫(又稱DSO或DLL)
    • Static Library:靜態連接庫
    • Bundle:不能被連接的Dylib,只能在運行時使用dlopen( )加載,可當作macOS的插件
    • Relocatable Object File :可重定向文件類型

二、Mach-O文件的組成

Mach-O文件主要包括三部份內容: Header(頭部)、Load Commands(加載命令)、Data(數據區)

  • Header(頭部),指明瞭 CPU 架構、大小端序、文件類型、Load Commands 個數等一些基本信息,Headers 能幫助校驗 Mach-O 合法性和定位文件的運行環境,64位架構爲例,Header結構定義以下:

    struct mach_header_64 {
        uint32_t    magic;        /* mach magic number identifier 魔數,用於快速確認該文件用於64位仍是32位 */
        cpu_type_t    cputype;    /* cpu specifier,CPU**類型,好比 arm */
        cpu_subtype_t    cpusubtype;    /* machine specifier,對應的具體類型,好比arm6四、armv7 */
        uint32_t    filetype;    /* type of file,文件類型,好比可執行文件、庫文件、Dsym文件,demo中是2 `MH_EXECUTE`,表明可執行文件*/
        uint32_t    ncmds;        /* number of load commands 加載命令條數 */
        uint32_t    sizeofcmds;    /* the size of all the load commands  全部加載命令的大小 */
        uint32_t    flags;        /* flags 標誌位 */
        uint32_t    reserved;    /* reserved  保留字段 */
    };
    複製代碼
  • Load Commands(加載命令),包含 Mach-O 裏命令類型信息,名稱和二進制文件的位置;以64位架構爲例,Load Commands結構定義以下:

    struct segment_command_64 { /* for 64-bit architectures */
        uint32_t    cmd;        /* cmd是Load commands的類型,LC_SEGMENT_64表明將文件中64位的段映射到進程的地址空間*/
        uint32_t    cmdsize;    /* includes sizeof section_64 structs 表明load command的大小 */
        char        segname[16];    /* segment name */
        uint64_t    vmaddr;        /* memory address of this segment 段的虛擬內存地址 */
        uint64_t    vmsize;        /* memory size of this segment  段的虛擬內存大小 */
        uint64_t    fileoff;    /* file offset of this segment 段在文件中偏移量 */
        uint64_t    filesize;    /* amount to map from the file 段在文件中的大小 */
        vm_prot_t    maxprot;    /* maximum VM protection */
        vm_prot_t    initprot;    /* initial VM protection */
        uint32_t    nsects;        /* number of sections in segment 標示了Segment中有多少secetion */
        uint32_t    flags;        /* flags */
    };
    複製代碼
    • 加載命令告訴加載器如何處理二進制數據,有些命令是由內核處理的,有些是由動態連接器處理的;LC_SEGMENT_64LC_SEGMENT` 是加載的主要命令, 他們指導內核來設置進程的內存空間;
  • Data(數據區)由Segment 的數據組成,是 Mach-O中 佔比最多的部分,有代碼有數據,好比符號表。Data 共三個 Segment:__TEXT(包含執行代碼以及其餘只讀數據)、__DATA(程序數據,該段可寫)、__LINKEDIT(包含連接器使用的符號以及其餘表)。

    • 其中,__TEXT 和 __DATA 對應一個或多個 Section,__LINKEDIT 沒有 Section,須要配合 LC_SYMTAB 來解析 symbol table 和 string table。這些裏面是 Mach-O 的主要數據。

    • 以64位架構爲例,Section的結構定義以下:

      struct section_64 { /* for 64-bit architectures */
      	char		sectname[16];	/* name of this section  好比_text、stubs */
      	char		segname[16];	/* segment this section goes in 該section所屬的segment,好比__TEXT*/
      	uint64_t	addr;		/* memory address of this section 該section在內存的起始位置 */
      	uint64_t	size;		/* size in bytes of this section 該section的大小*/
      	uint32_t	offset;		/* file offset of this section 該section的文件偏移*/
      	uint32_t	align;		/* section alignment (power of 2) 字節大小對齊*/
      	uint32_t	reloff;		/* file offset of relocation entries 重定位入口的文件偏移 */
      	uint32_t	nreloc;		/* number of relocation entries 須要重定位的入口數量 */
      	uint32_t	flags;		/* flags (section type and attributes) 包含section的type和attributes*/
      	uint32_t	reserved1;	/* reserved (for offset or index) */
      	uint32_t	reserved2;	/* reserved (for count or sizeof) */
      	uint32_t	reserved3;	/* reserved */
      };
      複製代碼
    • 備註:__TEXT表明的是Segment,小寫的__text表明 Section

三、FatFile/FatBinary

  • FatFile/FatBinary直譯「胖二進制」,是一個由不一樣的編譯架構的Mach-O產物合成的集合體。一個架構的Mach-O只能在相同架構的機器或者模擬器上用,爲了支持不一樣架構須要一個集合體。
  • 這裏的架構是指CPU的指令集,iOS設備使用的是ARM處理器,ARM支持的指令集有兩類:32位ARM指令集(armv7|armv7s)和 64位ARM指令集(arm64和arm64e)
  • 此外,還有i386(32位)和x86_64(64位)這兩個Mac處理器的指令集,iOS模擬器沒有運行ARM指令集,運行在iOS模擬器上的App須要支持i386 or x86_64的指令集。

3、分析Mach-O的基礎命令

一、lipo命令

  • 管理Fat File的工具, 能夠查看CPU架構, 提取特定架構,整合和拆分庫文件

  • 經常使用的方法以下:

    # 【查】看胖二進制支持的CPU架構列表
    lipo -info xxxx.a/xxxx.framework/xxxx
     # 【拆】從胖二進制中提取特定CPU架構的二進制
    lipo lxx.a -thin cpu_type(armv7s/arm64等)  -output xx_cpu_type.a
     # 【合】整合成Fat文件
    lipo -create xxxx1  xxxx2  -output xxxxfat
     #【刪】移除掉特定的cpu架構的文件
    lipo -remove cpu_type(armv7s/arm64等) xxxx -output  xxxx
    複製代碼

二、ar命令

  • 經常使用來建立、修改庫,從庫中提出單個模塊。

  • 常使用ar命令解壓.a文件,可是若是直接解壓第三方SDK的.a文件(如微信SDK),會遇到xxx.a is a fat file (use libtool(1) or lipo(1) and ar(1) on it)的錯誤。

  • 這是由於這類.a文件是一個胖二進制,包含了多個CPU架構,須要先使用lipo文件來提取特定的CPU架構的二進制文件,使用以下:

    # 拆分出個arm64架構的二進制
    lipo xx. a -thin arm64 -output xx_arm64.a
    # 解壓.a文件
    ar -x xx_arm64.a
    複製代碼

三、nm命令

  • 被用於顯示二進制目標文件的符號表(display name list (symbol table))

  • 經常使用的方法以下:

    # 獲得Mach-O中的程序符號表
    nm path
    # 目標文件的全部符號
    nm -nm path 
    複製代碼

四、grep命令

  • 用來判斷是否包含字符串

  • 經常使用的方法以下:

    # 檢查是否包含xxx字符串:
    grep -r "xxx」 path
    複製代碼

4、otool工具使用簡介

otool(object file displaying tool),能夠對指定目標文件或者庫文件以特定的方法解析顯示,是分析Mach-O文件的利器。(通常安裝了Xcode,默認安裝了otool)

一、查看Mach-O的header

otool -h app_name.app/app_name
複製代碼
  • header信息包括:magic、cputype、cpusubtype、caps、filetype、ncmds、sizeofcmds和flags

二、查看Mach-O的load commands

otool -l app_name.app/app_name
複製代碼
  • 信息主要包括Mach-O 裏命令類型信息,名稱和二進制文件的位置。

三、查看Mach-O依賴的動態庫

otool -L app_name.app/app_name
複製代碼
  • 動態庫信息包括:動態庫名稱、當前版本號、兼容版本號

四、查看Mach-O文件的加密信息

otool -l app_name.app/app_name | grep crypt
複製代碼
  • 執行結果中cryptid有 0(未加密)和1(加密) 兩個取值

五、查看Mach-O文件中全部類和引用類(地址)

# 獲取全部類的地址
otool -v -s __DATA __objc_classlist app_name.app/app_name
# 獲取全部引用類的地址
otool -v -s __DATA __objc_classrefs app_name.app/app_name 
複製代碼
  • 能夠利用這兩個結果的差值,而後進行符號化,就能夠獲得未被引用的類信息。不過,須要注意的是:未引用的類不等於未使用的類,一些實際使用(動態調用等)也可能被誤認爲是未使用的類。

六、擴展:MachOView工具

  • 使用otool當然方便,可是也可使用MachOView工具來查看Mach-O文件,更加直觀,很方便看到 Mach-O文件header、 load commands等信息,具體使用見Mach-O文件瀏覽器---MachOView
  • MachOView的工具界面左上角有一個 RAW、RVA 的選項。
    • RAW 就是指該字節相對於文件開始部分的絕對偏移,文件頭部的地址是從0x000開始的。
    • RVA 是相對於某個基地址的偏移,也就是總體的絕對偏移值再加上某個基地址,文件頭部的地址是從某個值(基地址)開始的。

5、class-dump工具使用簡介

一、概述

  • class-dump用來dump Mach-O文件的class信息;它利用OC語言的Runtime特性,將存儲在Mach-O文件中的頭文件信息提取出來,並生成對應的.h文件。
  • 逆向中也經常使用到class-dump這個工具

二、下載和安裝

  • Class-dump地址 下載最新的dmg文件
  • 打開dmg文件,將其中的class-dump拷貝到目錄中,好比$HOME/custom-tool/bin目錄下
  • 打開~/.bash_profile文件:vi ~/.bash_profile,在文件最上方加一行:export PATH=$HOME/custom-tool/bin/:$PATH,而後保存並退出
  • 執行source ~/.bash_profile
  • 至此,class-dump工具生效。

三、使用

  • 獲取ipa文件,修改後綴名爲.zip,解壓後,獲取Payload文件中的app文件;

  • 須要注意的是,從App Store下載的app文件都是通過加密的,可執行文件被加上了一層外殼,class-dump沒法直接做用於這樣的文件。須要使用其它方式將外殼破壞才能夠。

  • 將app文件放到指定目錄下,進入該目錄,執行以下命令

    # 導出Mach-O頭文件(頭文件內容按名字排序)
    class-dump -H Mach-O文件路徑 -o 頭文件存放目錄
    複製代碼
    • -H 表示要生成頭文件
    • -o用於制定頭文件的存放目錄
  • 補充統計文件和文件夾數的命令

    # 查看某個文件下的文件個數,包括子文件裏的
    ls -lR|grep "^-"|wc -l
     # 查看某文件下的文件夾的個數,包括子文件夾裏的
    ls -lR|grep "^d"|wc -l
    複製代碼

6、Link Map File

一、概述

  • 源碼通過編譯階段,每一個類會生成對應的.o文件(目標文件);而後在連接階段,把.o文件和動態庫連接在一塊兒,最終生成可執行文件;
  • Linkmap是iOS編譯過程的中間產物,記錄了二進制文件的佈局,裏面記錄了可執行文件的路徑、CPU架構、目標文件、符號等信息。
  • 經過Link Map File能夠了解內存分段、分區、分析可執行文件中類或庫佔用空間(能夠知道App瘦身)
  • Link Map File能夠設置 工程->Build Setting->Write Link Map File爲YES,Build後生成Link Map File文件的功能;還能夠經過設置Path to Link Map File,指定Link Map File存放的路徑。

二、Link Map File的重要組成

  • Path & Arch:Path是可執行文件的路徑,Arch是架構類型。

    # Path: /Users/xxx/Library/Developer/Xcode/DerivedData/..../app_name.app/app_name
    # Arch: arm64
    複製代碼
  • Object Files:生成二進制用到的link單元(包括.o文件和dylib庫)的路徑和文件編號;經過類編號能夠對應到具體的類。在後面的Symbols部分,咱們會用到類編號。

    # Object files:
    [  0] linker synthesized
    [  1] /Users/xxxx/Library/Developer/Xcode/DerivedData/..../AppDelegate.o
    [  2] /Users/xxxx/Library/Developer/Xcode/DerivedData/..../main.o
    # ...
    複製代碼
  • Sections: 記錄Mach-O中每一個Segment/section的地址範圍。Mach-O中有三類的Segement,Segement劃分紅了不一樣的Section,不一樣的Section存儲着不一樣的信息:Segement主要有三類:__TEXT__DATA__LINKEDIT

    • __TEXT包含 Mach header,被執行的代碼和只讀常量(如C 字符串),只讀可執行
    • __DATA 包含全局變量,靜態變量等,可讀寫
    • __LINKEDIT 包含包含了加載程序的『元數據』,好比函數的名稱和地址,只讀。
    # 第一列是Section起始位置,第二列是Section佔用內存大小,第三列是Segment類型,第四列是Section類型。
    # Sections:
    # Address Size Segment Section
    0x100002780 0x0129617D  __TEXT  __text
    0x1012988FE 0x000015E4  __TEXT  __stubs
    # ...
    複製代碼
  • Symbols: 按順序記錄每一個符號的地址範圍

    # Symbols:
    // __text代碼區
    # Address Size File Name
    0x100002780 0x00000450  [  2] -[UIButton(SSEdgeInsets) setImageUpTitleDownWithSpacing:]
    0x100002BD0 0x00000070  [  2] _UIEdgeInsetsMake
    # ...
    複製代碼
    • 根據Address肯定分佈的區域,如__TEXT段的__text區(存儲着代碼),__TEXT段的__objc_methname區(存儲着方法名)、__DATA的__objc_classlist區(存儲全部的類)等;
    • 根據Address ,還能夠經過符號表找到對應出具體的方法名Name(方法名越長,最終佔用的內存也越大)
    • 根據File編號找到代碼屬於哪一個類;
    • __objc_classlist區的size值都是8,區域裏存儲的值都是一個指針,指向了類的虛擬地址。

三、功能

  • 分析二進制中類和庫大小:在Symbols部分,咱們能夠把類編號相同的size加起來,能夠計算出類的大小;將同一個庫中類大小統計在一塊兒,能夠計算庫的大小。現成分析工具LinkMap
  • 找到未引用的類:利用_objc_classname(全部類名)和__objc_classrefs(引用到的類)的差集找到未引用的類(未引用的類未必是未使用的類)
  • 找到未引用的方法:_objc_methname(全部的方法)和__objc_selrefs(引用的方法)的差異,找到未引用的方法(未引用的方法未必是未使用的方法)
  • Link Map File還有不少可挖掘的用處

歷史文章

iOS App瘦身小記 -- 基本給出了App瘦身一些建議

PNG圖片原理二三事 -- 基本介紹了PNG原理,而後就對App瘦身中圖片壓縮佛繫了

文檔參考

Apple 將 iOS AppStore 下載限制從 150M 提升至 200M

iOS逆向 class-dump

iOS代碼瘦身實踐:刪除無用的類

當咱們談論iOS瘦身的時候,咱們到底在談論些什麼

Mac查看文件內容經常使用的命令小結

分析Mach-O文件

iOS中的可執行文件

iOS 指令集架構 armv六、armv七、armv7s、arm6四、arm64e、x86_6四、i386

解讀 Mach-O 文件格式

趣探 Mach-O:文件格式分析

相關文章
相關標籤/搜索