谈谈 block Part 1

Block 概述

Block 是 Apple 提供的一种闭包实现,比较方便实现一些函数嵌套实现的功能。Block 分为三种类型:

  • NSConcreteGlobalBlock
  • NSConcreteStackBlock
  • NSConcreteMallocBlock

这三种类型分别对应了三种不同的 Block 类型,值得注意的是,在启用了 ARC 之后,NSConcreteStackBlock 会转换类型为 NSConcreteMallocBlock。

换种比较容易的理解的说法:

  1. 在非 ARC 下,LLVM 编译下没有访问局部变量的 Block 应该是 NSConcreteGlobalBlock 类型的,访问了局部变量的 Block 是 NSConcreteStackBlock 类型的。

  2. 在 ARC 下,访问了局部变量的 Block 是 NSConcreteMallocBlock 类型的,未访问局部变量的 Block 是 NSConcreteGlobalBlock 类型的。

具体的实现代码可以参考 llvm 的 BlockRuntime

对于研究 block 的具体代码翻译,则可以使用 clang 的 rewrite-objc 功能,将 OC 文件转换成 cpp 文件。比如:

1
clang -rewrite-objc blocktest.c

这样就可以生成对应的 blocktest.cpp 文件。

从源码看 NSConcreteGlobalBlock

首先先看一下 NSConcreteGlobalBlock 的代码,从简单的开始:

1
2
3
4
5
6
7

#include <stdio.h>

int main()
{^{printf("Hello World!\n");}();
return 0;
}

翻译之后的代码(精简仅包含所有必须代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("Hello World!\n");}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {0, sizeof(struct __main_block_impl_0)};

int main()
{(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)();
return 0;
}

主要的 Block 代码被翻译成为了一个指针函数调用,__main_block_impl_0 是一个定义过的结构,每个 Block 的类型是固定的。其中 isa 是指明的 Block 类型,FuncPtr 则是函数指针。值得注意的是,虽然 impl.isa 填写的是 NSConcreteStackBlock,但是实际在编译过程中,这里还是会被处理成为 NSConcreteGlobalBlock。

从反汇编看 NSConcreteGlobalBlock

(x86_64):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

__text:0000000100000EC0 _main proc near
__text:0000000100000EC0
__text:0000000100000EC0 var_8 = dword ptr -8
__text:0000000100000EC0 var_4 = dword ptr -4
__text:0000000100000EC0
__text:0000000100000EC0 push rbp
__text:0000000100000EC1 mov rbp, rsp
__text:0000000100000EC4 sub rsp, 10h
__text:0000000100000EC8 mov eax, 0
__text:0000000100000ECD lea rcx, ___block_literal_global
__text:0000000100000ED4 mov [rbp+var_4], 0
__text:0000000100000EDB mov rdi, rcx
__text:0000000100000EDE mov [rbp+var_8], eax
__text:0000000100000EE1 call cs:off_100001050
__text:0000000100000EE7 mov eax, [rbp+var_8]
__text:0000000100000EEA add rsp, 10h
__text:0000000100000EEE pop rbp
__text:0000000100000EEF retn
__text:0000000100000EEF _main endp

(ARM):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

__text:00002F70 PUSH {R7,LR}
__text:00002F72 MOV R7, SP
__text:00002F74 SUB SP, SP, #8
__text:00002F76 MOVS R0, #0
__text:00002F7C MOV R1, #(___block_literal_global - 0x2F88) ; ___block_literal_global
__text:00002F84 ADD R1, PC ; ___block_literal_global
__text:00002F86 MOV R2, R1
__text:00002F88 STR R0, [SP,#0x10+var_C]
__text:00002F8A LDR R1, [R1,#(off_3028 - 0x301C)]
__text:00002F8C STR R0, [SP,#0x10+var_10]
__text:00002F8E MOV R0, R2
__text:00002F90 BLX R1 ; ___main_block_invoke
__text:00002F92 LDR R0, [SP,#0x10+var_10]
__text:00002F94 ADD SP, SP, #8
__text:00002F96 POP {R7,PC}

会发现在调用 block 时是采用的直接调用的方式 (call/blx),由于 NSConcreteGlobalBlock 没有传入参数,因此这个也就是关键在参数处理方式上。

在 X64 平台上,off_100001050 中保存的就是 main_block_invoke(也就是我们使用的 block)的地址,而在 ARM 平台上,R1(main_block_invoke)地址是在 LDR R1, [R1,#(off_3028 - 0x301C)] 这一句赋值而来,其中 off_3028 指向的就是 main_block_invoke 的地址。

<未完待续>