Mach-O檔案周邊二三事

南華Coder發表於2020-03-31

一、iOS App大小限制變化

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

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

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

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

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

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

2、可執行檔案大小的限制變化

  • 根據Apple的稽核要求,上傳App Store的ipa的可執行檔案有大小限制,這裡的可執行檔案大小不是指二進位制(Mach-O)檔案大小,而是指二進位制(Mach-O)檔案中__TEXT部分的大小。

  • IOS 7版本之前, 二進位制檔案中所有__TEXT部分總和不得超過80MB;

  • iOS 7.X 至 iOS 8.X,二進位制檔案中,每個 Architecture Slice(架構片段)中的__TEXT部分不得超過60MB

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

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

3、總結

  • 隨著4G普及,5G到來,流量費用大大降低、Apple放開了對App大小方面的限制、iOS使用者升級系統意願高等因素,iOS開發者對包大小可以鬆口氣,如不必很擔心超過二進位制__TEXT部分的限制,可以優先業務迭代,有人力的情況下,再去做包瘦身;
  • 如果追求App的更高品質,在競品中拔得頭籌,還是需要在包大小方面花很大功夫;一般來說,ROI最高的是無用資源(主要是圖片)的清理,其次是二進位制檔案大小的優化;二進位制檔案大小的優化一個是靠優化編譯器選項,一個是清理無用的類、函式和程式碼塊等。
  • 網路上有很多類似的包優化的部落格可以參考,本文就不說了,本文主要介紹Mach-O檔案周邊的知識:Mach-O檔案本身、 分析工具和Link Map File等。

二、Mach-O檔案簡介

1、概述

  • 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 :可重定向檔案型別

2、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,對應的具體型別,比如arm64、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

3、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的指令集。

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

1、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
    複製程式碼

2、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
    複製程式碼

3、nm命令

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

  • 常用的方法如下:

    # 得到Mach-O中的程式符號表
    nm path
    # 目標檔案的所有符號
    nm -nm path 
    複製程式碼

4、grep命令

  • 用來判斷是否包含字串

  • 常用的方法如下:

    # 檢查是否包含xxx字串:
    grep -r "xxx” path
    複製程式碼

四、otool工具使用簡介

otool(object file displaying tool),可以對指定目標檔案或者庫檔案以特定的方法解析顯示,是分析Mach-O檔案的利器。(一般安裝了Xcode,預設安裝了otool)

1、檢視Mach-O的header

otool -h app_name.app/app_name
複製程式碼
  • header資訊包括:magic、cputype、cpusubtype、caps、filetype、ncmds、sizeofcmds和flags

2、檢視Mach-O的load commands

otool -l app_name.app/app_name
複製程式碼
  • 資訊主要包括Mach-O 裡命令型別資訊,名稱和二進位制檔案的位置。

3、檢視Mach-O依賴的動態庫

otool -L app_name.app/app_name
複製程式碼
  • 動態庫資訊包括:動態庫名稱、當前版本號、相容版本號

4、檢視Mach-O檔案的加密資訊

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

5、檢視Mach-O檔案中所有類和引用類(地址)

# 獲取所有類的地址
otool -v -s __DATA __objc_classlist app_name.app/app_name
# 獲取所有引用類的地址
otool -v -s __DATA __objc_classrefs app_name.app/app_name 
複製程式碼
  • 可以利用這兩個結果的差值,然後進行符號化,就可以得到未被引用的類資訊。不過,需要注意的是:未引用的類不等於未使用的類,一些實際使用(動態呼叫等)也可能被誤認為是未使用的類。

6、擴充套件:MachOView工具

  • 使用otool固然方便,但是也可以使用MachOView工具來檢視Mach-O檔案,更加直觀,很方便看到 Mach-O檔案header、 load commands等資訊,具體使用見Mach-O檔案瀏覽器---MachOView
  • MachOView的工具介面左上角有一個 RAW、RVA 的選項。
    • RAW 就是指該位元組相對於檔案開始部分的絕對偏移,檔案頭部的地址是從0x000開始的。
    • RVA 是相對於某個基地址的偏移,也就是整體的絕對偏移值再加上某個基地址,檔案頭部的地址是從某個值(基地址)開始的。

五、class-dump工具使用簡介

1、概述

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

2、下載和安裝

  • 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工具生效。

3、使用

  • 獲取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
    複製程式碼

六、Link Map File

1、概述

  • 原始碼經過編譯階段,每個類會生成對應的.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存放的路徑。

2、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區(儲存著方法名)、__DA他的__objc_classlist區(儲存所有的類)等;
    • 根據Address ,還可以通過符號表找到對應出具體的方法名Name(方法名越長,最終佔用的記憶體也越大)
    • 根據File編號找到程式碼屬於哪個類;
    • __objc_classlist區的size值都是8,區域裡儲存的值都是一個指標,指向了類的虛擬地址。

3、功能

  • 分析二進位制中類和庫大小:在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 指令集架構 armv6、armv7、armv7s、arm64、arm64e、x86_64、i386

解讀 Mach-O 檔案格式

趣探 Mach-O:檔案格式分析

相關文章