iOS 차단 호출이 비어 있는 것으로 판단되어야 하는 이유는 무엇입니까?

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바이트를 차지하므로 블록의 기본 메모리 레이아웃은 다음과 같습니다.

발자국.jpg

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 함수의 입구에 중단점을 넣습니다.

중단점.png

Debug그런 다음 Xcode 메뉴 표시 줄에서 ->를 찾아 Debug Workflow확인하십시오.Always Show Disassembly

분해.png

그림과 같이 코드를 실행하고 중단점을 트리거한 다음 자동으로 Xcode 어셈블리에 들어갑니다.

asm.png

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 명령은 도우미 개체의 블록 속성, 즉 [helper block]함수의 get 메서드를 호출합니다.

->  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_retainAutoreleasedReturnValuex0은 여전히 ​​블록 포인터입니다.

    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에 저장된 주소는 블록 objectd9c의 호출 포인터 0x00000001001a5입니다.

    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,如下图:

crash.png

从这个用例中可以得出结论,当对象的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_fx0이 nil인지 여부를 판단하고 nil이면 레지스터를 지우고 메시지를 보낼 때 nil을 반환합니다. 따라서 nil 객체에서 메서드를 호출하는 것이 안전합니다. 호출에 대한 호출 포인터를 얻기 위한 블록 호출과 같이 레지스터의 내용에 대해 오프셋 계산을 수행하지 않습니다(메모리가 0인 경우에도 비어 있는 판단은 없음). 그러면 잘못된 주소를 가져오고 예외가 트리거됩니다.

objc_msgSend.png

0x4 요약

본 글은 위의 여러 테스트 케이스의 어셈블리 코드를 분석하여 어셈블리 레벨에서 OC 객체 함수와 블록 호출의 차이점을 분석하였다. 안전을 확보하십시오.

!block ?: block();
复制代码

다층 객체의 블록을 호출할 때 null판단도 필요하다는 점에 유의할 필요가 있는데, 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 판단을 수행해야 합니다.

추천

출처juejin.im/post/7213335620127031354