iOS彙編教程(五)Objc Block 的內存佈局和彙編表示

系列文章

  1. iOS彙編入門教程(一)ARM64彙編基礎
  2. iOS彙編入門教程(二)在Xcode工程中嵌入彙編代碼
  3. iOS彙編入門教程(三)彙編中的 Section 與數據存取
  4. iOS彙編教程(四)基於 LLDB 動態調試快速分析系統函數的實現

前言

在 Objc 中,Block 是一個特殊的對象,它的實例並不是是常規的對象結構,而是以 Block_layout 結構體的形式存在。在聲明時,Block 的結構體會以值類型的形式直接存儲在棧上,隨後會被 copy 到堆上,成爲一個特殊的對象,學習 Block 的底層原理一方面可以掌握複雜值類型的存儲和傳遞方式,另外一方面也能在逆向分析遇到 Block 時快速定位與分析相關邏輯。git

Block 的結構

Block 的結構能夠在 Runtime 的開源代碼 Objc4-706 中找到,它位於 Block-private.h 中:github

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
複製代碼

對比常規的 OC 對象 objc_object 結構:bash

struct objc_object {
private:
    isa_t isa; // union contains Class
    // ivar instances
}
複製代碼

能夠發現 Block 和常規對象有殊途同歸之妙,都是經過 isa 指向的類對象記錄基本信息,區別在於 Block 對象後面跟的是捕獲的變量列表,而常規對象後面跟的是 ivar 實例列表。iphone

Block 的彙編表示

下面咱們用一個簡單的例子來分析生成的彙編代碼:函數

// block.m
#import <Foundation/Foundation.h>

typedef int (^CommonBlock)(void);

CommonBlock simpleBlockOnStack() {
    int a = 1, b = 2, c = 3, d = 4, e = 5;
    int (^theBlock)(void) = ^int {
        return a + b + c + d + e;
    };
    return theBlock;
}

void invokeStackBlock() {
    CommonBlock block = simpleBlockOnStack();
    block();
}

int main(int argc, char *argv[]) {
    invokeStackBlock();
    return 0;
}
複製代碼

使用 clang 生成 a.out:佈局

clang -arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path` block.m -framework Foundation -fobjc-arc
複製代碼

將 a.out 拖入 IDA 或 Hopper 中進行反彙編,結合 simpleBlockOnStack 和 invokeStackBlock 兩個符號來分析 Block 的建立、傳遞和調用過程。post

注意,var_XY 的值是 -0xXY,var_s0 的值是 0。學習

Block 的建立過程

下面是 _simpleBlockOnStack 符號的反彙編結果:ui

__text:0000000100007D9C                 SUB             SP, SP, #0x70
__text:0000000100007DA0                 STP             X29, X30, [SP,#0x60+var_s0]
__text:0000000100007DA4                 ADD             X29, SP, #0x60
__text:0000000100007DA8                 MOV             W8, #1
__text:0000000100007DAC                 STUR            W8, [X29,#var_4]
__text:0000000100007DB0                 MOV             W8, #2
__text:0000000100007DB4                 STUR            W8, [X29,#var_8]
__text:0000000100007DB8                 MOV             W8, #3
__text:0000000100007DBC                 STUR            W8, [X29,#var_C]
__text:0000000100007DC0                 MOV             W8, #4
__text:0000000100007DC4                 STUR            W8, [X29,#var_10]
__text:0000000100007DC8                 MOV             W8, #5
__text:0000000100007DCC                 STUR            W8, [X29,#var_14]
__text:0000000100007DD0                 ADRP            X9, #__NSConcreteStackBlock_ptr@PAGE
__text:0000000100007DD4                 LDR             X9, [X9,#__NSConcreteStackBlock_ptr@PAGEOFF]
__text:0000000100007DD8                 STR             X9, [SP,#0x60+var_58]
__text:0000000100007DDC                 MOV             W8, #0xC0000000
__text:0000000100007DE0                 STR             W8, [SP,#0x60+var_50]
__text:0000000100007DE4                 MOV             W8, #0
__text:0000000100007DE8                 STR             W8, [SP,#0x60+var_4C]
__text:0000000100007DEC                 ADRP            X9, #___simpleBlockOnStack_block_invoke@PAGE
__text:0000000100007DF0                 ADD             X9, X9, #___simpleBlockOnStack_block_invoke@PAGEOFF
__text:0000000100007DF4                 STR             X9, [SP,#0x60+var_48]
__text:0000000100007DF8                 ADRP            X9, #___block_descriptor_52_e5_i8__0l@PAGE
__text:0000000100007DFC                 ADD             X9, X9, #___block_descriptor_52_e5_i8__0l@PAGEOFF
__text:0000000100007E00                 STR             X9, [SP,#0x60+var_40]
__text:0000000100007E04                 LDUR            W8, [X29,#var_4]
__text:0000000100007E08                 STR             W8, [SP,#0x60+var_38]
__text:0000000100007E0C                 LDUR            W8, [X29,#var_8]
__text:0000000100007E10                 STR             W8, [SP,#0x60+var_34]
__text:0000000100007E14                 LDUR            W8, [X29,#var_C]
__text:0000000100007E18                 STR             W8, [SP,#0x60+var_30]
__text:0000000100007E1C                 LDUR            W8, [X29,#var_10]
__text:0000000100007E20                 STR             W8, [SP,#0x60+var_2C]
__text:0000000100007E24                 LDUR            W8, [X29,#var_14]
__text:0000000100007E28                 STR             W8, [SP,#0x60+var_28]
__text:0000000100007E2C                 ADD             X0, SP, #0x60+var_58
__text:0000000100007E30                 BL              _objc_retainBlock
__text:0000000100007E34                 STUR            X0, [X29,#var_20]
__text:0000000100007E38                 LDUR            X0, [X29,#var_20]
__text:0000000100007E3C                 BL              _objc_retainBlock
__text:0000000100007E40                 SUB             X9, X29, #-var_20
__text:0000000100007E44                 MOV             X30, #0
__text:0000000100007E48                 STR             X0, [SP,#0x60+var_60]
__text:0000000100007E4C                 MOV             X0, X9
__text:0000000100007E50                 MOV             X1, X30
__text:0000000100007E54                 BL              _objc_storeStrong
__text:0000000100007E58                 LDR             X0, [SP,#0x60+var_60]
__text:0000000100007E5C                 LDP             X29, X30, [SP,#0x60+var_s0]
__text:0000000100007E60                 ADD             SP, SP, #0x70
__text:0000000100007E64                 B               _objc_autoreleaseReturnValue
複製代碼

顯然,從 7DA8 到 7DCC 的部分是對函數 simpleBlockOnStack 開頭的五個 int 變量 a-e 的定義,以當前棧幀的起始地址爲零點(後面討論棧上地址時都以此爲前提),變量 a-e 分別被存儲在棧的 -0x14 ~ -0x24 區域,spa

Block ISA

接下來 7DD0 - 7DD4 的代碼取出的 __NSConcreteStackBlock_ptr 是指向 __NSConcreteStackBlock 的指針,而 NSConcreteStackBlock 就是 Block 的 isa 數據。

__text:0000000100007DD0                 ADRP            X9, #__NSConcreteStackBlock_ptr@PAGE
__text:0000000100007DD4                 LDR             X9, [X9,#__NSConcreteStackBlock_ptr@PAGEOFF]
__text:0000000100007DD8                 STR             X9, [SP,#0x60+var_58]
複製代碼

它被存儲在了棧的 -0x68 區域(IDA中,var_XY = -0xXY,SP 指向 -0x70,-0x70 + 0x60 + (-0x58) = -0x68)。

Flags & Reserved

隨後緊接着的 4 句是 flags 和 reserved 的存儲邏輯,根據文章開頭給出的結構,他們是兩個 int 變量,Wn 寄存器取的是 Xn 的低 32 位,即一個 Word = 4B,正好是一個 int 的長度,他們分別存儲在棧的 -0x60 和 -0x5C 區域。

__text:0000000100007DDC                 MOV             W8, #0xC0000000
__text:0000000100007DE0                 STR             W8, [SP,#0x60+var_50]
__text:0000000100007DE4                 MOV             W8, #0
__text:0000000100007DE8                 STR             W8, [SP,#0x60+var_4C]
複製代碼

Block Invoker

接下來 3 句是 Block Invoker 的存儲邏輯,Block Invoker 就是 Block 的邏輯的函數指針,它被存儲在了棧的 -0x58 區域。

__text:0000000100007DEC                 ADRP            X9, #___simpleBlockOnStack_block_invoke@PAGE
__text:0000000100007DF0                 ADD             X9, X9, #___simpleBlockOnStack_block_invoke@PAGEOFF
__text:0000000100007DF4                 STR             X9, [SP,#0x60+var_48]
複製代碼

Block Descriptor

接下來是 Block Descriptor 的存儲邏輯,Descriptor 的結構爲:

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};
複製代碼

uintptr_t 是 unsigned long 的 alias:

typedef unsigned long           uintptr_t;
複製代碼

由此可計算出在 AArch64 下這是一個 16B 大小的結構體,注意內存中存儲的是它的指針,也就是 8B,它的存儲邏輯定義在 7DF8 - 7E00 處,它被存儲在棧的 -0x50 處。

__text:0000000100007DF8                 ADRP            X9, #___block_descriptor_52_e5_i8__0l@PAGE
__text:0000000100007DFC                 ADD             X9, X9, #___block_descriptor_52_e5_i8__0l@PAGEOFF
__text:0000000100007E00                 STR             X9, [SP,#0x60+var_40]
複製代碼

Imported Variables

從 7E04 - 7E28 區域是 Block 捕獲的變量存儲邏輯,因爲未聲明 __block,這些值只是簡單的靜態拷貝。

__text:0000000100007E04                 LDUR            W8, [X29,#var_4]
__text:0000000100007E08                 STR             W8, [SP,#0x60+var_38]
__text:0000000100007E0C                 LDUR            W8, [X29,#var_8]
__text:0000000100007E10                 STR             W8, [SP,#0x60+var_34]
__text:0000000100007E14                 LDUR            W8, [X29,#var_C]
__text:0000000100007E18                 STR             W8, [SP,#0x60+var_30]
__text:0000000100007E1C                 LDUR            W8, [X29,#var_10]
__text:0000000100007E20                 STR             W8, [SP,#0x60+var_2C]
__text:0000000100007E24                 LDUR            W8, [X29,#var_14]
__text:0000000100007E28                 STR             W8, [SP,#0x60+var_28]
複製代碼

這段邏輯分別取出了棧上 -0x14 ~ -0x24 區域的變量拷貝到 -0x48 ~ -0x38 區域,結合上文的分析,這是將局部變量 a-e 拷貝到了 Block 的變量捕獲區。

Stack Layout

有了上面的分析,咱們就能夠畫出 Block 在棧上的內存佈局了,其中淺藍色區域即爲 Block Layout 的所有內容。

Block 的傳遞

在 MRC 時代,棧上的 Block 不會自動拷貝到堆,這就意味着在使用 Block 時直接訪問的便是上圖中從 -0x68 ~ -0x34 的內容,在這種狀況下若是在調用 Block 前涉及到了其餘函數調用,Block 的存儲區會被覆蓋從而出錯,所以在 ARC 下在 Block 建立完成後會被當即拷貝到堆區,這段代碼在 7E2C ~ 7E48 區域:

__text:0000000100007E2C                 ADD             X0, SP, #0x60+var_58
__text:0000000100007E30                 BL              _objc_retainBlock
__text:0000000100007E34                 STUR            X0, [X29,#var_20]
__text:0000000100007E38                 LDUR            X0, [X29,#var_20]
__text:0000000100007E3C                 BL              _objc_retainBlock
__text:0000000100007E40                 SUB             X9, X29, #-var_20
__text:0000000100007E44                 MOV             X30, #0
__text:0000000100007E48                 STR             X0, [SP,#0x60+var_60]
複製代碼

在 7E2C 處首先計算了 Block ISA 的地址,X0 = -0x70 + 0x60 - 0x58 = -0x68 = &Block_ISA,隨後以 isa 的地址爲參數調用了 objc_retainBlock 函數:

BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    
id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}
複製代碼

實際調用了 BuiltIn 的函數 _Block_copy 來將整個 Block 複製到堆區,並返回堆區的 Block ISA 地址,將其存儲在棧的 -0x70 區域,並做爲函數的返回值。

綜上所述,Block 傳遞時實際上傳遞的是 Block ISA 的地址,根據 Block ISA 地址向高地址取值便可得到完整的 Block Layout 數據。

Block 的調用

Caller 分析

invokeStackBlock 函數是 Block Caller,咱們先分析下它的實現:

__text:0000000100007EA4                 SUB             SP, SP, #0x30
__text:0000000100007EA8                 STP             X29, X30, [SP,#0x20+var_s0]
__text:0000000100007EAC                 ADD             X29, SP, #0x20
__text:0000000100007EB0                 BL              _simpleBlockOnStack
__text:0000000100007EB4                 MOV             X29, X29
__text:0000000100007EB8                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000100007EBC                 STUR            X0, [X29,#var_8]
__text:0000000100007EC0                 LDUR            X0, [X29,#var_8]
__text:0000000100007EC4                 MOV             X30, X0
__text:0000000100007EC8                 LDR             X0, [X0,#0x10]
__text:0000000100007ECC                 STR             X0, [SP,#0x20+var_10]
__text:0000000100007ED0                 MOV             X0, X30
__text:0000000100007ED4                 LDR             X30, [SP,#0x20+var_10]
__text:0000000100007ED8                 BLR             X30
__text:0000000100007EDC                 SUB             X30, X29, #-var_8
__text:0000000100007EE0                 STR             W0, [SP,#0x20+var_14]
__text:0000000100007EE4                 MOV             X0, X30
__text:0000000100007EE8                 MOV             X30, #0
__text:0000000100007EEC                 MOV             X1, X30
__text:0000000100007EF0                 BL              _objc_storeStrong
__text:0000000100007EF4                 LDP             X29, X30, [SP,#0x20+var_s0]
__text:0000000100007EF8                 ADD             SP, SP, #0x30
__text:0000000100007EFC                 RET
複製代碼

重點看 7EB0 ~ 7ED8 區域,這是從 simpleBlockOnStack 函數返回 Block 並調用的過程:

__text:0000000100007EB0                 BL              _simpleBlockOnStack
__text:0000000100007EB4                 MOV             X29, X29
__text:0000000100007EB8                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000100007EBC                 STUR            X0, [X29,#var_8]
__text:0000000100007EC0                 LDUR            X0, [X29,#var_8]
__text:0000000100007EC4                 MOV             X30, X0
__text:0000000100007EC8                 LDR             X0, [X0,#0x10]
__text:0000000100007ECC                 STR             X0, [SP,#0x20+var_10]
__text:0000000100007ED0                 MOV             X0, X30
__text:0000000100007ED4                 LDR             X30, [SP,#0x20+var_10]
__text:0000000100007ED8                 BLR             X30
複製代碼

simpleBlockOnStack 返回的是堆區 Block 的 ISA 地址,在 7EC8 處,X0 = ISA + 0x10,根據上面的分析,ISA + 0x10 指向的是 Block Invoker,隨後它被賦給 X30 做爲 BLR 的參數,實現對 Block Invoker 的調用,注意 7EC4 和 7ED0 兩句,前者先備份了 X0 = ISA 的值,隨後還原,所以 Block Invoker 的入參是 Block ISA 的地址,這是爲了可以在實現中取出 Block 信息,例如捕獲的變量。

Callee 分析

下面咱們分析 ___simpleBlockOnStack_block_invoke 的實現,它指向的代碼以下:

__text:0000000100007E68                 SUB             SP, SP, #0x10
__text:0000000100007E6C                 STR             X0, [SP,#0x10+var_8]
__text:0000000100007E70                 MOV             X8, X0
__text:0000000100007E74                 STR             X8, [SP,#0x10+var_10]
__text:0000000100007E78                 LDR             W9, [X0,#0x20]
__text:0000000100007E7C                 LDR             W10, [X0,#0x24]
__text:0000000100007E80                 ADD             W9, W9, W10
__text:0000000100007E84                 LDR             W10, [X0,#0x28]
__text:0000000100007E88                 ADD             W9, W9, W10
__text:0000000100007E8C                 LDR             W10, [X0,#0x2C]
__text:0000000100007E90                 ADD             W9, W9, W10
__text:0000000100007E94                 LDR             W10, [X0,#0x30]
__text:0000000100007E98                 ADD             W0, W9, W10
__text:0000000100007E9C                 ADD             SP, SP, #0x10
__text:0000000100007EA0                 RET
複製代碼

根據上面的分析,這裏的 X0 = &Block_ISA,看一下 7E78 ~ 7E80 的代碼,它從 X0 + 0x20 和 X0 + 0x24 處取出值相加,回到上文 Block Layout 的圖中查看,從 ISA 開始向上偏移 0x20 和 0x24,分別是被捕獲的 a、b 的地址,到這裏 Block Invoker 的實現基本就清晰了:經過傳入 Block ISA 來獲取 Block 信息,其餘邏輯與通常函數一致。

有參 Block

不過這裏依然有個問題,若是 Block 自己就有參數,那麼 ISA 如何傳入呢?下面咱們來作個實驗,在文首的代碼中再加入兩個函數:

typedef int (^CommonBlockWithParams)(int);

CommonBlockWithParams simpleBlockWithParamsOnStack() {
    int a = 1, b = 2, c = 3, d = 4, e = 5;
    int (^theBlock)(int) = ^int (int f) {
        return a + b + c + d + e + f;
    };
    return theBlock;
}

void invokeStackBlockWithParams() {
    CommonBlockWithParams block = simpleBlockWithParamsOnStack();
    block(100);
}
複製代碼

隨後分析一下 invokeStackBlockWithParams 的實現,依然是節選從調用 simpleBlockWithParamsOnStack 獲取 Block 到調用的片斷。

__text:0000000100007E98                 BL              _simpleBlockWithParamsOnStack
__text:0000000100007E9C                 MOV             X29, X29
__text:0000000100007EA0                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000100007EA4                 STUR            X0, [X29,#var_8]
__text:0000000100007EA8                 LDUR            X0, [X29,#var_8]
__text:0000000100007EAC                 MOV             X30, X0
__text:0000000100007EB0                 LDR             X0, [X0,#0x10]
__text:0000000100007EB4                 STR             X0, [SP,#0x20+var_10]
__text:0000000100007EB8                 MOV             X0, X30
__text:0000000100007EBC                 MOV             W1, #0x64
__text:0000000100007EC0                 LDR             X30, [SP,#0x20+var_10]
__text:0000000100007EC4                 BLR             X30
複製代碼

重點看 7EB8 和 7EBC,可見 Block 的 ISA 依然使用 X0 傳遞,而 Block 的入參則是使用了 X1,所以咱們能夠獲得結論,Block 有固定入參 ISA 使用 X0 傳遞,函數的入參從 X1 開始。

Block 的動態捕獲

默認狀況下 Block 採用 Copy 的形式捕獲成員,這使得沒法在 Block Invoker 中修改原變量的值,若要修改,則須要將變量用 __block 修飾,使其拷貝到堆區,這個部分較爲複雜,將在下一篇文章中介紹。

總結

Block 是一種特殊的對象,若是將其類比普通 OC 對象,它只是沒有 SEL,結構與 objc_object 基本一致;其實例在內存中的結構是一個 Block_layout 結構體附加捕獲列表,這相似於普通 OC 對象的 isa + ivar list,在 Block 調用時,Block 的固定入參 X0 = Block ISA,這相似於 OC 方法的固定入參 X0 = self,X1 = SEL,Block 函數的參數從 X1 開始順次存儲。

相關文章
相關標籤/搜索