iOS彙編教程(五)Objc Block 的記憶體佈局和彙編表示
系列文章
- iOS彙編入門教程(一)ARM64彙編基礎
- iOS彙編入門教程(二)在Xcode工程中嵌入彙編程式碼
- iOS彙編入門教程(三)彙編中的 Section 與資料存取
- iOS彙編教程(四)基於 LLDB 動態除錯快速分析系統函式的實現
前言
在 Objc 中,Block 是一個特殊的物件,它的例項並非是常規的物件結構,而是以 Block_layout
結構體的形式存在。在宣告時,Block 的結構體會以值型別的形式直接儲存在棧上,隨後會被 copy 到堆上,成為一個特殊的物件,學習 Block 的底層原理一方面能夠掌握複雜值型別的儲存和傳遞方式,另一方面也能在逆向分析遇到 Block 時快速定位與分析相關邏輯。
Block 的結構
Block 的結構可以在 Runtime 的開原始碼 Objc4-706 中找到,它位於 Block-private.h
中:
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
struct objc_object {
private:
isa_t isa; // union contains Class
// ivar instances
}
複製程式碼
可以發現 Block 和常規物件有異曲同工之妙,都是通過 isa 指向的類物件記錄基本資訊,區別在於 Block 物件後面跟的是捕獲的變數列表,而常規物件後面跟的是 ivar 例項列表。
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 的建立、傳遞和呼叫過程。
注意,var_XY
的值是 -0xXY,var_s0
的值是 0。
Block 的建立過程
下面是 _simpleBlockOnStack 符號的反彙編結果:
__text:0000000100007D9C SUB SP,SP,#0x70
__text:0000000100007DA0 STP X29,X30,[SP,#0x60+var_s0]
__text:0000000100007DA4 ADD X29,#0x60
__text:0000000100007DA8 MOV W8,#1
__text:0000000100007DAC STUR W8,[X29,#var_4]
__text:0000000100007DB0 MOV W8,#2
__text:0000000100007DB4 STUR W8,#var_8]
__text:0000000100007DB8 MOV W8,#3
__text:0000000100007DBC STUR W8,#var_C]
__text:0000000100007DC0 MOV W8,#4
__text:0000000100007DC4 STUR W8,#var_10]
__text:0000000100007DC8 MOV W8,#5
__text:0000000100007DCC STUR W8,#var_14]
__text:0000000100007DD0 ADRP X9,#__NSConcreteStackBlock_ptr@PAGE
__text:0000000100007DD4 LDR X9,[X9,#__NSConcreteStackBlock_ptr@PAGEOFF]
__text:0000000100007DD8 STR X9,#0x60+var_58]
__text:0000000100007DDC MOV W8,#0xC0000000
__text:0000000100007DE0 STR W8,#0x60+var_50]
__text:0000000100007DE4 MOV W8,#0
__text:0000000100007DE8 STR W8,#0x60+var_4C]
__text:0000000100007DEC ADRP X9,#___simpleBlockOnStack_block_invoke@PAGE
__text:0000000100007DF0 ADD X9,X9,#___simpleBlockOnStack_block_invoke@PAGEOFF
__text:0000000100007DF4 STR X9,#0x60+var_48]
__text:0000000100007DF8 ADRP X9,#___block_descriptor_52_e5_i8__0l@PAGE
__text:0000000100007DFC ADD X9,#___block_descriptor_52_e5_i8__0l@PAGEOFF
__text:0000000100007E00 STR X9,#0x60+var_40]
__text:0000000100007E04 LDUR W8,#var_4]
__text:0000000100007E08 STR W8,#0x60+var_38]
__text:0000000100007E0C LDUR W8,#var_8]
__text:0000000100007E10 STR W8,#0x60+var_34]
__text:0000000100007E14 LDUR W8,#var_C]
__text:0000000100007E18 STR W8,#0x60+var_30]
__text:0000000100007E1C LDUR W8,#var_10]
__text:0000000100007E20 STR W8,#0x60+var_2C]
__text:0000000100007E24 LDUR W8,#var_14]
__text:0000000100007E28 STR W8,#0x60+var_28]
__text:0000000100007E2C ADD X0,#0x60+var_58
__text:0000000100007E30 BL _objc_retainBlock
__text:0000000100007E34 STUR X0,#var_20]
__text:0000000100007E38 LDUR X0,#var_20]
__text:0000000100007E3C BL _objc_retainBlock
__text:0000000100007E40 SUB X9,X29,#-var_20
__text:0000000100007E44 MOV X30,#0
__text:0000000100007E48 STR X0,#0x60+var_60]
__text:0000000100007E4C MOV X0,X9
__text:0000000100007E50 MOV X1,X30
__text:0000000100007E54 BL _objc_storeStrong
__text:0000000100007E58 LDR X0,#0x60+var_60]
__text:0000000100007E5C LDP X29,#0x60+var_s0]
__text:0000000100007E60 ADD SP,#0x70
__text:0000000100007E64 B _objc_autoreleaseReturnValue
複製程式碼
顯然,從 7DA8 到 7DCC 的部分是對函式 simpleBlockOnStack 開頭的五個 int 變數 a-e 的定義,以當前棧幀的起始地址為零點(後面討論棧上地址時都以此為前提),變數 a-e 分別被儲存在棧的 -0x14 ~ -0x24 區域,
Block ISA
接下來 7DD0 - 7DD4 的程式碼取出的 __NSConcreteStackBlock_ptr
是指向 __NSConcreteStackBlock
的指標,而 NSConcreteStackBlock 就是 Block 的 isa 資料。
__text:0000000100007DD0 ADRP X9,#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,#0x60+var_4C]
複製程式碼
Block Invoker
接下來 3 句是 Block Invoker 的儲存邏輯,Block Invoker 就是 Block 的邏輯的函式指標,它被儲存在了棧的 -0x58 區域。
__text:0000000100007DEC ADRP X9,#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,#0x60+var_40]
複製程式碼
Imported Variables
從 7E04 - 7E28 區域是 Block 捕獲的變數儲存邏輯,由於未宣告 __block
,這些值只是簡單的靜態拷貝。
__text:0000000100007E04 LDUR W8,#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,#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,#0x30
__text:0000000100007EA8 STP X29,#0x20+var_s0]
__text:0000000100007EAC ADD X29,#0x20
__text:0000000100007EB0 BL _simpleBlockOnStack
__text:0000000100007EB4 MOV X29,X29
__text:0000000100007EB8 BL _objc_retainAutoreleasedReturnValue
__text:0000000100007EBC STUR X0,#var_8]
__text:0000000100007EC0 LDUR X0,#var_8]
__text:0000000100007EC4 MOV X30,X0
__text:0000000100007EC8 LDR X0,[X0,#0x10]
__text:0000000100007ECC STR X0,#0x20+var_10]
__text:0000000100007ED0 MOV X0,X30
__text:0000000100007ED4 LDR X30,#0x20+var_10]
__text:0000000100007ED8 BLR X30
__text:0000000100007EDC SUB X30,#-var_8
__text:0000000100007EE0 STR W0,#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,#0x20+var_s0]
__text:0000000100007EF8 ADD SP,#0x30
__text:0000000100007EFC RET
複製程式碼
重點看 7EB0 ~ 7ED8 區域,這是從 simpleBlockOnStack 函式返回 Block 並呼叫的過程:
__text:0000000100007EB0 BL _simpleBlockOnStack
__text:0000000100007EB4 MOV X29,#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,#0x10
__text:0000000100007E6C STR X0,#0x10+var_8]
__text:0000000100007E70 MOV X8,X0
__text:0000000100007E74 STR X8,#0x10+var_10]
__text:0000000100007E78 LDR W9,#0x20]
__text:0000000100007E7C LDR W10,#0x24]
__text:0000000100007E80 ADD W9,W9,W10
__text:0000000100007E84 LDR W10,#0x28]
__text:0000000100007E88 ADD W9,W10
__text:0000000100007E8C LDR W10,#0x2C]
__text:0000000100007E90 ADD W9,W10
__text:0000000100007E94 LDR W10,#0x30]
__text:0000000100007E98 ADD W0,W10
__text:0000000100007E9C ADD 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,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,#var_8]
__text:0000000100007EA8 LDUR X0,#var_8]
__text:0000000100007EAC MOV X30,X0
__text:0000000100007EB0 LDR X0,#0x10]
__text:0000000100007EB4 STR X0,#0x20+var_10]
__text:0000000100007EB8 MOV X0,X30
__text:0000000100007EBC MOV W1,#0x64
__text:0000000100007EC0 LDR X30,#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 開始順次儲存。