搬运自 https://zhuanlan.zhihu.com/p/40663395

最近在微博上看到一个关于 block 的问题,有这么一段代码:

#include <stdio.h>

int main() {
    typedef int(^Block)(void);
    Block blocks[3];
    int i;
    for (i = 0; i < 3; ++i) {
        blocks[i] = ^{
            return i;
        };
    }
    for (i = 0; i < 3; ++i) {
        printf("%d\n", blocks[i]());
    }
}

这段代码在 ARC 和 MRC 模式下分别会输出什么?答案是:ARC 模式下面会输出 012,而 MRC 模式下会输出 222

MRC 模式下的 block

我们先来讨论 MRC 模式下这段代码的行为,关于 block 的实现细节,可以参考这篇文章:Block Implementation Specification。从中我们可以看到编译器会生成一些描述这个 block 的 struct,由于我们引用了栈上的局部变量 i,这个 block 会存在于栈内存里(而不是全局内存里)。其实这 3 个 block 指针都指向栈上面同一片内存,每次循环唯一修改的只是这个 block 里面捕获的变量 i 的副本,循环完后,block 里面保存的副本值为 2,所以 MRC 模式下输出 3 个 2

道理不难,我倒是很好奇编译器到底生成了什么样的代码,带着这个疑惑,我研究了一下上面代码对应的汇编代码,为了看懂这个汇编,我还临时抱佛脚,看了一篇汇编的介绍

首先生成 MRC 模式下的汇编代码:

clang -fno-stack-protector -fno-objc-arc -S blocktest.m -o blocktest_mrc.s

因为主要区别在于 3 个 blocks[i] 赋值,我们重点看这一部分。这一段汇编代码结合上述 block 实现细节那篇文章的 Imported Variables 这一节看有奇效:

根据文章,我们的 block 大概会编译成:

struct __block_literal_tmp {
    void *isa;  // &__NSConcreteStackBlock
    int flags;  // 0xC0000000
    int reserved;  // 0 
    void (*invoke)(struct __block_literal_2 *);
    struct __block_descriptor_2 *descriptor;
    const int i;
};

我摘取了对应的汇编代码分享一下:

...
_main:
  pushq %rbp
  movq  %rsp, %rbp
  subq  $80, %rsp                       ## 栈上留出 80 bytes
  movl  $0, -4(%rbp)
  movl  $0, -36(%rbp)                   ## -36(%rbp) 存的是局部变量 i
LBB0_1:                                 ## 第一个循环
  cmpl  $3, -36(%rbp)
  jge LBB0_4                            ## i >= 3 则跳出循环

  leaq  -72(%rbp), %rax                 ## block 的地址
  leaq  ___block_descriptor_tmp(%rip), %rcx
  leaq  ___main_block_invoke(%rip), %rdx
  movq  __NSConcreteStackBlock@GOTPCREL(%rip), %rsi
  movq  %rsi, -72(%rbp)                              ## 初始化 isa
  movl  $-1073741824, -64(%rbp) ## imm = 0xC0000000  ## 初始化 flags
  movl  $0, -60(%rbp)                   ## 初始化 reserved 
  movq  %rdx, -56(%rbp)                 ## 初始化 invoke
  movq  %rcx, -48(%rbp)                 ## 初始化 descriptor
  movl  -36(%rbp), %edi                 ## 这两行把 i 的值复制给 block 里面的副本
  movl  %edi, -40(%rbp) 
  movslq  -36(%rbp), %rcx               ## %rcx = i
  movq  %rax, -32(%rbp,%rcx,8)          ## 赋值给 blocks[i]

  movl  -36(%rbp), %eax                 ## ++i
  addl  $1, %eax
  movl  %eax, -36(%rbp)
  jmp LBB0_1
LBB0_4:                                 ## 第二个循环
  movl  $0, -36(%rbp)
...

第一个循环结束之后栈内存大概长这个样子:

%rsp       -> |-------------------------------------------|
                                 ...
              |-------------------------------------------|
              |       block[2] = %rax = -72(%rbp)         |-----------------|
              |                                           |                 |
-16(%rsp)  -> |-------------------------------------------|                 |
              |       block[1] = %rax = -72(%rbp)         |-----------------|
              |                                           |                 |
-24(%rsp)  -> |-------------------------------------------|                 |
              |       block[0] = %rax = -72(%rbp)         |-----------------|
              |                                           |                 |
-32(%rsp)  -> |-------------------------------------------|                 |
              |       2 // local int i                    |                 |
-36(%rsp)  -> |-------------------------------------------| ----            |
              |       2 // block const int i              |    |            |
              |-------------------------------------------|    |            |
              |       &___block_descriptor_tmp            |    |            |
              |                                           |    |            |
              |-------------------------------------------|    |            |
              |       &___main_block_invoke               |    |            |
              |                                           |    |            |
              |-------------------------------------------| block_literal   |
              |       0 // reserved                       |    |            |
              |-------------------------------------------|    |            |
              |       0xC0000000 // flags                 |    |            |
              |-------------------------------------------|    |            |
              |       &__NSConcreteStackBlock             |    |            |
              |                                           |    |            |
-72(%rsp)  -> |-------------------------------------------| ----     <<<----|
                                 ...

看汇编代码就很清晰了,基本上 block 这块内存来回被初始化了 3 次,之后 block 里面的副本 i 变成了 2。

ARC 模式下的 block

我们继续看 ARC 模式下的 block,我们先拿到汇编代码:

clang -fno-stack-protector -fobjc-arc -S blocktest.m -o blocktest_arc.s

同样摘取第一个循环部分,重点看两边不同的地方:

...
_main:
  pushq %rbp
  movq  %rsp, %rbp

## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  ## 这里不同的地方在于 ARC 模式下会用 memset 清空 blocks 
  subq  $112, %rsp
  xorl  %esi, %esi
  movl  $24, %eax
  movl  %eax, %edx
  leaq  -32(%rbp), %rcx
  movl  $0, -4(%rbp)
  movq  %rcx, %rdi
  callq _memset
## <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

  movl  $0, -36(%rbp)                   ## -36(%rbp) 存的是局部变量 i
LBB0_1:                                 ## 第一个循环
  cmpl  $3, -36(%rbp)
  jge LBB0_4                            ## i >= 3 则跳出循环

  leaq  -72(%rbp), %rax                 ## block 的地址
  leaq  ___block_descriptor_tmp(%rip), %rcx
  leaq  ___main_block_invoke(%rip), %rdx
  movq  __NSConcreteStackBlock@GOTPCREL(%rip), %rsi
  movq  %rsi, -72(%rbp)                              ## 初始化 isa
  movl  $-1073741824, -64(%rbp) ## imm = 0xC0000000  ## 初始化 flags
  movl  $0, -60(%rbp)                   ## 初始化 reserved 
  movq  %rdx, -56(%rbp)                 ## 初始化 invoke
  movq  %rcx, -48(%rbp)                 ## 初始化 descriptor
  movl  -36(%rbp), %edi                 ## 这两行把 i 的值复制给 block 里面的副本

## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  ## 这里不同的地方是我们初始化 block 之后,会调用 _bojc_retainBlock  block 复制到 heap 离去
  ## blocks[i] 保存的 heap 里面的 block
  movq  %rax, %rdi                      ## _objc_retainBlock(__block_literal)
  callq _objc_retainBlock
  movslq  -36(%rbp), %rcx
  movq  -32(%rbp,%rcx,8), %rdx          ## %rdx = blocks[i]
  movq  %rax, -32(%rbp,%rcx,8)          ## blocks[i] = _objc_retainBlock(__block_literal)
  movq  %rdx, %rdi                      ## _objc_release(%rdx)
  callq _objc_release
## <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

  movl  -36(%rbp), %eax                 ## ++i
  addl  $1, %eax
  movl  %eax, -36(%rbp)
  jmp LBB0_1
LBB0_4:                                 ## 第二个循环
  movl  $0, -36(%rbp)
...

到这里这个问题就算破案了,欲知后事如何,请听下回分解!