谈谈 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 文件。比如:

clang -rewrite-objc blocktest.c

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

从源码看 NSConcreteGlobalBlock

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


    #include <stdio.h>

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

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


	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):


	__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):


	__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 的地址。

<未完待续>

Built with Hugo
主题 StackJimmy 设计