從彙編程式碼理解 Block 的記憶體結構

  • 2020 年 4 月 10 日
  • 筆記

❓ 在斷點調試 iOS 程式碰到 block 作為函數的形參時,如果想知道該 block 本身的函數簽名資訊和函數體地址時,有哪些辦法?
? 當然是在源碼裡面直接查看 block 的聲明和調用了!
❗️ 但如果源碼不可見呢?在分析第三方閉源庫或友商 App 的某些邏輯實現時,就只有彙編程式碼可用

☕️ 本文將通過彙編程式碼入手探討 block 的記憶體結構,並嘗試還原 block 的函數簽名資訊和函數體真實地址

環境準備

如果拿真實案例來做分析,勢必會有一系列繁瑣的前期準備步驟,而且一時也想不起來有哪些友商 App 剛好可以做 demo,所以本文將使用本地環境做模擬,並直接從最關鍵的步驟開始

  1. 打開 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  
  2. 為了盡量模擬真實環境也為了去掉彙編中不必要的指令,在 Xcode 的 Scheme 中將 Run -> Build Configuration 修改為 Release ;然後在 [self doBlockTest:testBlock]; 這行下斷點

  3. Run,等運行到斷點位置後,勾選 Xcode 菜單 Debug -> Debug Workflow -> Always Show Disassembly 打開彙編指令視圖:

調用前的分析

從現在開始,我們假裝忘記了剛剛寫的源碼,嘗試從彙編程式碼中得到 block 的基本資訊。

  1. 從第 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 的調用

  2. 按住 Control 鍵,通過點 Step info 單步執行到第 19 行。objc_msgSend 原型為objc_msgSend(id self, SEL op,...),本例中有三個參數,按照約定這三個參數將依次放在x0x1x2 中。所以 x0ViewController 實例,x1 為 selector,x2 為 block 結構體指針。讀取暫存器和列印 x0 可驗證:

block 類型形參的分析

前面的更多是彙編基礎的回顧,現在即將進入重點

  1. Step over 進入到真正被調用函數,如圖:

    按照剛剛的分析,x2 裡面就是這個 block 形參,直接 po該形參是沒有函數體地址和函數簽名等資訊的:

  2. 嘗試還原 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 指向的地址上執行

  3. 根據上下文,第 2 行中的 x2 中就是 block 形參指針(0x0000000100584090),在第 3 行中,取了該指針指向內容並偏移 0x10 處的地址 P 賦值給了 x3,在第 7 行中執行了 x3 指向的內容(也是一個地址),說明 P 是一個函數(函數指針)可被執行,這正是 block 的特性。參考 block 的記憶體結構衍生出來的面試題

  4. 根據 llvm 官網對 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 的實現裡面

  5. 獲取 block 的函數簽名
    函數簽名在 Block_descriptor_1 類型結構體 descriptorsignature 成員變數上,我們目標是通過 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_helperdispose_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),和源碼一致。

  6. 函數體地址正確性驗證
    還記得第 4 步在 0x00000001005822b4 設置的斷點嗎?點 Continue 或 lldb 輸入 c,讓程式繼續執行,結果在 0x00000001005822b4 處被斷住了:
    而且斷住的位置在 ViewController.m:19,查看源碼發現剛好就是 block 的實現:

PS:本人彙編小白,難免有理解不到位或者錯誤的地方,希望大佬多多指教!

Ref