0x1 Vorwort
In iOS ist es sicher, OC-Methoden mit einem Nullzeiger aufzurufen, aber das Aufrufen eines Blocks mit einem Nullzeiger führt zu einem Absturz. Dieser Artikel wird dieses Phänomen aus der Perspektive der Kompilierung erläutern.
Die Struktur des 0x2-Blocks
Die Struktur von Block findet sich im Open-Source-Code Objc4-706 von Runtime , der sich in Block-private.h befindet:
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
};
复制代码
In arm64 belegt ein Zeiger 8 Bytes und int32_t 4 Bytes, sodass das grundlegende Speicherlayout eines Blocks wie folgt ist:
0x3 Testcode
1. Definieren Sie zuerst den Hilfstest der Helper-Klasse. Der Code lautet wie folgt:
@interface Helper : NSObject
@property (nonatomic, copy) dispatch_block_t block;
@end
@implementation Helper
- (void)triger {}
@end
复制代码
2. Testfall 1: Aufruf eines normalen Objektblocks
- (void)testBlock {
Helper *helper = [Helper new];
helper.block = ^{
NSLog(@"test");
};
helper.block();
}
复制代码
Setzen Sie einen Haltepunkt am Eingang der testBlock-Funktion
Dann suchen Debug
-> in der Xcode-Menüleiste Debug Workflow
prüfenAlways Show Disassembly
Führen Sie den Code aus, lösen Sie den Haltepunkt aus und geben Sie automatisch die Xcode-Assembly ein, wie in der Abbildung gezeigt.
3. Analysebaugruppe
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
复制代码
Machen Sie einen Haltepunkt an 0x1001a5d64 <+72>: bl 0x1001a6460 ; objc_msgSend$block
der Anweisung, zu diesem Zeitpunkt ist x0 das Hilfsobjekt, und die bl-Anweisung ruft die get-Methode des Blockattributs des Hilfsobjekts auf, dh [helper block]
die Funktion.
-> 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>
复制代码
Einzelschritt-Haltepunkt Die nächste Anweisung, der Haltepunkt 0x1001a5d68 <+76>: mov x29, x29
ist überall [helper block]
, zu diesem Zeitpunkt wird die Funktion ausgeführt, der Zeiger des Blocks zurückgegeben und in das Register x0 gestellt, was einfach als x0 = [Hilfsblock] verstanden werden kann. .
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
复制代码
Der Haltepunkt wird an 0x1001a5d70 <+84>: str x0, [sp, #0x8]
der Anweisung gesetzt, und nach Ausführung der Funktion objc_retainAutoreleasedReturnValue
ist x0 immer noch der Blockzeiger.
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
复制代码
Der Haltepunkt befindet sich an 0x1001a5d78 <+92>: blr x8
der Anweisung.Der 0x1001a5d74 <+88>: ldr x8, [x0, #0x10]
Pseudocode dieser Anweisung lautet: x8 = x0 + 0x10, d. h. 0x00000001001a8060 = 0x00000001001a8050 + 0x10. Die im Speicher unter der Adresse 0x00000001001a8060 gespeicherte Adresse ist der Aufrufzeiger 0x00000001001a5d9 des Blockobjekts c.
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
复制代码
Gemäß objc_msgSend
der Implementierung cbz r0, LNilReceiver_f
beurteilt die Anweisung zuerst, ob x0 gleich Null ist, wenn es gleich Null ist, löscht sie das Register und gibt Null zurück, wenn die Nachricht gesendet wird. Daher ist es sicher, Methoden für Null-Objekte aufzurufen. Es führt keine Offset-Berechnung für den Inhalt des Registers durch (auch wenn der Speicher 0 ist, kein Urteil ist leer) wie ein Blockaufruf, um den aufzurufenden Aufrufzeiger zu erhalten, was zum Abrufen einer illegalen Adresse und zum Auslösen einer Ausnahme führen würde.
0x4 Zusammenfassung
Dieser Artikel analysiert den Assemblercode der oben genannten mehreren Testfälle und analysiert den Unterschied zwischen der OC-Objektfunktion und dem Blockaufruf auf Assemblyebene.Dieser Unterschied führt dazu, dass der Aufruf des Blocks als leer beurteilt werden muss Gewährleistung der Sicherheit.
!block ?: block();
复制代码
Es ist erwähnenswert, dass beim Aufrufen des Blocks eines mehrschichtigen Objekts auch eine Null-Beurteilung durchgeführt werden muss Selbst wenn das d-Objekt und sein Block existieren müssen, kann dies daran liegen, dass eines der a, b und c-Objekte ist nil, was zur Szene von Testfall 3 führt. Der Aufruf eines Blocks mit einem nil-Objekt verursacht einen Absturz, wie zum Beispiel:
//不安全调用
a.b.c.d.block();
//安全调用
!a.b.c.d.block ?: a.b.c.d.block();
复制代码
In diesem Fall kann eine Funktionskapselungsschicht auf dem Block ausgeführt werden, um eine übermäßig lange Beurteilungslogik zu vermeiden.
//d类
- (void)callBlock {
!self.block ?: self.block();
}
//调用
[a.b.c.d callBlock];
复制代码
Insgesamt muss vor dem Blockaufruf eine Nullentscheidung durchgeführt werden.