從彙編程式碼理解 Block 的記憶體結構
- 2020 年 4 月 10 日
- 筆記
❓ 在斷點調試 iOS 程式碰到 block 作為函數的形參時,如果想知道該 block 本身的函數簽名資訊和函數體地址時,有哪些辦法?
? 當然是在源碼裡面直接查看 block 的聲明和調用了!
❗️ 但如果源碼不可見呢?在分析第三方閉源庫或友商 App 的某些邏輯實現時,就只有彙編程式碼可用
☕️ 本文將通過彙編程式碼入手探討 block 的記憶體結構,並嘗試還原 block 的函數簽名資訊和函數體真實地址
環境準備
如果拿真實案例來做分析,勢必會有一系列繁瑣的前期準備步驟,而且一時也想不起來有哪些友商 App 剛好可以做 demo,所以本文將使用本地環境做模擬,並直接從最關鍵的步驟開始
-
打開 Xcode,新建一個
Single View App
,將下面的程式碼貼入ViewController.m
中,準備好證書和 64bit 真機。#import "ViewController.h" typedef void(^HHBlock)(NSString *,NSInteger ); @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; void (^testBlock)(NSString *, NSInteger ) = ^(NSString *var1, NSInteger var2){ NSLog(@"var1 = %@, var2 = %@", var1, @(var2)); }; [self doBlockTest:testBlock]; } - (void)doBlockTest:(HHBlock)block { block(@"v1",10086); } @end
-
為了盡量模擬真實環境也為了去掉彙編中不必要的指令,在 Xcode 的 Scheme 中將
Run -> Build Configuration
修改為 Release ;然後在[self doBlockTest:testBlock];
這行下斷點 -
點
Run
,等運行到斷點位置後,勾選 Xcode 菜單Debug -> Debug Workflow -> Always Show Disassembly
打開彙編指令視圖:
調用前的分析
從現在開始,我們假裝忘記了剛剛寫的源碼,嘗試從彙編程式碼中得到 block 的基本資訊。
-
從第 14 行開始分析:
L14.nop
: 空指令,什麼也不做,猜測是和記憶體對齊有關
L15.ldr x1, #0x3260
: 將#0x3260
指向的記憶體數據載入到暫存器x1
中
L16.adr x2, #0x1dfc
: 將#0x1dfc
指向的記憶體數據載入到暫存器x2
中
L17. 同第 14 行
L18.mov x0, x19
: 將 19 號暫存器的值複製到 0 號暫存器
L19.bl
: 調用0x100582540
處的函數,即objc_msgSend
的調用 -
按住
Control
鍵,通過點Step info
單步執行到第 19 行。objc_msgSend
原型為objc_msgSend(id self, SEL op,...)
,本例中有三個參數,按照約定這三個參數將依次放在x0
、x1
、x2
中。所以x0
為ViewController
實例,x1
為 selector,x2
為 block 結構體指針。讀取暫存器和列印x0
可驗證:
block 類型形參的分析
前面的更多是彙編基礎的回顧,現在即將進入重點
-
點
Step over
進入到真正被調用函數,如圖:
按照剛剛的分析,x2
裡面就是這個 block 形參,直接po
該形參是沒有函數體地址和函數簽名等資訊的:
-
嘗試還原 block 資訊,按行分析:
L3.ldr x3, [x2, #0x10]
: 將x2
指向的內容加上 16 個位元組偏移後的地址,載入到x3
中
L4.adr x1, #0x1d94
: 將#0x1d94
指向的內容載入到x1
L5. 同上
L6.mov w2, #0x2766
: 將#0x2766
複製到w2
的低 32 位上。0x2766
也就是十進位的10086
L7.br x3
: 跳轉到x3
指向的地址上執行 -
根據上下文,第 2 行中的
x2
中就是 block 形參指針(0x0000000100584090
),在第 3 行中,取了該指針指向內容並偏移0x10
處的地址 P 賦值給了x3
,在第 7 行中執行了x3
指向的內容(也是一個地址),說明 P 是一個函數(函數指針)可被執行,這正是 block 的特性。參考 block 的記憶體結構衍生出來的面試題 -
struct Block_literal_1 { void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 { unsigned long int reserved; // NULL unsigned long int size; // sizeof(struct Block_literal_1) // optional helper functions void (*copy_helper)(void *dst, void *src); // IFF (1<<25) void (*dispose_helper)(void *src); // IFF (1<<25) // required ABI.2010.3.16 const char *signature; // IFF (1<<30) } *descriptor; // imported variables };
block 結構體指針偏移
sizeof(void *) + sizeof(int) + sizeof(reserved)
後就是真正函數體invoke
的地址。在 64bit 系統上指針類型和int
類型分別佔用 8 個位元組和 4 個位元組,考慮到結構體記憶體對齊,真正的函數體的偏移量為: 8 + 8 = 0x10,即 16 個位元組,invoke
的地址為:結構體指針地址 + offset(16 位元組)
。
接下來我們讀取這個函數體地址,首先列印 block 結構體指針指向的內容:
結構體內容開始於
0x00000001b365a288
,函數體invoke
地址開始於 16 位元組後,佔用 8 個位元組,所以其地址為:0x00000001005822b4
,我們在這裡下一個斷點:br s -a 0x00000001005822b4
,等會驗證下是不是剛好斷在 block 的實現裡面 -
獲取 block 的函數簽名
函數簽名在Block_descriptor_1
類型結構體descriptor
的signature
成員變數上,我們目標是通過signature
得到NSMethodSignature
實例,進而得到invoke
詳細的函數簽名資訊。descriptor
的偏移量:offset(*invoke) + sizeof(*invoke)
,所以descriptor
的地址為0x0000000100584070
。另外根據文檔,並不是所有 block 都存在這個signature
變數,需要通過flags
與 block 中定義的枚舉掩碼進行&
操作來判斷,枚舉掩碼定義:enum { // Set to true on blocks that have captures (and thus are not true // global blocks) but are known not to escape for various other // reasons. For backward compatibility with old runtimes, whenever // BLOCK_IS_NOESCAPE is set, BLOCK_IS_GLOBAL is set too. Copying a // non-escaping block returns the original block and releasing such a // block is a no-op, which is exactly how global blocks are handled. BLOCK_IS_NOESCAPE = (1 << 23), BLOCK_HAS_COPY_DISPOSE = (1 << 25), BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code BLOCK_IS_GLOBAL = (1 << 28), BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE BLOCK_HAS_SIGNATURE = (1 << 30), };
按照剛剛的方法,嘗試獲取
flags
的值:
flags
的值為0x50000000
,判斷後發現存在signature
:
signature
近在咫尺!但是仔細看 block 文檔的定義,排在signature
前面的兩個函數指針copy_helper
和dispose_helper
有這句注釋:optional helper functions
,看來還需要判斷這兩個指針是否存在:
嗯,不存在,所以
signature
的偏移量:offset(long int) + offset(long int)
,長度sizeof(char *)
,接著列印descriptor
的內容:
所以
signature
的值為0x0000000100583382
,列印看看是什麼:
眼熟的 Type Encodings 字元串,轉換成
NSMethodSignature
的實例看看:
這個 block 函數體沒有返回值(
v
->void
),接收 3 個參數,第一個是 block ——這是所有 block 函數體的潛規則,後續兩個的參數依次為:@
(NSString *
) 和q
(long long
),和源碼一致。 -
函數體地址正確性驗證
還記得第 4 步在0x00000001005822b4
設置的斷點嗎?點Continue
或 lldb 輸入c
,讓程式繼續執行,結果在0x00000001005822b4
處被斷住了:
而且斷住的位置在ViewController.m:19
,查看源碼發現剛好就是 block 的實現:
PS:本人彙編小白,難免有理解不到位或者錯誤的地方,希望大佬多多指教!