iOS block呼叫為啥要判空
0x1 前言
在iOS中,使用nil指標呼叫OC的方法是安全的,但是使用nil指標呼叫block卻會產生崩潰。本篇文章,將會從彙編的角度解釋該現象。
0x2 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 };
```
在arm64中,一個指標佔8位元組,int32_t佔4個位元組,所以一個block的記憶體基本佈局如下圖:
0x3 測試程式碼
1.首先定義Helper類輔助測試,程式碼如下:
``` @interface Helper : NSObject
@property (nonatomic, copy) dispatch_block_t block;
@end
@implementation Helper
- (void)triger {}
@end ```
2.測試用例1:呼叫一個正常物件的block
- (void)testBlock {
Helper *helper = [Helper new];
helper.block = ^{
NSLog(@"test");
};
helper.block();
}
在testBlock函式入口處打上斷點
然後在Xcode選單欄找到Debug
-> Debug Workflow
,勾選Always Show Disassembly
執行程式碼,觸發斷點,會自動進入Xcode彙編,如圖所示。
3.分析彙編
TestBlock`-[ViewController testBlock]:
0x1001a5d1c <+0>: sub sp, sp, #0x40
0x1001a5d20 <+4>: stp x29, x30, [sp, #0x30]
0x1001a5d24 <+8>: add x29, sp, #0x30
0x1001a5d28 <+12>: stur x0, [x29, #-0x8]
0x1001a5d2c <+16>: stur x1, [x29, #-0x10]
0x1001a5d30 <+20>: adrp x8, 8
-> 0x1001a5d34 <+24>: ldr x0, [x8, #0x428]
0x1001a5d38 <+28>: bl 0x1001a634c ; symbol stub for: objc_opt_new
0x1001a5d3c <+32>: ldr x1, [sp]
0x1001a5d40 <+36>: add x8, sp, #0x18
0x1001a5d44 <+40>: str x8, [sp, #0x10]
0x1001a5d48 <+44>: str x0, [sp, #0x18]
0x1001a5d4c <+48>: ldr x0, [sp, #0x18]
0x1001a5d50 <+52>: adrp x2, 3
0x1001a5d54 <+56>: add x2, x2, #0x50 ; __block_literal_global.13
0x1001a5d58 <+60>: bl 0x1001a64c0 ; objc_msgSend$setBlock:
0x1001a5d5c <+64>: ldr x1, [sp]
0x1001a5d60 <+68>: ldr x0, [sp, #0x18]
0x1001a5d64 <+72>: bl 0x1001a6460 ; objc_msgSend$block
0x1001a5d68 <+76>: mov x29, x29
0x1001a5d6c <+80>: bl 0x1001a6364 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x1001a5d70 <+84>: str x0, [sp, #0x8]
0x1001a5d74 <+88>: ldr x8, [x0, #0x10]
0x1001a5d78 <+92>: blr x8
0x1001a5d7c <+96>: ldr x0, [sp, #0x8]
0x1001a5d80 <+100>: bl 0x1001a6358 ; symbol stub for: objc_release
0x1001a5d84 <+104>: ldr x0, [sp, #0x10]
0x1001a5d88 <+108>: mov x1, #0x0
0x1001a5d8c <+112>: bl 0x1001a6388 ; symbol stub for: objc_storeStrong
0x1001a5d90 <+116>: ldp x29, x30, [sp, #0x30]
0x1001a5d94 <+120>: add sp, sp, #0x40
0x1001a5d98 <+124>: ret
打個斷點在0x1001a5d64 <+72>: bl 0x1001a6460 ; objc_msgSend$block
指令處,此時x0為helper物件,bl指令呼叫的是helper物件的block屬性的get方法,即[helper block]
函式。
```
-> 0x1001a5d64 <+72>: bl 0x1001a6460 ; objc_msgSend$block
0x1001a5d68 <+76>: mov x29, x29
0x1001a5d6c <+80>: bl 0x1001a6364 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x1001a5d70 <+84>: str x0, [sp, #0x8]
0x1001a5d74 <+88>: ldr x8, [x0, #0x10]
0x1001a5d78 <+92>: blr x8
(lldb) register read x0
x0 = 0x0000000282b80a60
(lldb) po 0x0000000282b80a60
```
單步斷點下一個指令,斷點到0x1001a5d68 <+76>: mov x29, x29
處,此時執行完[helper block]
函式,返回了block的指標,放於暫存器x0中,可以簡單的理解為x0 = [helper block]。
```
0x1001a5d64 <+72>: bl 0x1001a6460 ; objc_msgSend$block
-> 0x1001a5d68 <+76>: mov x29, x29
0x1001a5d6c <+80>: bl 0x1001a6364 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x1001a5d70 <+84>: str x0, [sp, #0x8]
0x1001a5d74 <+88>: ldr x8, [x0, #0x10]
0x1001a5d78 <+92>: blr x8
(lldb) register read x0 x0 = 0x00000001001a8050 TestBlock`__block_literal_global.13 ```
斷點打在0x1001a5d70 <+84>: str x0, [sp, #0x8]
指令處,執行完objc_retainAutoreleasedReturnValue
函式後,x0仍然是block指標。
``` 0x1001a5d64 <+72>: bl 0x1001a6460 ; objc_msgSend$block 0x1001a5d68 <+76>: mov x29, x29 0x1001a5d6c <+80>: bl 0x1001a6364 ; symbol stub for: objc_retainAutoreleasedReturnValue -> 0x1001a5d70 <+84>: str x0, [sp, #0x8] 0x1001a5d74 <+88>: ldr x8, [x0, #0x10] 0x1001a5d78 <+92>: blr x8
(lldb) register read x0 x0 = 0x00000001001a8050 TestBlock`__block_literal_global.13 ```
斷點打在0x1001a5d78 <+92>: blr x8
指令處,0x1001a5d74 <+88>: ldr x8, [x0, #0x10]
這句指令的虛擬碼:x8 = x0 + 0x10, 即 0x00000001001a8060 = 0x00000001001a8050 + 0x10,在地址0x00000001001a8060處記憶體存放的地址就是block物件的invoke指標0x00000001001a5d9c。
```
0x1001a5d64 <+72>: bl 0x1001a6460 ; objc_msgSend$block
0x1001a5d68 <+76>: mov x29, x29
0x1001a5d6c <+80>: bl 0x1001a6364 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x1001a5d70 <+84>: str x0, [sp, #0x8]
0x1001a5d74 <+88>: ldr x8, [x0, #0x10]
-> 0x1001a5d78 <+92>: blr x8
(lldb) register read x0
x0 = 0x00000001001a8050 TestBlock__block_literal_global.13
(lldb) memory read 0x00000001001a8060
0x1001a8060: 9c 5d 1a 00 01 00 00 00 10 80 1a 00 01 00 00 00 .]..............
0x1001a8070: b8 2f 97 f6 01 00 00 00 c8 07 00 00 00 00 00 00 ./..............
(lldb) register read x8
x8 = 0x00000001001a5d9c TestBlock
__27-[ViewController testBlock]_block_invoke at ViewController.m:71
``
根據block的記憶體佈局圖可以知道在block的isa + 0x10處的記憶體就是block的invoke指標地址。指令
0x1001a5d78 <+92>: blr x8是呼叫block的invoke指標進行函式呼叫,即呼叫的是
helper block`,執行block的呼叫。這是一個正常oc物件的block的調用匯編分析,現在來看一下下面兩種測試用例。
4.測試用例2:呼叫一個物件的nil block,重複2步驟,進入Xcode彙編
- (void)testBlockNilBlock {
Helper *helper = [Helper new];
helper.block();
}
將斷點打到0x100d09c14 <+52>: bl 0x100d0a460 ; objc_msgSend$block
指令處,獲取block指標的指令呼叫之後。檢視此時的x0,發現獲取的值為0,也就是nil,取到一個為nil的block指標。
``
TestBlock
-[ViewController testBlockNilBlock]:
0x100d09be0 <+0>: sub sp, sp, #0x40
0x100d09be4 <+4>: stp x29, x30, [sp, #0x30]
0x100d09be8 <+8>: add x29, sp, #0x30
0x100d09bec <+12>: stur x0, [x29, #-0x8]
0x100d09bf0 <+16>: stur x1, [x29, #-0x10]
0x100d09bf4 <+20>: adrp x8, 8
0x100d09bf8 <+24>: ldr x0, [x8, #0x428]
0x100d09bfc <+28>: bl 0x100d0a34c ; symbol stub for: objc_opt_new
0x100d09c00 <+32>: ldr x1, [sp]
0x100d09c04 <+36>: add x8, sp, #0x18
0x100d09c08 <+40>: str x8, [sp, #0x10]
0x100d09c0c <+44>: str x0, [sp, #0x18]
0x100d09c10 <+48>: ldr x0, [sp, #0x18]
0x100d09c14 <+52>: bl 0x100d0a460 ; objc_msgSend$block
-> 0x100d09c18 <+56>: mov x29, x29
0x100d09c1c <+60>: bl 0x100d0a364 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x100d09c20 <+64>: str x0, [sp, #0x8]
0x100d09c24 <+68>: ldr x8, [x0, #0x10]
0x100d09c28 <+72>: blr x8
0x100d09c2c <+76>: ldr x0, [sp, #0x8]
0x100d09c30 <+80>: bl 0x100d0a358 ; symbol stub for: objc_release
0x100d09c34 <+84>: ldr x0, [sp, #0x10]
0x100d09c38 <+88>: mov x1, #0x0
0x100d09c3c <+92>: bl 0x100d0a388 ; symbol stub for: objc_storeStrong
0x100d09c40 <+96>: ldp x29, x30, [sp, #0x30]
0x100d09c44 <+100>: add sp, sp, #0x40
0x100d09c48 <+104>: ret
(lldb) register read x0
x0 = 0x0000000000000000
將斷點打在`0x100d09c24 <+68>: ldr x8, [x0, #0x10]`指令處,該指令等價於x8 = x0 + 0x10,由於此時x0為0x0000000000000000,所以 0x0000000000000010 = 0x0000000000000000 + 0x10,該地址0x0000000000000010為非法地址,所以會觸發非法地址異常。
0x100d09c14 <+52>: bl 0x100d0a460 ; objc_msgSend$block
0x100d09c18 <+56>: mov x29, x29
0x100d09c1c <+60>: bl 0x100d0a364 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x100d09c20 <+64>: str x0, [sp, #0x8]
-> 0x100d09c24 <+68>: ldr x8, [x0, #0x10]
0x100d09c28 <+72>: blr x8
```
放開斷點,繼續執行,觸發EXC_BAD_ACCESS
異常,異常資訊中address=0x10,如下圖:
從這個用例中可以得出結論,當物件的block為nil時,在彙編層,仍然會按照正常的block呼叫邏輯去取block的invoke指標去執行,當暫存器進行計算獲取invoke指標時,由於block為nil,暫存器計算出的地址為0x10,觸發非法地址異常。
5.測試用例3:呼叫一個nil物件的block,重複2步驟,進入Xcode彙編
- (void)testBlockNilObj {
Helper *helper = nil;
helper.block();
}
TestBlock`-[ViewController testBlockNilObj]:
0x1025a5b78 <+0>: sub sp, sp, #0x40
0x1025a5b7c <+4>: stp x29, x30, [sp, #0x30]
0x1025a5b80 <+8>: add x29, sp, #0x30
0x1025a5b84 <+12>: mov x8, x1
0x1025a5b88 <+16>: stur x0, [x29, #-0x8]
0x1025a5b8c <+20>: stur x8, [x29, #-0x10]
0x1025a5b90 <+24>: add x8, sp, #0x18
0x1025a5b94 <+28>: str x8, [sp, #0x8]
0x1025a5b98 <+32>: mov x8, #0x0
0x1025a5b9c <+36>: str x8, [sp, #0x10]
-> 0x1025a5ba0 <+40>: str xzr, [sp, #0x18]
0x1025a5ba4 <+44>: ldr x0, [sp, #0x18]
0x1025a5ba8 <+48>: bl 0x1025a6460 ; objc_msgSend$block
0x1025a5bac <+52>: mov x29, x29
0x1025a5bb0 <+56>: bl 0x1025a6364 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x1025a5bb4 <+60>: str x0, [sp]
0x1025a5bb8 <+64>: ldr x8, [x0, #0x10]
0x1025a5bbc <+68>: blr x8
0x1025a5bc0 <+72>: ldr x0, [sp]
0x1025a5bc4 <+76>: bl 0x1025a6358 ; symbol stub for: objc_release
0x1025a5bc8 <+80>: ldr x0, [sp, #0x8]
0x1025a5bcc <+84>: ldr x1, [sp, #0x10]
0x1025a5bd0 <+88>: bl 0x1025a6388 ; symbol stub for: objc_storeStrong
0x1025a5bd4 <+92>: ldp x29, x30, [sp, #0x30]
0x1025a5bd8 <+96>: add sp, sp, #0x40
0x1025a5bdc <+100>: ret
對比其獲取block指標到取invoke指標去執行這一過程,與測試用例2並無區別:
0x1025a5ba8 <+48>: bl 0x1025a6460 ; objc_msgSend$block
0x1025a5bac <+52>: mov x29, x29
0x1025a5bb0 <+56>: bl 0x1025a6364 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x1025a5bb4 <+60>: str x0, [sp]
0x1025a5bb8 <+64>: ldr x8, [x0, #0x10]
0x1025a5bbc <+68>: blr x8
所以,不管是呼叫nil物件的block還是正常物件的一個為nil的block指標最終都會觸發到非法地址異常上。
6.測試用例4: 呼叫一個nil物件的函式,重複2步驟,進入Xcode彙編
- (void)test {
Helper *helper = nil;
[helper triger];
}
TestBlock`-[ViewController test]:
0x102635c4c <+0>: sub sp, sp, #0x40
0x102635c50 <+4>: stp x29, x30, [sp, #0x30]
0x102635c54 <+8>: add x29, sp, #0x30
0x102635c58 <+12>: mov x8, x1
0x102635c5c <+16>: stur x0, [x29, #-0x8]
0x102635c60 <+20>: stur x8, [x29, #-0x10]
0x102635c64 <+24>: add x8, sp, #0x18
0x102635c68 <+28>: str x8, [sp, #0x8]
0x102635c6c <+32>: mov x8, #0x0
0x102635c70 <+36>: str x8, [sp, #0x10]
-> 0x102635c74 <+40>: str xzr, [sp, #0x18]
0x102635c78 <+44>: ldr x0, [sp, #0x18]
0x102635c7c <+48>: bl 0x102636500 ; objc_msgSend$triger
0x102635c80 <+52>: ldr x0, [sp, #0x8]
0x102635c84 <+56>: ldr x1, [sp, #0x10]
0x102635c88 <+60>: bl 0x102636388 ; symbol stub for: objc_storeStrong
0x102635c8c <+64>: ldp x29, x30, [sp, #0x30]
0x102635c90 <+68>: add sp, sp, #0x40
0x102635c94 <+72>: ret
對於OC函式呼叫最終都會轉換成objc_msgSend的呼叫
0x102635c7c <+48>: bl 0x102636500 ; objc_msgSend$triger
檢視objc_msgSend
的實現可知,指令cbz r0, LNilReceiver_f
先判斷x0是否為nil,如果為nil,清空暫存器,訊息傳送返回nil。所以對於nil物件的方法呼叫,是安全的。並不會像block呼叫一樣對暫存器中的內容(即使記憶體為0,沒有作判空)進行偏移計算獲取invoke指標進行呼叫,進而導致取到非法地址,觸發異常。
0x4 總結
本篇文章通過分析以上幾個測試用例的彙編程式碼,分析了OC物件函式與block呼叫在彙編層面上的區別,這種區別導致了對於block的呼叫需要進行判空後才能確保安全。
!block ?: block();
值得注意的是,呼叫多層物件的block時,也需要進行判空,即使d物件與其block必然存在,也可能因為a、b、c物件中任意一個為nil,導致出現測試用例3的場景,呼叫一個nil物件的block產生崩潰,比如:
```
//不安全呼叫
a.b.c.d.block();
//安全呼叫
!a.b.c.d.block ?: a.b.c.d.block();
對於這種情況,可以對將該block進行一層函式封裝,可以避免過長的判斷邏輯。
//d類
- (void)callBlock {
!self.block ?: self.block();
}
//呼叫 [a.b.c.d callBlock]; ```
總而言之,block呼叫之前需要進行判空。