二進制文件重排的解決方案 APP啟動速度提升

啟動是App給用戶的第一印象,對用戶體驗至關重要。修改代碼在二進制文件的布局可以提高啟動性能,

原理

虛擬內存

在早期的計算機中 , 并沒有虛擬內存的概念 , 任何應用被從磁盤中加載到運行內存中時 , 都是完整加載和按序排列的 .
那么因此 , 就會出現兩個問題 :

  • 安全問題 : 由于在內存條中使用的都是真實物理地址 , 而且內存條中各個應用進程都是按順序依次排列的 . 那么在 進程1 中通過地址偏移就可以訪問到 其他進程 的內存 .
  • 效率問題 : 隨著軟件的發展 , 一個軟件運行時需要占用的內存越來越多 , 但往往用戶并不會用到這個應用的所有功能 , 造成很大的內存浪費 , 而后面打開的進程往往需要排隊等待 .
虛擬內存工作原理

引用了虛擬內存后 , 在我們進程中認為自己有一大片連續的內存空間實際上是虛擬的 , 也就是說從 0x000000 ~ 0xffffff 我們是都可以訪問的 . 但是實際上這個內存地址只是一個虛擬地址 , 而這個虛擬地址通過一張映射表映射后才可以獲取到真實的物理地址 .

可以理解為 , 系統對真實物理內存訪問做了一層限制 , 只有被寫到映射表中的地址才是被認可可以訪問的 .
例如 , 虛擬地址 0x000000 ~ 0xffffff 這個范圍內的任意地址我們都可以訪問 , 但是這個虛擬地址對應的實際物理地址是計算機來隨機分配到內存頁上的 .

虛擬內存解決進程間安全問題原理:

引用虛擬內存后就不存在通過偏移可以訪問到其他進程的地址空間的問題了 .

因為每個進程的映射表是單獨的 , 在你的進程中隨便你怎么訪問 , 這些地址都是受映射表限制的 , 其真實物理地址永遠在規定范圍內 , 也就不存在通過偏移獲取到其他進程的內存空間的問題了 .
而且實際上 , 每次應用被加載到內存中 , 實際分配的物理內存并不一定是固定或者連續的 , 這是因為內存分頁以及懶加載以及 ASLR 所解決的安全問題 .

cpu 尋址過程

引入虛擬內存后 , cpu 在通過虛擬內存地址訪問數據的過程如下 :

  • 通過虛擬內存地址 , 找到對應進程的映射表 .
  • 通過映射表找到其對應的真實物理地址 , 進而找到數據 .

這個過程被稱為 地址翻譯 , 這個過程是由操作系統以及 cpu 上集成的一個 硬件單元 MMU 協同來完成的 .

虛擬內存解決效率問題

剛剛提到虛擬內存和物理內存通過映射表進行映射 , 但是這個映射并不可能是一一對應的 , 那樣就太過浪費內存了 . 為了解決效率問題 , 實際上真實物理內存是分頁的 . 而映射表同樣是以頁為單位的 .

換句話說 , 映射表只會映射到一頁 , 并不會映射到具體每一個地址 .

linux 系統中 , 一頁內存大小為 4KB , 在不同平臺可能各有不同 .

  • Mac OS 系統中 , 一頁為 4KB ,
  • iOS 系統中 , 一頁為 16KB .

我們可以使用 pagesize 命令直接查看 .

Page Fault

  • 當應用被加載到內存中時 , 并不會將整個應用加載到內存中 . 只會放用到的那一部分 . 也就是懶加載的概念 , 換句話說就是應用使用多少 , 實際物理內存就實際存儲多少 .

  • 當應用訪問到某個地址 , 映射表中為 0 , 也就是說并沒有被加載到物理內存中時 , 系統就會立刻阻塞整個進程 , 觸發一個我們所熟知的 缺頁中斷 - Page Fault

當一個缺頁中斷被觸發 , 操作系統會從磁盤中重新讀取這頁數據到物理內存上 , 然后將映射表中虛擬內存指向對應 ( 如果當前內存已滿 , 操作系統會通過置換頁算法 找一頁數據進行覆蓋 , 這也是為什么開再多的應用也不會崩掉 , 但是之前開的應用再打開時 , 就重新啟動了的根本原因 ).

通過App Store渠道分發的App,Page Fault還會進行簽名驗證,所以一次Page Fault的耗時比想象的要多:


image.png

重排

在了解了內存分頁會觸發中斷異常 Page Fault 會阻塞進程后 , 我們就知道了這個問題是會對性能產生影響的 .

編譯器在生成二進制代碼的時候,默認按照鏈接的Object File(.o)順序寫文件,按照Object File內部的函數順序寫函數。

靜態庫文件.a就是一組.o文件的ar包,可以用ar -t查看.a包含的所有.o

image.png

簡化問題:假設我們只有兩個page:page1/page2,其中綠色的method1和method3啟動時候需要調用,為了執行對應的代碼,系統必須進行兩個Page Fault。

但如果我們把method1和method3排布到一起,那么只需要一個Page Fault即可,這就是二進制文件重排的核心原理。

實際項目中的做法是將啟動時需要調用的函數放到一起 ( 比如 前10頁中 ) 以盡可能減少 page fault , 達到優化目的 . 而這個做法就叫做 : 二進制重排 .

存在的問題

為了完成重排,有以下幾個問題要解決:

  • 重排效果怎么樣 - 要想看到優化效果 , 就應該知道如何獲取啟動階段的page fault次數 , 以此來幫助我們查看優化前以及優化后的效果 .
  • 如何重排 - 讓鏈接器按照指定順序生成Mach-O
  • 重排成功了沒 - 拿到當前二進制的函數布局
  • 重排的內容 - 獲取啟動時候用到的函數
    • hook objc_MsgSend ( 只能拿到 oc 以及 swift 加上 @objc dynamic 修飾后的方法 ) .
    • 靜態掃描 mach-o 特定段和節里面所存儲的符號以及函數數據 . (靜態掃描 , 主要用來獲取 load 方法 , c++ 構造(有關 c++ 構造 , 參考 從頭梳理 dyld 加載流程 這篇文章有詳細講述和演示 ) .
    • clang 插樁 ( 完美版本 , 完全拿到 swift , oc , c , block 全部函數 )

獲取啟動階段的page fault次數

如果想查看真實 page fault 次數 , 應該將應用卸載 , 查看第一次應用安裝后的效果 , 或者先打開很多個其他應用 .
因為之前運行過 app , 應用其中一部分已經被加載到物理內存并做好映射表映射 , 這時再啟動就會少觸發一部分缺頁中斷 , 并且殺掉應用再打開也是如此 .
其實就是希望將物理內存中之前加載的覆蓋/清理掉 , 減少誤差 .

System Trace

日常開發中性能分析是用最多的工具無疑是Time Profiler,但Time Profiler是基于采樣的,并且只能統計線程實際在運行的時間,而發生Page Fault的時候線程是被blocked,所以我們需要用一個不常用但功能卻很強大的工具:System Trace。

選中主線程,在VM Activity中的File Backed Page In次數就是Page Fault次數,并且雙擊還能按時序看到引起Page Fault的堆棧:


image.png

當然 , 你可以通過添加 DYLD_PRINT_STATISTICS 來查看 pre-main 階段總耗時來做一個側面輔證 .

signpost

在Instrument中已經能拿到某個時間段的Page In次數,可以通過os_signpost和啟動映射起來呢?

os_signpost是iOS 12開始引入的一組API,可以在Instruments繪制一個時間段,代碼也很簡單

os_log_t logger = os_log_create("com.bytedance.tiktok", "performance");
os_signpost_id_t signPostId = os_signpost_id_make_with_pointer(logger,sign);
//標記時間段開始
os_signpost_interval_begin(logger, signPostId, "Launch","%{public}s", "");
//標記結束
os_signpost_interval_end(logger, signPostId, "Launch");

有多少個Mach-O,就會有多少個Load和C++靜態初始化階段,用signpost相關API對對應階段打點,方便跟蹤每個階段的優化效果。

二進制重排具體如何操作

ld

Xcode使用的鏈接器件是ld,ld有一個不常用的參數-order_file,通過man ld可以看到詳細文檔:

Alters the order in which functions and data are laid out. For each section in the output file, any symbol in that section that are specified in the order file file is moved to the start of its section and laid out in the same order as in the order file file.

改變函數和數據的排列順序。對于輸出文件中的每個section,在order file中指定的該section中的任何符號都將移動到其section的開頭,并按照與order file中相同的順序排列。

可以看到,order_file中的符號會按照順序排列在對應section的開始,完美的滿足了我們的需求。

我們可以通過這個參數配置一個 order 文件的路徑 .Xcode的GUI也提供了order_file選項:Build Settings -> Linking -> Order File
在這個 order 文件中 , 將你需要的符號按順序寫在里面 .
當工程 build 的時候 , Xcode 會讀取這個文件 , 打的二進制包就會按照這個文件中的符號順序進行生成對應的 mach-O .

如果order_file中的符號實際不存在 , ld會忽略這些符號,如果提供了link選項 -order_file_statistics,會以warning的形式把這些沒找到的符號打印在日志里。

這種方式不會影響上架 ? 首先 , objc 源碼自己也在用這種方式 .二進制重排只是重新排列了所生成的 macho 中函數表與符號表的順序 .

通過 Linkmap 查看自己工程的符號順序

Linkmap是iOS編譯過程的中間產物,記錄了二進制文件的布局,通過Xcode的Build Settings里開啟Write Link Map File 設置輸出與否 , 默認是 no .

修改完畢后 clean 一下 , 運行工程 , Products - show in finder, 找到 macho 的上上層目錄.

linkmap主要包括三大部分:

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

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

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

# Symbols:
# Address   Size        File  Name
0x100005460 0x000000B0  [  2] -[MQTTCFSocketDecoder init]
0x100005510 0x000000C0  [  2] -[MQTTCFSocketDecoder open]
0x1000055D0 0x00000050  [  2] -[MQTTCFSocketDecoder dealloc]
0x100005620 0x000000A0  [  2] -[MQTTCFSocketDecoder close]
0x1000056C0 0x00000740  [  2] -[MQTTCFSocketDecoder stream:handleEvent:]
0x100005E00 0x00000020  [  2] -[MQTTCFSocketDecoder state]
0x100005E20 0x00000020  [  2] -[MQTTCFSocketDecoder setState:]
0x100005E40 0x00000020  [  2] -[MQTTCFSocketDecoder error]
0x100005E60 0x00000040  [  2] -[MQTTCFSocketDecoder setError:]
0x100005EA0 0x00000020  [  2] -[MQTTCFSocketDecoder stream]
0x100005EC0 0x00000040  [  2] -[MQTTCFSocketDecoder setStream:]
0x100005F00 0x00000030  [  2] -[MQTTCFSocketDecoder delegate]
0x100005F30 0x00000030  [  2] -[MQTTCFSocketDecoder setDelegate:]
0x100005F60 0x0000005C  [  2] -[MQTTCFSocketDecoder .cxx_destruct]
0x100005FC0 0x00000120  [  3] -[YXPhotographyMainNavView initWithFrame:]
0x1000060E0 0x000001E0  [  3] -[YXPhotographyMainNavView isRepeatClickDetail:]
0x1000062C0 0x000001E0  [  3] -[YXPhotographyMainNavView resetDetailItemStatus]
0x1000064A0 0x000002C0  [  3] -[YXPhotographyMainNavView setDetailIndex:]
0x100006760 0x00000270  [  3] -[YXPhotographyMainNavView setMainType:]
0x1000069D0 0x00000F00  [  3] -[YXPhotographyMainNavView setUpView]

符號順序明顯是按照 Compile Sources 的文件順序來排列的 .

最左側地址就是 實際代碼地址而并非符號地址 , 二進制重排并非只是修改符號地址 , 而是利用符號順序 , 重新排列整個代碼在文件的偏移地址 , 將啟動需要加載的方法地址放到前面內存頁中 , 以此達到減少 page fault 的次數從而實現時間上的優化 ,
可以利用 MachOView 查看排列前后在 _text 段 ( 代碼段 ) 中的源碼順序來幫助理解 .

簡單操作

來到工程根目錄 , 新建一個文件 touch lb.order . 隨便挑選幾個啟動時就需要加載的方法 ,

-[YXCenterTitleNavView .cxx_destruct]
+[YXRemoteVideoFile cacheContentType:]
+[YXRemoteVideoFile tmpContentType:]
+[YXRemoteVideoFile cacheFileExit:]
+[YXRemoteVideoFile cacheFilePath:]
+[YXRemoteVideoFile cacheFileSize:]
+[YXRemoteVideoFile tempFileExit:]
+[YXRemoteVideoFile tempFilePath:]
+[YXRemoteVideoFile tempFileSize:]
+[YXRemoteVideoFile clearTempFile:]

寫到該文件中 , 保存 , 配置 order_file 文件路徑 .
重新運行 , 查看 .Linkmap

# Symbols:
# Address   Size        File  Name
0x100005460 0x00000085  [ 23] -[YXCenterTitleNavView .cxx_destruct]
0x1000054F0 0x00000110  [ 24] +[YXRemoteVideoFile cacheContentType:]
0x100005600 0x00000110  [ 24] +[YXRemoteVideoFile tmpContentType:]
0x100005710 0x000000D0  [ 24] +[YXRemoteVideoFile cacheFileExit:]
0x1000057E0 0x00000110  [ 24] +[YXRemoteVideoFile cacheFilePath:]
0x1000058F0 0x00000170  [ 24] +[YXRemoteVideoFile cacheFileSize:]
0x100005A60 0x000000D0  [ 24] +[YXRemoteVideoFile tempFileExit:]
0x100005B30 0x000000D0  [ 24] +[YXRemoteVideoFile tempFilePath:]
0x100005C00 0x00000170  [ 24] +[YXRemoteVideoFile tempFileSize:]
0x100005D70 0x000000C0  [ 24] +[YXRemoteVideoFile clearTempFile:]
0x100005E30 0x000000B0  [  2] -[MQTTCFSocketDecoder init]
0x100005EE0 0x000000C0  [  2] -[MQTTCFSocketDecoder open]

我們所寫的方法已經被放到最前面了 , 至此 , 生成的 macho 中距離首地址偏移量最小的代碼就是我們所寫的這三個方法

獲取啟動時用的函數符號。

首先排除了解析Instruments(Time Profiler/System Trace) trace文件方案,因為他們都是基于特定場景采樣的,大多數符號獲取不到。最后選擇了靜態掃描+運行時Trace結合的解決方案。

Load

Objective C的符號名是+-[Class_name(category_name) method:name:],其中+表示類方法,-表示實例方法。

linkmap里記錄了所有的符號名,所以只要掃一遍linkmap的__TEXT,__text,正則匹配("^\+\[.*\ load\]$")可以拿到所有的load方法符號。

C++靜態初始化

C++并不像Objective C方法那樣,大部分方法調用編譯后都是objc_msgSend,也就沒有一個入口函數去運行時hook。

可以用-finstrument-functions在編譯期插樁“hook”,這套方案需要修改依賴三方庫的構建過程。二進制文件重排在沒有業界經驗可供參考,不確定收益的情況下,選擇了并不完美但成本最低的靜態掃描方案。

  1. 掃描linkmap的__DATA,__mod_init_func,這個section存儲了包含C++靜態初始化方法的文件,獲得文件號[ 5]。
//__mod_init_func
0x100008060    0x00000008  [  5] ltmp7
//[  5]對應的文件
[  5] .../Build/Products/Debug-iphoneos/libStaticLibrary.a(StaticLibrary.o)
  1. 通過文件號,解壓出.o。
lipo libStaticLibrary.a -thin arm64 -output arm64.a
ar -x arm64.a StaticLibrary.o
  1. 通過.o,獲得靜態初始化的符號名_demo_constructor。
objdump -r -section=__mod_init_func StaticLibrary.o

StaticLibrary.o:    file format Mach-O arm64

RELOCATION RECORDS FOR [__mod_init_func]:
0000000000000000 ARM64_RELOC_UNSIGNED _demo_constructor
  1. 通過符號名,文件號,在linkmap中找到符號在二進制中的范圍:
0x100004A30    0x0000001C  [  5] _demo_constructor
  1. 通過起始地址,對代碼進行反匯編:
objdump -d --start-address=0x100004A30 --stop-address=0x100004A4B demo_arm64 
_demo_constructor:
100004a30:    fd 7b bf a9     stp x29, x30, [sp, #-16]!
100004a34:    fd 03 00 91     mov x29, sp
100004a38:    20 0c 80 52     mov w0, #97
100004a3c:    da 06 00 94     bl  #7016 
100004a40:    40 0c 80 52     mov w0, #98
100004a44:    fd 7b c1 a8     ldp x29, x30, [sp], #16
100004a48:    d7 06 00 14     b   #7004 
  1. 通過掃描bl指令掃描子程序調用,子程序在二進制的開始地址為:100004a3c +1b68(對應十進制的7016)。
100004a3c:    da 06 00 94     bl  #7016 
  1. 通過開始地址,可以找到符號名和結束地址,然后重復5~7,遞歸的找到所有的子程序調用的函數符號。

小坑

STL里會針對string生成初始化函數,這樣會導致多個.o里存在同名的符號,例如:

__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1IDnEEPKc

類似這樣的重復符號的情況在C++里有很多,所以C/C++符號在order_file里要帶著所在的.o信息:

//order_file.txt
libDemoLibrary.a(object.o):__GLOBAL__sub_I_demo_file.cpp

局限性
branch系列匯編指令除了bl/b,還有br/blr,即通過寄存器的間接子程序調用,靜態掃描無法覆蓋到這種情況。

Local符號

做C++靜態初始化掃描的時候,會掃描出很多類似l002的符號。這是由于依賴方輸出靜態庫的時候裁剪了local符號。導致__GLOBAL__sub_I_demo_file.cpp 變成了l002。

需要靜態庫出包的時候保留local符號,CI腳本不要執行strip -x,同時Xcode對應target的Build Setting -> Deployment -> Strip Style修改為Debugging symbol

靜態庫保留的local符號會在宿主App生成IPA之前裁剪掉,所以不會對最后的IPA包大小有影響。宿主App的Strip Style要選擇All Symbols,宿主動態庫選擇Non-Global Symbols。

Objective C方法

絕大部分Objective C的方法在編譯后會走objc_msgSend,所以通過fishhook(https://github.com/facebook/fishhook) hook這一個C函數即可獲得Objective C符號。由于objc_msgSend是變長參數,所以hook代碼需要用匯編來實現:http://www.lxweimin.com/p/f0ecf5ca5114

//代碼參考InspectiveC
__attribute__((__naked__))
static void hook_Objc_msgSend() {
    save()
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    call(blr, &before_objc_msgSend)
    load()
    call(blr, orig_objc_msgSend)
    save()
    call(blr, &after_objc_msgSend)
    __asm volatile ("mov lr, x0\n");
    load()
    ret()
}

子程序調用時候要保存和恢復參數寄存器,所以save和load分別對x0~x9, q0~q9入棧/出棧。call則通過寄存器來間接調用函數:

#define save() \
__asm volatile ( \
"stp q6, q7, [sp, #-32]!\n"\
...

#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
...

#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");

在before_objc_msgSend中用棧保存lr,在after_objc_msgSend恢復lr。由于要生成trace文件,為了降低文件的大小,直接寫入的是函數地址,且只有當前可執行文件的Mach-O(app和動態庫)代碼段才會寫入:

iOS中,由于ALSR的存在,在寫入之前需要先減去偏移量slide:

IMP imp = (IMP)class_getMethodImplementation(object_getClass(self), _cmd);
unsigned long imppos = (unsigned long)imp;
unsigned long addr = immpos - macho_slide

獲取一個二進制的__text段地址范圍:

unsigned long size = 0;
unsigned long start = (unsigned long)getsectiondata(mhp,  "__TEXT", "__text", &size);
unsigned long end = start + size;

獲取到函數地址后,反查linkmap既可找到方法的符號名。

Block

block是一種特殊的單元,block在編譯后的函數體是一個C函數,在調用的時候直接通過指針調用,并不走objc_msgSend,所以需要單獨hook。

通過Block的源碼可以看到block的內存布局如下:

struct Block_layout {
    void *isa;
    int32_t flags; // contains ref count
    int32_t reserved;
    void  *invoke;
    struct Block_descriptor1 *descriptor;
};
struct Block_descriptor1 {
    uintptr_t reserved;
    uintptr_t size;
};

其中invoke就是函數的指針,hook思路是將invoke替換為自定義實現,然后在reserved保存為原始實現。

//參考 https://github.com/youngsoft/YSBlockHook
if (layout->descriptor != NULL && layout->descriptor->reserved == NULL)
{
    if (layout->invoke != (void *)hook_block_envoke)
    {
        layout->descriptor->reserved = layout->invoke;
        layout->invoke = (void *)hook_block_envoke;
    }
}

由于block對應的函數簽名不一樣,所以這里仍然采用匯編來實現hook_block_envoke:

__attribute__((__naked__))
static void hook_block_envoke() {
    save()
    __asm volatile ("mov x1, lr\n");
    call(blr, &before_block_hook);
    __asm volatile ("mov lr, x0\n");
    load()
    //調用原始的invoke,即resvered存儲的地址
    __asm volatile ("ldr x12, [x0, #24]\n");
    __asm volatile ("ldr x12, [x12]\n");
    __asm volatile ("br x12\n");
}

在before_block_hook中獲得函數地址(同樣要減去slide)。

intptr_t before_block_hook(id block,intptr_t lr)
{
    Block_layout * layout = (Block_layout *)block;
    //layout->descriptor->reserved即block的函數地址
    return lr;
}

同樣,通過函數地址反查linkmap既可找到block符號。

瓶頸
基于靜態掃描+運行時trace的方案仍然存在少量瓶頸:

  • initialize hook不到
  • 部分block hook不到
  • C++通過寄存器的間接函數調用靜態掃描不出來
    目前的重排方案能夠覆蓋到80%~90%的符號,可以嘗試編譯期插樁等方案來進行100%的符號覆蓋,讓重排達到最優效果。
image.png

設置條件觸發流程

工程注入Trace動態庫,選擇release模式編譯出.app/linkmap/中間產物

運行一次App到啟動結束,Trace動態庫會在沙盒生成Trace log

以Trace Log,中間產物和linkmap作為輸入,運行腳本解析出order_file

clang 插樁

官方文檔 https://clang.llvm.org/docs/SanitizerCoverage.html
clang 插樁主要有兩個實現思路 , 一是自己編寫 clang 插件 , 另外一個就是利用 clang 本身已經提供的一個工具 or 機制來實現我們獲取所有符號的需求 . 本文是第二種思路

新建一個工程,按照文檔指示來走 ,測試和使用一下這個靜態插樁代碼覆蓋工具的機制和原理 .

  1. 首先 , 添加編譯設置 .
    Build Settings -> Apple Clang - Custom Compiler Flags -> Other C Flags 中 , 添加
-fsanitize-coverage=trace-pc-guard
  1. 添加 hook 代碼 .
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);
}

寫在空工程的 ViewController.m 里的打印

INIT: 0x108435788 0x1084357d4
guard: 0x1084357ac a PC 
guard: 0x1084357a0 7 PC \340\272?a\377?
guard: 0x1084357c8 11 PC 
guard: 0x1084357cc 12 PC ?
guard: 0x1084357c8 11 PC ?
guard: 0x1084357c8 11 PC ?
guard: 0x1084357b0 b PC YNu \377?
guard: 0x1084357c8 11 PC  
guard: 0x1084357c8 11 PC \300?}\347\376?
guard: 0x1084357c8 11 PC 
guard: 0x1084357c0 f PC \377\377
guard: 0x1084357b8 d PC \377\377

代碼命名 INIT 后面打印的兩個指針地址叫 start 和 stop . 通過 lldb 來查看下從 start 到 stop 這個內存地址里面所存儲的到底是啥 .

INIT: 0x10bcbd788 0x10bcbd7d4
guard: 0x10bcbd7ac a PC 
guard: 0x10bcbd7a0 7 PC \340\272?a\377?
guard: 0x10bcbd7c8 11 PC 
guard: 0x10bcbd7cc 12 PC ?
guard: 0x10bcbd7c8 11 PC ?
guard: 0x10bcbd7c8 11 PC ?
guard: 0x10bcbd7b0 b PC YNu \377?
guard: 0x10bcbd7c8 11 PC  
guard: 0x10bcbd7c8 11 PC \300\204\364\343\376?
guard: 0x10bcbd7c8 11 PC 
guard: 0x10bcbd7c0 f PC \377\377
guard: 0x10bcbd7b8 d PC \377\377
(lldb) x 0x10bcbd788
0x10bcbd788: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x10bcbd798: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb) x 0x10bcbd7a8
0x10bcbd7a8: 09 00 00 00 0a 00 00 00 0b 00 00 00 0c 00 00 00  ................
0x10bcbd7b8: 0d 00 00 00 0e 00 00 00 0f 00 00 00 10 00 00 00  ................
(lldb) x 0x10bcbd7c8
0x10bcbd7c8: 11 00 00 00 12 00 00 00 13 00 00 00 00 00 00 00  ................
0x10bcbd7d8: 50 94 d2 0b 01 00 00 00 00 00 00 00 00 00 00 00  P...............
(lldb) 

發現存儲的是從 1 到 19 這個序號 . 那么我們來添加一個 oc 方法 .

再次運行查看 .

INIT: 0x10419d7a8 0x10419d7f8
guard: 0x10419d7d0 b PC 
guard: 0x10419d7c4 8 PC \340\272?a\377?
guard: 0x10419d7ec 12 PC 
guard: 0x10419d7f0 13 PC ?
guard: 0x10419d7ec 12 PC ?
guard: 0x10419d7ec 12 PC ?
guard: 0x10419d7d4 c PC YNu \377?
guard: 0x10419d7ec 12 PC  
guard: 0x10419d7ec 12 PC \300\204\246\353\376?
guard: 0x10419d7ec 12 PC 
guard: 0x10419d7e4 10 PC \377\377
guard: 0x10419d7dc e PC \377\377
(lldb) x 0x10419d7a8
0x10419d7a8: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x10419d7b8: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb) x 0x10419d7c8
0x10419d7c8: 09 00 00 00 0a 00 00 00 0b 00 00 00 0c 00 00 00  ................
0x10419d7d8: 0d 00 00 00 0e 00 00 00 0f 00 00 00 10 00 00 00  ................
(lldb) x 0x10419d7e8
0x10419d7e8: 11 00 00 00 12 00 00 00 13 00 00 00 14 00 00 00  ................
0x10419d7f8: 50 94 20 04 01 00 00 00 00 00 00 00 00 00 00 00  P. .............
(lldb) 

發現從 13 變成了 14 . 也就是說存儲的 1 到 19 這個序號變成了 1 到 20 .
那么我們再添加一個 c 函數 , 一個 block , 和一個觸摸屏幕方法來看下 .

void testCFun(){
    NSLog(@"C函數");
}

- (void)testOCFunc{
    
}

void (^testBlock)(void) = ^(){
    NSLog(@"block");
};
//
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

}
INIT: 0x10fe987c0 0x10fe9881c
guard: 0x10fe987f4 e PC 
guard: 0x10fe987e8 b PC \340\272?a\377?
guard: 0x10fe98810 15 PC 
guard: 0x10fe98814 16 PC ?
guard: 0x10fe98810 15 PC ?
guard: 0x10fe98810 15 PC ?
guard: 0x10fe987f8 f PC YNu \377?
guard: 0x10fe98810 15 PC  
guard: 0x10fe98810 15 PC \300\324\326\337\376?
guard: 0x10fe98810 15 PC 
guard: 0x10fe98808 13 PC \377\377
guard: 0x10fe98800 11 PC \377\377
(lldb) x 0x10fe9881c-0x4
0x10fe98818: 17 00 00 00 00 00 00 00 50 44 f0 0f 01 00 00 00  ........PD......
0x10fe98828: 90 30 e9 0f 01 00 00 00 00 00 00 00 00 00 00 00  .0..............
(lldb) 

變成了23 , 可以得到一個猜想 , 這個內存區間保存的就是工程所有符號的個數 .

其次 , 點擊四次屏幕,

guard: 0x10163e7e4 a PC 
guard: 0x10163e7e4 a PC \275\223?a\377?
guard: 0x10163e7e4 a PC \275\223?a\377?
guard: 0x10163e7e4 a PC \275\223?a\377?

我們在觸摸屏幕方法調用了 c 函數 , c 函數中調用了 block . 之后點擊四次屏幕 ,

guard: 0x10f0557ec a PC 
guard: 0x10f0557e0 7 PC 
2021-01-07 17:24:58.234205+0800 WhatFuck[58381:5771061] C函數
guard: 0x10f0557e8 9 PC 
2021-01-07 17:24:58.234330+0800 WhatFuck[58381:5771061] block
guard: 0x10f0557ec a PC \275\223?a\377?
guard: 0x10f0557e0 7 PC 7\331???
2021-01-07 17:25:14.187490+0800 WhatFuck[58381:5771061] C函數
guard: 0x10f0557e8 9 PC \267T?a\377?
2021-01-07 17:25:14.187779+0800 WhatFuck[58381:5771061] block
guard: 0x10f0557ec a PC \275\223?a\377?
guard: 0x10f0557e0 7 PC 7\331???
2021-01-07 17:25:15.655994+0800 WhatFuck[58381:5771061] C函數
guard: 0x10f0557e8 9 PC \267T?a\377?
2021-01-07 17:25:15.656135+0800 WhatFuck[58381:5771061] block
guard: 0x10f0557ec a PC \275\223?a\377?
guard: 0x10f0557e0 7 PC 7\331???
2021-01-07 17:25:15.985723+0800 WhatFuck[58381:5771061] C函數
guard: 0x10f0557e8 9 PC \267T?a\377?
2021-01-07 17:25:15.985874+0800 WhatFuck[58381:5771061] block

發現我們實際調用幾個方法 , 就會打印幾次 guard : .

類似我們埋點統計所實現的效果 . 在觸摸方法添加一個斷點查看匯編 :

通過匯編我們發現 , 在每個函數調用的第一句實際代碼 ( 棧平衡與寄存器數據準備除外 ) , 被添加進去了一個 bl 調用到 __sanitizer_cov_trace_pc_guard 這個函數中來 .
這也是靜態插樁的原理和名稱由來 .

`-[ViewController touchesBegan:withEvent:]:
    0x101ce3910 <+0>:   pushq  %rbp
    0x101ce3911 <+1>:   movq   %rsp, %rbp
    0x101ce3914 <+4>:   subq   $0x40, %rsp
    0x101ce3918 <+8>:   leaq   0x7ecd(%rip), %rax
    0x101ce391f <+15>:  movq   %rdi, -0x28(%rbp)
    0x101ce3923 <+19>:  movq   %rax, %rdi
    0x101ce3926 <+22>:  movq   %rsi, -0x30(%rbp)
    0x101ce392a <+26>:  movq   %rdx, -0x38(%rbp)
    0x101ce392e <+30>:  movq   %rcx, -0x40(%rbp)
    0x101ce3932 <+34>:  callq  0x101ce41ce               ; symbol stub for: __sanitizer_cov_trace_pc_guard
    0x101ce3937 <+39>:  movq   -0x28(%rbp), %rax
    0x101ce393b <+43>:  movq   %rax, -0x8(%rbp)
    0x101ce393f <+47>:  movq   -0x30(%rbp), %rcx
    0x101ce3943 <+51>:  movq   %rcx, -0x10(%rbp)
    0x101ce3947 <+55>:  movq   $0x0, -0x18(%rbp)
    0x101ce394f <+63>:  leaq   -0x18(%rbp), %rdx
    0x101ce3953 <+67>:  movq   -0x38(%rbp), %rsi
    0x101ce3957 <+71>:  movq   %rdx, %rdi
    0x101ce395a <+74>:  callq  0x101ce4216               ; symbol stub for: objc_storeStrong
    0x101ce395f <+79>:  movq   $0x0, -0x20(%rbp)
    0x101ce3967 <+87>:  leaq   -0x20(%rbp), %rax
    0x101ce396b <+91>:  movq   -0x40(%rbp), %rcx
    0x101ce396f <+95>:  movq   %rax, %rdi
    0x101ce3972 <+98>:  movq   %rcx, %rsi
    0x101ce3975 <+101>: callq  0x101ce4216               ; symbol stub for: objc_storeStrong
->  0x101ce397a <+106>: callq  0x101ce4222               ; symbol stub for: testCFun
    0x101ce397f <+111>: xorl   %r8d, %r8d

靜態插樁總結

靜態插樁實際上是在編譯期就在每一個函數內部二進制源數據添加 hook 代碼 ( 我們添加的 __sanitizer_cov_trace_pc_guard 函數 ) 來實現全局的方法 hook 的效果 .

究竟是直接修改二進制在每個函數內部都添加了調用 hook 函數這個匯編代碼 , 還是只是類似于編譯器在所生成的二進制文件添加了一個標記 , 然后在運行時如果有這個標記就會自動多做一步調用 hook 代碼呢 ?

使用 hopper 來看下生成的 mach-o 二進制文件 .

                     -[ViewController touchesBegan:withEvent:]:
0000000100001910         push       rbp                                         ; Objective C Implementation defined at 0x1000082e0 (instance method), DATA XREF=0x1000082e0
0000000100001911         mov        rbp, rsp
0000000100001914         sub        rsp, 0x40
0000000100001918         lea        rax, qword [0x1000097ec]
000000010000191f         mov        qword [rbp+var_28], rdi
0000000100001923         mov        rdi, rax
0000000100001926         mov        qword [rbp+var_30], rsi
000000010000192a         mov        qword [rbp+var_38], rdx
000000010000192e         mov        qword [rbp+var_40], rcx
0000000100001932         call       imp___stubs____sanitizer_cov_trace_pc_guard
0000000100001937         mov        rax, qword [rbp+var_28]
000000010000193b         mov        qword [rbp+var_8], rax
000000010000193f         mov        rcx, qword [rbp+var_30]
0000000100001943         mov        qword [rbp+var_10], rcx
0000000100001947         mov        qword [rbp+var_18], 0x0
000000010000194f         lea        rdx, qword [rbp+var_18]
0000000100001953         mov        rsi, qword [rbp+var_38]                     ; argument "value" for method imp___stubs__objc_storeStrong
0000000100001957         mov        rdi, rdx                                    ; argument "addr" for method imp___stubs__objc_storeStrong
000000010000195a         call       imp___stubs__objc_storeStrong
000000010000195f         mov        qword [rbp+var_20], 0x0
0000000100001967         lea        rax, qword [rbp+var_20]
000000010000196b         mov        rcx, qword [rbp+var_40]
000000010000196f         mov        rdi, rax                                    ; argument "addr" for method imp___stubs__objc_storeStrong
0000000100001972         mov        rsi, rcx                                    ; argument "value" for method imp___stubs__objc_storeStrong
0000000100001975         call       imp___stubs__objc_storeStrong
000000010000197a         call       imp___stubs__testCFun
000000010000197f         xor        r8d, r8d
0000000100001982         mov        esi, r8d                                    ; argument "value" for method imp___stubs__objc_storeStrong
0000000100001985         lea        rax, qword [rbp+var_20]
0000000100001989         mov        rdi, rax                                    ; argument "addr" for method imp___stubs__objc_storeStrong
000000010000198c         call       imp___stubs__objc_storeStrong
0000000100001991         xor        r8d, r8d
0000000100001994         mov        esi, r8d                                    ; argument "value" for method imp___stubs__objc_storeStrong
0000000100001997         lea        rax, qword [rbp+var_18]
000000010000199b         mov        rdi, rax                                    ; argument "addr" for method imp___stubs__objc_storeStrong
000000010000199e         call       imp___stubs__objc_storeStrong
00000001000019a3         add        rsp, 0x40
00000001000019a7         pop        rbp
00000001000019a8         ret
                        ; endp
00000001000019a9         nop        dword [rax]

的確是函數內部 一開始就添加了 調用額外方法的匯編代碼 . 這也是我們為什么稱其為 " 靜態插樁 " .

獲取所有函數符號

原理大體上了解了 , 那么如何才能拿到函數的符號

思路

我們現在知道了 , 所有函數內部第一步都會去調用 __sanitizer_cov_trace_pc_guard 這個函數 . 那么函數嵌套時 , 在跳轉子函數時都會保存下一條指令的地址在 X30 ( 又叫 lr 寄存器) 里 .

例如 , A 函數中調用了 B 函數 , 在 arm 匯編中即 bl + 0x**** 指令 , 該指令會首先將下一條匯編指令的地址保存在 x30 寄存器中 ,]

然后在跳轉到 bl 后面傳遞的指定地址去執行 . ( 提示 : bl 能實現跳轉到某個地址的匯編指令 , 其原理就是修改 pc 寄存器的值來指向到要跳轉的地址 , 而且實際上 B 函數中也會對 x29 / x30 寄存器的值做保護防止子函數又跳轉其他函數會覆蓋掉 x30 的值 , 當然 , 葉子函數除外 . ) .

當 B 函數執行 ret 也就是返回指令時 , 就會去讀取 x30 寄存器的地址 , 跳轉過去 , 因此也就回到了上一層函數的下一步 .
這種思路來實現實際上是可以的 . 我們所寫的 __sanitizer_cov_trace_pc_guard 函數中的這一句代碼 :

void *PC = __builtin_return_address(0); 

它的作用其實就是去讀取 x30 中所存儲的要返回時下一條指令的地址 . 所以他名稱叫做__builtin_return_address . 換句話說 , 這個地址就是我當前這個函數執行完畢后 , 要返回到哪里去 .

也就是說 , 我們現在可以在 __sanitizer_cov_trace_pc_guard 這個函數中 , 通過 __builtin_return_address 數拿到原函數調用 __sanitizer_cov_trace_pc_guard 這句匯編代碼的下一條指令的地址 .

根據內存地址獲取函數名稱

拿到了函數內部 下一行代碼 的地址 , 需要獲取函數名稱

熟悉安全攻防 , 逆向的同學可能會清楚 . 我們為了防止某些特定的方法被別人使用 fishhook hook 掉 , 會利用 dlopen 打開動態庫 , 拿到一個句柄 , 進而拿到函數的內存地址直接調用 .

是不是跟我們這個流程有點相似 , 只是我們好像是反過來的 . 其實反過來也是可以的 .

dlopen 相同 , 在 dlfcn.h 中有一個方法如下 :

typedef struct dl_info {
        const char      *dli_fname;     /* 所在文件 */
        void            *dli_fbase;     /* 文件地址 */
        const char      *dli_sname;     /* 符號名稱 */
        void            *dli_saddr;     /* 函數起始地址 */
} Dl_info;

//這個函數能通過函數內部地址找到函數符號
int dladdr(const void *, Dl_info *);

實驗一下 , 先導入頭文件#import <dlfcn.h> , 然后修改代碼如下 :

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;  // Duplicate the guard check.
    
    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    
    printf("fname=%s \nfbase=%p \nsname=%s\nsaddr=%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
    
    char PcDescr[1024];
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

打印結果

INIT: 0x10d8447d0 0x10d84482c
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=main
saddr=0x10d83cca0 
guard: 0x10d844804 e PC 
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[AppDelegate application:didFinishLaunchingWithOptions:]
saddr=0x10d83c9a0 
guard: 0x10d8447f8 b PC \340\272?a\377?
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC 
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate setWindow:]
saddr=0x10d83d0c0 
guard: 0x10d844824 16 PC ?
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC ?
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC ?
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate scene:willConnectToSession:options:]
saddr=0x10d83cd70 
guard: 0x10d844808 f PC YNu \377?
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC \300?<\342\376?
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC ?\225\204
?
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate window]
saddr=0x10d83d070 
guard: 0x10d844820 15 PC 
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate sceneWillEnterForeground:]
saddr=0x10d83cf90 
guard: 0x10d844818 13 PC \377\377
fname=/Users/geneqiao/Library/Developer/CoreSimulator/Devices/E85FCCF7-A3B0-473F-9759-4172A466E9BD/data/Containers/Bundle/Application/45D702CA-A27F-4AC6-8326-46859B66D239/WhatFuck.app/WhatFuck 
fbase=0x10d83b000 
sname=-[SceneDelegate sceneDidBecomeActive:]
saddr=0x10d83ceb0 
guard: 0x10d844810 11 PC \377\377

收集符號

多線程問題

由于項目各個方法肯定有可能會在不同的線程執行 , 因此 __sanitizer_cov_trace_pc_guard 這個函數也有可能受多線程影響 , 所以不能簡簡單單用一個數組來接收所有的符號就搞定了 .

考慮到這個方法會來特別多次 , 使用鎖會影響性能 , 這里使用蘋果底層的原子隊列 ( 底層實際上是個棧結構 , 利用隊列結構 + 原子性來保證順序 ) 來實現

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //遍歷出隊
    while (true) {
        //offsetof 就是針對某個結構體找到某個屬性相對這個結構體的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);
        
        printf("%s \n",info.dli_sname);
    }
}
//原子隊列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定義符號結構體
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;  // Duplicate the guard check.
    void *PC = __builtin_return_address(0);
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入隊
    // offsetof 用在這里是為了入隊添加下一個節點找到 前一個節點next指針的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}

上述這種 clang 插樁的方式 , 會在循環中同樣插入 hook 代碼 .

當確定了隊列入隊和出隊都是沒問題的 , 對應的保存和讀取也是沒問題的 ,但會死循環 .因為dladdr()會走這個函數

通過匯編會查看到 一個帶有 while 循環的方法 , 會被靜態加入多次 __sanitizer_cov_trace_pc_guard 調用 , 導致死循環.

解決方案

Other C Flags 修改為如下 ,代表僅針對 func 進行 hook . 再次運行 .

-fsanitize-coverage=func,trace-pc-guard
load 方法

load 方法時 , __sanitizer_cov_trace_pc_guard 函數的參數 guard 是 0.

上述打印并沒有發現 load . 因此 屏蔽掉 __sanitizer_cov_trace_pc_guard 函數中的

if (!*guard) return;

load 方法就有了 .

如果我們希望從某個函數之后/之前開始優化 , 通過一個全局靜態變量 , 在特定的時機修改其值 , 在 __sanitizer_cov_trace_pc_guard 這個函數中做好對應的處理即可 .

使用注意

  • 由于用的先進后出原因 , 我們要倒敘一下
  • 需要做去重 .
  • order 文件格式要求c 函數 , block 調用前面還需要加 _ , 下劃線 .
  • 寫入文件即可 .

全部代碼

#import "ViewController.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
@interface ViewController ()
@end

@implementation ViewController
+ (void)load{
    
}
- (void)viewDidLoad {
    [super viewDidLoad];
    testCFunc();
    [self testOCFunc];
}
- (void)testOCFunc{
    NSLog(@"oc函數");
}
void testCFunc(){
    LBBlock();
}
void(^LBBlock)(void) = ^(void){
    NSLog(@"block");
};

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)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    while (true) {
        //offsetof 就是針對某個結構體找到某個屬性相對這個結構體的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);
        
        NSString * name = @(info.dli_sname);
        
        // 添加 _
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        
        //去重
        if (![symbolNames containsObject:symbolName]) {
            [symbolNames addObject:symbolName];
        }
    }

    //取反
    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
    NSLog(@"%@",symbolAry);
    
    //將結果寫入到文件
    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"%@",filePath);
    }else{
        NSLog(@"文件寫入出錯");
    }
    
}
//原子隊列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定義符號結構體
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    //if (!*guard) return;  // Duplicate the guard check.
    
    void *PC = __builtin_return_address(0);
    
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入隊
    // offsetof 用在這里是為了入隊添加下一個節點找到 前一個節點next指針的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}
@end

wift 工程 / 混編工程問題

通過如上方式適合純 OC 工程獲取符號方式 .

由于 swift 的編譯器前端是自己的 swift 編譯前端程序 , 因此配置稍有不同 .

搜索 Other Swift Flags , 添加兩條配置即可 :

  • -sanitize-coverage=func
  • -sanitize=undefined

swift 類通過上述方法同樣可以獲取符號 .

cocoapod 工程問題

對于 cocoapod 工程引入的庫 , 由于針對不同的 target . 那么我們在主程序中的 target 添加的編譯設置 Write Link Map File , -fsanitize-coverage=func,trace-pc-guard 以及 order file 等設置肯定是不會生效的 . 解決方法就是針對需要的 target 去做對應的設置即可 .

對于直接手動導入到工程里的 sdk , 不管是 靜態庫 .a 還是 動態庫 , 默認主工程的設置就可以了 , 是可以拿到符號的 .

手動導入的三方庫如果沒有導入并且使用的話 , 是不會加載的 . 添加了 load 方法也是如此 .

參考鏈接

https://mp.weixin.qq.com/s/Drmmx5JtjG3UtTFksL6Q8Q

https://juejin.cn/post/6844904130406793224

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。