026*:冷熱啟動優化、二進制重排、clang插樁(Header、Load Commands 、segment)(main函數前、main函數后)重簽名 、ASLR、(PageFault 、System Trace、order文件)-fsanitize-coverage=func,trace-pc-guard Dl_info


問題

1:(虛擬內存、ASLR)(PE、ELF、Mach-O)

2:(Header、Load Commands 、segment)

3:Header(cputype、filetype)

4:Load Commands(動態鏈接器的位置、程序的入口、依賴庫的信息、代碼的位置、符號表的位置)

5:(main函數前、main函數后)重簽名 、ASLR、

6:(PageFault 、System Trace、order文件)

7:-fsanitize-coverage=func,trace-pc-guard   Dl_info 

目錄

1:基本概念

2:啟動優化

3:二進制重排

預備

 

正文

一:基本概念

1:虛擬內存 & 物理內存

早期的數據訪問是直接通過物理地址訪問的,這種方式有以下兩個問題:

  • 1、內存不夠用

  • 2、內存數據的安全問題

內存不夠用的方案:虛擬內存

針對問題1,我們在進程和物理內存之間增加一個中間層,這個中間層就是所謂的虛擬內存,主要用於解決當多個進程同時存在時,對物理內存的管理。提高了CPU的利用率,使多個進程可以同時、按需加載。所以虛擬內存其本質就是一張虛擬地址和物理地址對應關系的映射表

  • 每個進程都有一個獨立的虛擬內存,其地址都是從0開始,大小是4G固定的,每個虛擬內存又會划分為一個一個的(頁的大小在iOS中是16K,其他的是4K),每次加載都是以頁為單位加載的,進程間是無法互相訪問的,保證了進程間數據的安全性。

  • 一個進程中,只有部分功能是活躍的,所以只需要將進程中活躍的部分放入物理內存,避免物理內存的浪費

  • 當CPU需要訪問數據時,首先是訪問虛擬內存,然后通過虛擬內存去尋址,即可以理解為在表中找對應的物理地址,然后對相應的物理地址進行訪問

  • 如果在訪問時,虛擬地址的內容未加載到物理內存,會發生缺頁異常(pagefault),將當前進程阻塞掉,此時需要先將數據載入到物理內存,然后再尋址,進行讀取。這樣就避免了內存浪費

如下圖所示,虛擬內存與物理內存間的關系

 

內存數據的安全問題:ASLR技術

在上面解釋的虛擬內存中,我們提到了虛擬內存的起始地址與大小都是固定的,這意味着,當我們訪問時,其數據的地址也是固定的,這會導致我們的數據非常容易被破解,為了解決這個問題,所以蘋果為了解決這個問題,在iOS4.3開始引入了ASLR技術。

ASLR的概念:(Address Space Layout Randomization ) 地址空間配置隨機加載,是一種針對緩沖區溢出安全保護技術,通過對堆、棧、共享庫映射等線性區布局的隨機化,通過增加攻擊者預測目的地址的難度,防止攻擊者直接定位攻擊代碼位置,達到阻止溢出攻擊的目的的一種技術。

其目的的通過利用隨機方式配置數據地址空間,使某些敏感數據(例如APP登錄注冊、支付相關代碼)配置到一個惡意程序無法事先獲知的地址,令攻擊者難以進行攻擊。

由於ASLR的存在,導致可執行文件和動態鏈接庫在虛擬內存中的加載地址每次啟動都不固定,所以需要在編譯時來修復鏡像中的資源指針,來指向正確的地址。正確的內存地址 = ASLR地址 + 偏移值

2:可執行文件

不同的操作系統,其可執行文件的格式也不同。系統內核將可執行文件讀取到內存,然后根據可執行文件的頭簽名( magic魔數)判斷二進制文件的格式
 

其中 PE、ELF、Mach-O這三種可執行文件格式都是 COFF(Command file format)格式的變種,COFF的主要貢獻是目標文件里面 引入了“段”的機制,不同的目標文件可以擁有不同數量和不同類型的“段”。

2.1:通用二進制文件

因為不同CPU平台支持的指令不同,比如arm64x86,蘋果中的通用二進制格式就是將多種架構的Mach-O文件打包在一起,然后系統根據自己的CPU平台,選擇合適的Mach-O,所以通用二進制格式也被稱為胖二進制格式,如下圖所示

通用二進制格式的定義在 <mach-o/fat.h>中,可以在 下載xnu,然后根據  xnu -> EXTERNAL_HEADERS ->mach-o中找到該文件,通用二進制文件開始的 Fat Headerfat_header結構體,而Fat Archs是表示通用二進制文件中有多少個Mach-O,單個Mach-O的描述是通過 fat_arch結構體。兩個結構體的定義如下:
/*
 - magic:可以讓系統內核讀取該文件時知道是通用二進制文件
 - nfat_arch:表明下面有多個fat_arch結構體,即通用二進制文件包含多少個Mach-O
 */
struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC */
    uint32_t    nfat_arch;  /* number of structs that follow */
};

/*
 fat_arch是描述Mach-O
 - cputype 和 cpusubtype:說明Mach-O適用的平台
 - offset(偏移)、size(大小)、align(頁對齊)描述了Mach-O二進制位於通用二進制文件的位置
 */
struct fat_arch {
    cpu_type_t  cputype;    /* cpu specifier (int) */
    cpu_subtype_t   cpusubtype; /* machine specifier (int) */
    uint32_t    offset;     /* file offset to this object file */
    uint32_t    size;       /* size of this object file */
    uint32_t    align;      /* alignment as a power of 2 */
};

所以,綜上所述,

  • 通用二進制文件是蘋果公司提出的一種新的二進制文件的存儲結構,可以同時存儲多種架構的二進制指令,使CPU在讀取該二進制文件時可以自動檢測並選用合適的架構,以最理想的方式進行讀取

  • 由於通用二進制文件會同時存儲多種架構,所以比單一架構的二進制文件大很多,會占用大量的磁盤空間,但由於系統會自動選擇最合適的,不相關的架構代碼不會占用內存空間,且執行效率高

  • 還可以通過指令來進行Mach-O的合並與拆分

    • 查看當前Mach-O的架構:lipo -info MachO文件

    • 合並:lipo -create MachO1 MachO2 -output 輸出文件路徑

    • 拆分:lipo MachO文件 –thin 架構 –output 輸出文件路徑

2.2:Mach-O文件

Mach-O文件是Mach Object文件格式的縮寫,它是用於可執行文件、動態庫、目標代碼的文件格式。作為a.out格式的替代,Mach-O格式提供了更強的擴展性,以及更快的符號表信息訪問速度

熟悉Mach-O文件格式,有助於更好的理解蘋果底層的運行機制,更好的掌握dyld加載Mach-O的步驟。

查看Mach-O文件
如果想要查看具體的Mach-O文件信息,可以通過以下兩種方式,推薦使用第二種方式,更直觀

【方式一】otool終端命令:otool -l Mach-O文件名

 

【方式二】 MachOView工具(推薦):將Mach-O可執行文件拖動到MachOView工具打開

 

2.3:Mach-O文件格式

對於OS X 和iOS來說,Mach-O是其可執行文件的格式,主要包括以下幾種文件類型

  • Executable:可執行文件
  • Dylib:動態鏈接庫
  • Bundle:無法被鏈接的動態庫,只能在運行時使用dlopen加載
  • Image:指的是Executable、Dylib和Bundle的一種
  • Framework:包含Dylib、資源文件和頭文件的集合

下面圖示是Mach-O 鏡像文件格式

以上是Mach-O文件的格式,一個完成的 Mach-O文件主要分為三大部分:
  • Header Mach-O頭部:主要是Mach-O的cpu架構,文件類型以及加載命令等信息

  • Load Commands 加載命令:描述了文件中數據的具體組織結構,不同的數據類型使用不同的加載命令表示

  • Data 數據:數據中的每個段(segment)的數據都保存在這里,段的概念與ELF文件中段的概念類似。每個段都有一個或多個部分,它們放置了具體的數據與代碼,主要包含代碼,數據,例如符號表,動態符號表等等

2.3.1:Header

Mach-O的Header包含了整個Mach-O文件的關鍵信息,使得CPU能快速知道Mac-O的基本信息,其在Mach.h(路徑同前文的fat.h一致)針對32位和64位架構的cpu,分別使用了mach_headermach_header_64結構體來描述Mach-O頭部mach_header是連接器加載時最先讀取的內容,決定了一些基礎架構、系統類型、指令條數等信息,這里查看64位架構的mach_header_64結構體定義,相比於32位架構的mach_header,只是多了一個reserved保留字段,

/*
 - magic:0xfeedface(32位) 0xfeedfacf(64位),系統內核用來判斷是否是mach-o格式
 - cputype:CPU類型,比如ARM
 - cpusubtype:CPU的具體類型,例如arm64、armv7
 - filetype:由於可執行文件、目標文件、靜態庫和動態庫等都是mach-o格式,所以需要filetype來說明mach-o文件是屬於哪種文件
 - ncmds:sizeofcmds:LoadCommands加載命令的條數(加載命令緊跟header之后)
 - sizeofcmds:LoadCommands加載命令的大小
 - flags:標志位標識二進制文件支持的功能,主要是和系統加載、鏈接有關
 - reserved:保留字段
 */
struct mach_header_64 {
    uint32_t    magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t    filetype;   /* type of file */
    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 */
};

其中filetype主要記錄Mach-O的文件類型,常用的有以下幾種

#define MH_OBJECT   0x1     /* 目標文件*/
#define MH_EXECUTE  0x2     /* 可執行文件*/
#define MH_DYLIB    0x6     /* 動態庫*/
#define MH_DYLINKER 0x7     /* 動態鏈接器*/
#define MH_DSYM     0xa     /* 存儲二進制文件符號信息,用於debug分析*/

相對應的,Header在MachOView中的展示如下

 

2.3.2:Load Commands

在Mach-O文件中,Load Commands主要是用於加載指令,其大小和數目在Header中已經被提供,其在Mach.h中的定義如下

/*
 load_command用於加載指令
 - cmd 加載命令的類型
 - cmdsize 加載命令的大小
 */
struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

我們在MachOView中查看Load Commands,其中記錄了很多信息,例如動態鏈接器的位置、程序的入口、依賴庫的信息、代碼的位置、符號表的位等等,如下所示

其中LC_SEGMENT_64的類型segment_command_64定義如下

/*
 segment_command 段加載命令
 - cmd:表示加載命令類型,
 - cmdsize:表示加載命令大小(還包括了緊跟其后的nsects個section的大小)
 - segname:16個字節的段名字
 - vmaddr:段的虛擬內存起始地址
 - vmsize:段的虛擬內存大小
 - fileoff:段在文件中的偏移量
 - filesize:段在文件中的大小
 - maxprot:段頁面所需要的最高內存保護(4 = r,2 = w,1 = x)
 - initprot:段頁面初始的內存保護
 - nsects:段中section數量
 - flags:其他雜項標志位
 
 - 從fileoff(偏移)處,取filesize字節的二進制數據,放到內存的vmaddr處的vmsize字節。(fileoff處到filesize字節的二進制數據,就是“段”)
 - 每一個段的權限相同(或者說,編譯時候,編譯器把相同權限的數據放在一起,成為段),其權限根據initprot初始化。initprot指定了如何通過讀/寫/執行位初始化頁面的保護級別
 - 段的保護設置可以動態改變,但是不能超過maxprot中指定的值(在iOS中,+x和+w是互斥的)
 */
struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* includes sizeof section_64 structs */
    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 */
    uint32_t    flags;      /* flags */
};

2.3.3:Data

Load Commands后就是Data區域,這個區域存儲了具體的只讀、可讀寫代碼,例如方法、符號表、字符表、代碼數據、連接器所需的數據(重定向、符號綁定等)。主要是存儲具體的數據。其中大多數的Mach-O文件均包含以下三個段:

  • __TEXT 代碼段:只讀,包括函數,和只讀的字符串
  • __DATA 數據段:讀寫,包括可讀寫的全局變量等
  • __LINKEDIT: __LINKEDIT包含了方法和變量的元數據(位置,偏移量),以及代碼簽名等信息。

Data區中,Section占了很大的比例,SectionMach.h中是以結構體section_64(在arm64架構下)表示,其定義如下

/*
 Section節在MachO中集中體現在TEXT和DATA兩段里.
 - sectname:當前section的名稱
 - segname:section所在的segment名稱
 - addr:內存中起始位置
 - size:section大小
 - offset:section的文件偏移
 - align:字節大小對齊
 - reloff:重定位入口的文件偏移
 - nreloc:重定位入口數量
 - flags:標志,section的類型和屬性
 - reserved1:保留(用於偏移量或索引)
 - reserved2:保留(用於count或sizeof)
 - reserved3:保留
 */

struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   /* name of this section */
    char        segname[16];    /* segment this section goes in */
    uint64_t    addr;       /* memory address of this section */
    uint64_t    size;       /* size in bytes of this section */
    uint32_t    offset;     /* file offset of this 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)*/
    uint32_t    reserved1;  /* reserved (for offset or index) */
    uint32_t    reserved2;  /* reserved (for count or sizeof) */
    uint32_t    reserved3;  /* reserved */
};

SectionMachOView中可以看出,主要集中體現在TEXTDATA兩段里,如下所示

其中常見的section,主要有以下一些
section - __TEXT 說明
__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 類名稱

 

 

 

 

 

 

 

 

 

 

 

section - __DATA 說明
__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 Objective-C 類列表
__DATA.__objc_protolist Objective-C 原型
__DATA.__objc_imginfo Objective-C 鏡像信息
__DATA.__objc_selfrefs Objective-C self 引用
__DATA.__objc_protorefs Objective-C 原型引用
__DATA.__objc_superrefs Objective-C 超類引用

 

 

 

 

 

 

 

 

 

 

 

 

 

 
 
 

 

二:啟動優化

1. 冷啟動和熱啟動

首次啟動應用、kill應用后重新打開應用、應用置於后台隔一段時間再返回前台等情況,都是應用啟動

有時啟動,有時啟動。這是冷啟動熱啟動的原因:

冷啟動是指內存中不包含該應用程序相關的數據,必須要從磁盤載入到內存中的啟動過程。

注意:重新打開 APP, 不一定就是冷啟動。

  1. 當內存不足,APP被系統自動殺死后,再啟動就是冷啟動。
  2. 如果在重新打開 APP 之前,APP 的相關數據還存儲在內存中,這時再打開 APP,就是熱啟動
  3. 冷啟動與熱啟動是由系統決定的,我們無法決定。
  4. 當然設備重啟以后,第一次打開 APP 的過程,一定是冷啟動

2. 啟動性能檢測和分析

測試APP啟動,分為兩個階段:

  • main函數前: dyld負責的啟動流程

系統處理,我們從dyld應用加載的流程來優化。(借助系統工具分析耗時)

  • main函數后開發者自己的業務代碼

通過檢測業務流程優化main函數打個時間點第一個頁面渲染完成打個時間點。測算耗時)

2.1 main函數前

Edit Scheme -> Run -> Arguments ->Environment Variables點擊+添加環境變量  DYLD_PRINT_STATISTICS設為  1),然后運行,以下是iPhone7p正常啟動的pre-main時間(以WeChat為例)
擴展:重簽名

1:新建APP文件夾,放入砸殼后的包、

2:加入appSign.sh重簽名腳本:

# ${SRCROOT} 它是工程文件所在的目錄
TEMP_PATH="${SRCROOT}/Temp"
#資源文件夾,我們提前在工程目錄下新建一個APP文件夾,里面放ipa包
ASSETS_PATH="${SRCROOT}/APP"
#目標ipa包路徑
TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"
#清空Temp文件夾
rm -rf "${SRCROOT}/Temp"
mkdir -p "${SRCROOT}/Temp"

#----------------------------------------
# 1. 解壓IPA到Temp下
unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
# 拿到解壓的臨時的APP的路徑
TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")
# echo "路徑是:$TEMP_APP_PATH"

#----------------------------------------
# 2. 將解壓出來的.app拷貝進入工程下
# BUILT_PRODUCTS_DIR 工程生成的APP包的路徑
# TARGET_NAME target名稱
TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
echo "app路徑:$TARGET_APP_PATH"

rm -rf "$TARGET_APP_PATH"
mkdir -p "$TARGET_APP_PATH"
cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"

#----------------------------------------
# 3. 刪除extension和WatchAPP.個人證書沒法簽名Extention
rm -rf "$TARGET_APP_PATH/PlugIns"
rm -rf "$TARGET_APP_PATH/Watch"

#----------------------------------------
# 4. 更新info.plist文件 CFBundleIdentifier
#  設置:"Set : KEY Value" "目標文件路徑"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier >$PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"

#----------------------------------------
# 5. 給MachO文件上執行權限
# 拿到MachO文件的路徑
APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
#上可執行權限
chmod +x "$TARGET_APP_PATH/$APP_BINARY"

#----------------------------------------
# 6. 重簽名第三方 FrameWorks
TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
then
for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
do

#簽名
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
done
fi

#注入
#yololib "$TARGET_APP_PATH/$APP_BINARY" >"Frameworks/HankHook.framework/HankHook"

3:Demo工程添加腳本指令./appSign.sh

真機運行后,可看到:

Total pre-main time: 1.2 seconds (100.0%)
         dylib loading time: 326.38 milliseconds (25.4%)
        rebase/binding time: 146.54 milliseconds (11.4%)
            ObjC setup time:  40.49 milliseconds (3.1%)
           initializer time: 767.04 milliseconds (59.9%)
           slowest intializers :
             libSystem.B.dylib :   6.86 milliseconds (0.5%)
    libMainThreadChecker.dylib :  38.26 milliseconds (2.9%)
          libglInterpose.dylib : 447.73 milliseconds (34.9%)
             marsbridgenetwork :  48.86 milliseconds (3.8%)
                          mars :  30.85 milliseconds (2.4%)
                       砸殼應用 : 212.00 milliseconds (16.5%)

2.2 分析DYLD耗時元素:

Total pre-main time: main函數前總耗時

dylib loading timedylib庫加載耗時(官方建議,動態庫不超過6個

此應用的Frameworks:

 
rebase/binding time:  重定向綁定操作的 耗時
[rebase重定向]:從磁盤的 MachOimage鏡像內存中)
[binding綁定]: MachO中每個文件 使用其他庫符號時,綁定 庫名地址
出於 安全考慮, 編譯時運行時地址不一樣。使用了 ASLR(Address space layout randomization) 地址空間配置隨機加載,每次 載入內存后,需要將 原地址加上 ASLR隨機偏移值來進行 內存讀取。 具體原因, 下面分析 虛擬內存物理內存時,就 清楚

ObjC setup time: OC類注冊耗時 (OC類越多,越耗時)

swift沒有OC類,所以在這一步有優越性

initializer time:初始化耗時(load非懶加載類和c++構造函數的耗時)

slowest intializers: 最慢啟動對象

  • libSystem.B.dylib: 系統庫
  • libMainThreadChecker.dylib: 系統庫
  • libglInterpose.dylib: 系統庫(調試使用的,不影響)
  • 砸殼應用:自己的APP耗時

2.2 main函數后

main函數階段的優化建議主要有以下幾點:

在main函數之后的didFinishLaunching方法中,主要是執行了各種業務,有很多並不是必須在這里立即執行的,這種業務我們可以采取延遲加載,防止影響啟動時間。

1:業務層面:

  • 減少啟動初始化的流程,能懶加載的懶加載,能延遲的延遲,能放后台初始化的放后台,盡量不要占用主線程的啟動時間

  • 優化代碼邏輯,去除非必須的代碼邏輯,減少每個流程的消耗時間

  • 啟動階段能使用多線程來初始化的,就使用多線程

  • 盡量使用純代碼來進行UI框架的搭建,尤其是主UI框架,例如UITabBarController。盡量避免使用Xib或者SB,相比純代碼而言,這種更耗時

  • 刪除廢棄類、方法

2:技術層面

  • 1.二進制重排
    (重排的是編譯階段文件順序減少啟動時刻,硬盤內存作次數

三:二進制重排

從上面的Page Fault的次數以及加載順序,可以發現其實 導致Page Fault次數過多的根本原因是啟動時刻需要調用的方法,處於不同的Page導致的。因此,我們的優化思路就是: 將所有啟動時刻需要調用的方法,排列在一起,即放在一個頁中,這樣就從多個Page Fault變成了一個Page Fault。這就是二進制重排的 核心原理 

1:二進制重排原理

  • 應用啟動前頁表的,每一頁都是PageFault(頁缺省),啟動時用到的每一頁都需要cpu從硬盤讀取物理內存中,雖然加載一頁的耗時沒什么感覺。但如果同時加載幾百頁,這個耗時就得考慮了。

本節我們研究的就是APP啟動優化,所以這里也是一個優化點

  • 優化核心: 減少啟動時需要加載頁數
  • iOS每一頁16K大小,但是16K中,可能真正在啟動時刻需要用到的,可能不到1K。 啟動需要訪問到這1K數據,不得不整頁加載
  • 我們的二進制重排,就是為了啟動用到的這些數據整合到一起,然后進行內存分頁。這樣啟動用到的數據都在前幾頁中了。啟動時只需加載幾頁數據就可以了。

1.1 二進制重排中的二進制

二進制: 只有01的兩個數的數制。是機器識別進制

  • 此處二進制,主要是我們代碼文件中的函數編譯后變成的機器識別符號,再轉換二進制文件。

  • 所以二進制重排,重排的是代碼文件函數順序。只加載用到的數據。用到的頁

1.2 二進制數據順序

創建個Demo項目,加入測試代碼:

#import "ViewController.h"

@interface ViewController ()
@end

@implementation ViewController

void test1() {
    printf("1");
}

void test2() {
    printf("2");
}

- (void)viewDidLoad {
    [super viewDidLoad];

    printf("viewDidLoad");
    test1();
}

+(void)load {
    printf("load");
    test2();
}

@end

Build Settings中搜索link Map,設置Write Link Map FileYES:



Command + B編譯后, 右鍵 Show In Finder打開 包文件夾
 

沿路徑找到並打開Demo-LinkMap-normal-x86_64.txt文件:

 

函數順序:(書寫順序)

文件順序:(加入順序)

Build Setting -> Write Link Map File設置為YES

CMD+B編譯demo,然后在對應的路徑下查找  link map文件,如下所示,可以發現 類中 函數的加載順序是從上到下的,而 文件的順序是根據 Build Phases -> Compile Sources中的順序加載的

總結

  • 二進制的排列順序:先文件按照加載順序排列,文件內部按照函數書寫順序從上到下排列

我們要做的,就是把啟動用到函數排列在一起

2.PageFault檢測

1:連接真機運行自己項目,打開Instruments檢測工具:

 

 
2:選擇 System Trace:
 

3:選擇真機,選擇自己的項目點擊第一個按鈕運行,等APP啟動后點擊第一個按鈕停止

4:選擇自己項目,選中主線程,選擇虛擬內存,查看File Backed Page In(就是PageFault缺省頁):

可以看到這里啟動加載了1783頁,總耗時278毫秒,平均耗時156微秒
多試幾次可能物理內存中存在已有數據,加載頁數少一些。完全冷啟動的話,加載頁數應該會更多,耗時更明顯)

3.體驗二進制重排

二進制重排,關鍵是order文件

前面講objc源碼時,會在工程中看到order文件:

 

打開.order文件,可以看到內部都是排序好函數符號

這是因為蘋果自己的都進行了二進制重排

  • 我們打開創建的Demo項目,我想把排序改成load->test1->ViewDidAppear->main

1:在Demo項目根目錄創建一個.order文件

 

2:在ht.order文件中手動順序寫入函數(還寫了個不存在的hello函數)

3:在Build Settings中搜索order file,加入./ht.order

4:Command + B編譯后,再次去查看link map文件

  • 發現order文件不存在的函數(hello),編譯器直接跳過
  • 其他函數符號,完全按照我們order順序排列。
  • order沒有的函數,按照默認順序接在order函數后面

4:二進制重排實踐

下面,我們來進行具體的實踐,首先理解幾個名詞

Link Map
Linkmap是iOS編譯過程的中間產物,記錄了二進制文件的布局,需要在Xcode的Build Settings里開啟Write Link Map File,Link Map主要包含三部分:

  • Object Files生成二進制用到的link單元的路徑和文件編號

  • Sections記錄Mach-O每個Segment/section的地址范圍

  • Symbols按順序記錄每個符號的地址范圍

ld

ld是Xcode使用的鏈接器,有一個參數order_file,我們可以通過在Build Settings -> Order File配置一個后綴為order的文件路徑。在這個order文件中,將所需要的符號按照順序寫在里面,在項目編譯時,會按照這個文件的順序進行加載,以此來達到我們的優化

所以二進制重排的本質就是對啟動加載的符號進行重新排列

到目前為止,原理我們基本弄清楚了,如果項目比較小,完全可以自定義一個order文件,將方法的順序手動添加,但是如果項目較大,涉及的方法特別多,此時我們如何獲取啟動運行的函數呢?有以下幾種思路

  • 1、hook objc_msgSend:我們知道,函數的本質是發送消息,在底層都會來到objc_msgSend,但是由於objc_msgSend的參數是可變的,需要通過匯編獲取,對開發人員要求較高。而且也只能拿到OC和 swift中@objc后的方法

  • 2、靜態掃描:掃描 Mach-O特定段和節里面所存儲的符號以及函數數據

  • 3、Clang插樁:即批量hook,可以實現100%符號覆蓋,即完全獲取swift、OC、C、block函數

四:clang插樁

官方介紹: https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs

  • 官方提供了LLVM代碼覆蓋監測工具。其中包含了Tracing PCs(追蹤PC)。

  • 我們創建TranceDemo項目,按照官方給的示例,來嘗試開發

1 添加trace

  • 按照官方描述,可以加入跟蹤代碼,並給出了回調函數

打開TranceDemo , Build Settings中搜索Other C,加入-fsanitize-coverage=trace-pc-guard,需要實現兩個方法

下面實現這兩個函數

// trace-pc-guard-cb.cc
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

// This callback is inserted by the compiler as a module constructor
// into every DSO. 'start' and 'stop' correspond to the
// beginning and end of the section with the guards for the entire
// binary (executable or DSO). The callback will be called at least
// once per DSO and may be called multiple times with the same parameters.
extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  // If you set *guard to 0 this code will not be called again for this edge.
  // Now you can get the PC and do whatever you want:
  //   store it somewhere or symbolize it and print right away.
  // The values of `*guard` are as you set them in
  // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
  // and use them to dereference an array or a bit vector.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  // This function is a part of the sanitizer run-time.
  // To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

2:復制項目案例粘貼到項目的ViewController中,去除注釋extern 聲明,加入幾個測試函數:

#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

@interface ViewController ()

@end

@implementation ViewController

+(void)load {}

void (^block)(void) = ^{ printf("123"); };

void test() { block(); }

- (void)viewDidLoad {
    [super viewDidLoad];
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
//  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    test();
}

@end

Command+B編譯,發現找不到符號__sanitizer_symbolize_pc(需要導入庫),我們暫時把這一行注釋掉

3:運行程序

 

執行順序是:
touchBegin->  __sanitizer_cov_trace_pc_guard->
test->  __sanitizer_cov_trace_pc_guard->
block->  __sanitizer_cov_trace_pc_guard

確實每個函數觸發時,都調用了__sanitizer_cov_trace_pc_guard函數。

原因:

  • 只要在Other C Flags標記,開啟了trace功能。LLVM會在每個函數邊緣(開始位置),插入一行調用__sanitizer_cov_trace_pc_guard的代碼。編譯期插入了。所以可以100%覆蓋。
  • 以上,就是Clang插樁插樁操作完成后,我們需要獲取所有函數符號存儲導出order文件

3. 獲取函數符號

  • __builtin_return_address: return的地址。

函數return,是返回到上一層函數

  • 通過return的地址,拿到的是上一層級函數信息
  • 參數: 0: 表示當前函數的上一層。 1:是上一層上一層地址。
  • 導入#import <dlfcn.h>,通過Dl_info拿到函數信息:
typedef struct dl_info {
        const char      *dli_fname;     /* 文件地址*/
        void            *dli_fbase;     /* 起始地址(machO模塊的虛擬地址)*/
        const char      *dli_sname;     /* 符號名稱 */
        void            *dli_saddr;     /* 內存真實地址(偏移后的真實物理地址) */
} Dl_info;
  • __sanitizer_cov_trace_pc_guard函數加入代碼:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    
    if(!*guard) return;
    void *PC = __builtin_return_address(0); //0 當前函數地址, 1 上一層級函數地址
    Dl_info info; // 聲明對象
    dladdr(PC, &info); // 讀取PC地址,賦值給info
    printf("dli_fname:%s \n dli_fbase:%p \n dli_sname:%s \n dli_saddr:%p \n ", info.dli_fname, info.dli_fbase, info.dli_sname, info.dli_saddr);
    
}
  • 運行程序,可以看到:

  • dli_fname: 文件地址
  • dli_fbase: 起始地址(machO模塊的虛擬地址)
  • dli_sname: 符號名稱
  • dli_saddr: 內存真實地址(偏移后的真實物理內存地址)
  • 此時,我們成功拿到函數符號

4.存儲符號

注意:__sanitizer_cov_trace_pc_guard函數是在多線程環境下,所以需要注意寫入安全

  • 寫入安全,就是上鎖。此處我使用OSAtomic原子鎖
  • 存儲方式,也有很多種, 此處我使用隊列進行存儲
  • 導入#include <libkern/OSAtomic.h>原子頭文件,創建原子隊列,定義節點結構體
#import "ViewController.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h> // 原子操作

@interface ViewController ()

@end

@implementation ViewController

+(void)load {}

void (^block)(void) = ^{ printf("123"); };

void test666() { block(); }

- (void)viewDidLoad {
    [super viewDidLoad];
}

// 定義原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; // 原子隊列初始化

// 定義符號結構體
typedef struct {
    void * pc;
    void * next;
    
}SYNode;

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
  static uint64_t N;
  if (start == stop || *start) return;
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    
    // 這里是多線程,會有資源搶奪。
    // 這個會影響load函數,所以需要移除哨兵
//    if(!*guard) return;
    
    void *PC = __builtin_return_address(0); //0 當前函數地址, 1 上一層級函數地址
    Dl_info info; // 聲明對象
    dladdr(PC, &info); // 讀取PC地址,賦值給info
    
    // 創建結構體
    SYNode * node = malloc(sizeof(SYNode)); // 創建結構體空間
    *node = (SYNode){PC, NULL}; // node節點的初始化賦值(pc為當前PC值,NULL為next值)
    
    // 加入結構 (offsetof: 按照參數1大小作為偏移值,給到next)
    // 拿到並賦值
    // 拿到symbolList地址,偏移SYNode字節,將node賦值給symbolList最后節點的next指針。
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    // 創建可變數組
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];

    // 每次while循環,都會加入一次hook (__sanitizer_cov_trace_pc_guard)   只要是跳轉,就會被block
    // 直接修改[other c clang]: -fsanitize-coverage=func,trace-pc-guard 指定只有func才加Hook
    while (1) {

        // 去除鏈表
        SYNode * node =  OSAtomicDequeue(&symbolList, offsetof(SYNode, next));

        if(node ==NULL) break;
        
        Dl_info info = {0};
        // 取出節點的pc,賦值給info
        dladdr(node->pc, &info);

        // 釋放節點
        free(node);

        // 存名字
        NSString *name = @(info.dli_sname);
        // 三目運算符 寫法
        BOOL isObjc = [name hasPrefix: @"+["] || [name hasPrefix: @"-["];
        NSString * symbolName = isObjc ? name : [NSString stringWithFormat:@"_%@",name];
        [symbolNames addObject:symbolName];

    }
    
    // 反向集合
    NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
    
    // 創建數組
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];

    // 臨時變量
    NSString * name;
    
    // 遍歷集合,去重,添加到funcs中
    while (name = [enumerator nextObject]) {
        // 數組中去重添加
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }

    // 移除當前touchesBegan函數 (跟啟動無關)
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];

    // 數組轉字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];

    // 文件路徑
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ht.order"];

    // 文件內容
    NSData * fielContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    
    // 創建文件
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fielContents attributes:nil];

    NSLog(@"%@",funcs);
    NSLog(@"%@",filePath);
    NSLog(@"%@",fielContents);
}
@end

坑點:

  1. if(!*guard) return;需要去掉,會影響+load寫入

  2. while循環,也會觸發__sanitizer_cov_trace_pc_guard

 

【原因】:

  • 通過看匯編,可以看到while也觸發了__sanitizer_cov_trace_pc_guard的跳轉。原因是,trace觸發,並不是根據函數來進行hook的,而是hook每一個跳轉(bl)
  • while也有跳轉,所以進入了死循環

【方案】:

  • Build Settings 的Other C Flags 配置,添加一個func指定條件: -fsanitize-coverage=func,trace-pc-guard 

根據打印路徑,查看ht.order文件,完美!

從真機沙盒中拿到ht.order文件。

復制ht.order文件,放到根目錄,就完成了。

 

可以根據上一節的內容,打開link Map查看最終符號排序,使用Instruments檢查自己應用的PageFault數量耗時

注意

  1. 【二進制重排order文件】需要代碼封版后再生成。 (代碼還在變動,生成就沒意義了)
  2. 【二進制重排相關代碼不要寫到自己項目中去。寫個小工具跑一下,拿到order文件即可。

注意

 

引用

1:iOS-底層原理 32:啟動優化(一)基本概念

2:iOS-底層原理 32:啟動優化(二)優化建議

3:iOS-底層原理 32:啟動優化(三)二進制重排

4:OC底層原理三十三:啟動優化(二進制重排)

5:OC底層原理三十四:啟動優化(Clang插樁)

6:二十七、iOS冷啟動優化 - 二進制重排 & Clang插樁

7:二十六、 啟動優化,二進制重排

8:iOS-OC啟動優化:clang插樁實現二進制重排

9:iOS 啟動優化(上)

10:IOS-啟動優化(上)

11:IOS-啟動優化(下)

12:二進制重排&優化啟動


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM