0x1 序文
iOS では、nil ポインターを使用して OC メソッドを呼び出すことは安全ですが、nil ポインターを使用してブロックを呼び出すと、クラッシュが発生します。この記事では、この現象をコンパイルの観点から説明します。
0x2ブロックの構造
Block の構造は、Block-private.h にあるRuntime のオープン ソース コードObjc4-706にあります。
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 バイトを占有するため、ブロックの基本的なメモリ レイアウトは次のようになります。
0x3 テスト コード
1. 最初に Helper クラスの補助テストを定義します。コードは次のとおりです。
@interface Helper : NSObject
@property (nonatomic, copy) dispatch_block_t block;
@end
@implementation Helper
- (void)triger {}
@end
复制代码
2. テスト ケース 1: 通常のオブジェクト ブロックを呼び出す
- (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 はヘルパー オブジェクトであり、bl 命令はヘルパー オブジェクトのブロック属性の 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
<Helper: 0x282b80a60>
复制代码
シングルステップ ブレークポイント 次の命令、ブレークポイント 0x1001a5d68 <+76>: mov x29, x29
はどこにでもあります。この[helper block]
時点で関数が実行され、ブロックのポインタが返され、x0 = [ヘルパー ブロック] として簡単に理解できるレジスタ x0 に配置されます。 .
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 はブロック ポインターのままです。
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
复制代码
The breakpoint is at 0x1001a5d78 <+92>: blr x8
the instruction. 0x1001a5d74 <+88>: ldr x8, [x0, #0x10]
The pseudocode of this instruction is: x8 = x0 + 0x10, つまり、0x00000001001a8060 = 0x00000001001a8050 + 0x10. アドレス 0x00000001001a8060 でメモリに格納されているアドレスは、ブロックのオブジェクト呼び出しポインタ 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 オブジェクトのメソッドを安全に呼び出すことができます。ブロック呼び出しのように、呼び出し先のインボークポインタを取得するためのレジスタの内容に対するオフセット計算 (メモリが 0 であっても判定が空でない場合) を実行しないため、不正なアドレスがフェッチされて例外が発生します。
0x4 要約
この記事では、上記のいくつかのテスト ケースのアセンブリ コードを分析し、アセンブリ レベルで OC オブジェクト関数とブロック呼び出しの違いを分析します.この違いは、ブロックへの呼び出しが空であると判断する必要があるという事実につながります。安全を確保します。
!block ?: block();
复制代码
なお、多層オブジェクトのブロックを呼び出す際には、空判定も行う必要がある. d オブジェクトとそのブロックが存在しなければならない場合でも、a, b, のいずれかが原因である可能性がある. c オブジェクトは nil であり、テスト ケース 3 のシーンになります。 nil オブジェクトでブロックを呼び出すと、次のようなクラッシュが発生します。
//不安全调用
a.b.c.d.block();
//安全调用
!a.b.c.d.block ?: a.b.c.d.block();
复制代码
この場合、関数のカプセル化のレイヤーをブロックで実行して、過度に長い判断ロジックを回避できます。
//d类
- (void)callBlock {
!self.block ?: self.block();
}
//调用
[a.b.c.d callBlock];
复制代码
全体として、ブロック呼び出しの前に null 判定を実行する必要があります。