(译)窥探Blocks(2)

作者: foolishBoy | 来源:发表于2017-11-02 09:15 被阅读109次

    本文翻译自Matt Galloway的博客

    之前的文章(译)窥探Blocks(1)我们已经了解了block的内部原理,以及编译器如何处理它。本文我将讨论一下非常量的blocks以及它们在栈上的组织方式。

    Block 类型

    第一篇文章中,我们看到block有__NSConcreteGlobalBlock类。block结构体和descriptor都在编译阶段基于已知的变量完全初始化了。block还有一些不同的类型,每一个类型都对应一个相关的类。为了简单起见,我们只考虑其中的三个:

    1. _NSConcreteGlobalBlock是一个全局定义的block,在编译阶段就完成创建工作。这些block没有捕获任何域,比如一个空block。
    2. _NSConcreteStackBlock是一个在栈上的block,这是所有blocks在最终拷贝到堆上之前所开始的地方。
    3. _NSConcreteMallocBlock是一个在堆上的block,这是拷贝一个block后最终的位置。它们在这里被引用计数并且在引用计数变为0时被释放。

    捕获域的block

    现在我们来看看下面一段代码:

    #import <dispatch/dispatch.h>
    
    typedef void(^BlockA)(void);
    void foo(int);
    
    __attribute__((noinline))
    void runBlockA(BlockA block) {
        block();
    }
    
    void doBlockA() {
        int a = 128;
        BlockA block = ^{
            foo(a);
        };
        runBlockA(block);
    }
    

    这里有一个方法foo,因此block捕获了一些东西,用一个捕获到的变量来调用方法。我又看了一下armv7所产生的一小段相关代码:

        .globl  _runBlockA
        .align  2
        .code   16                      @ @runBlockA
        .thumb_func     _runBlockA
    _runBlockA:
        ldr     r1, [r0, #12]
        bx      r1 
    

    首先,runBlockA方法与之前的结果一样,它调用block的invoke方法。然后看看doBlockA

    .globl  _doBlockA
        .align  2
        .code   16                      @ @doBlockA
        .thumb_func     _doBlockA
    _doBlockA:
        push    {r7, lr}
        mov     r7, sp
        sub     sp, #24
        movw    r2, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
        movt    r2, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
        movw    r1, :lower16:(___doBlockA_block_invoke_0-(LPC1_1+4))
    LPC1_0:
        add     r2, pc
        movt    r1, :upper16:(___doBlockA_block_invoke_0-(LPC1_1+4))
        movw    r0, :lower16:(___block_descriptor_tmp-(LPC1_2+4))
    LPC1_1:
        add     r1, pc
        ldr     r2, [r2]
        movt    r0, :upper16:(___block_descriptor_tmp-(LPC1_2+4))
        str     r2, [sp]
        mov.w   r2, #1073741824
        str     r2, [sp, #4]
        movs    r2, #0
    LPC1_2:
        add     r0, pc
        str     r2, [sp, #8]
        str     r1, [sp, #12]
        str     r0, [sp, #16]
        movs    r0, #128
        str     r0, [sp, #20]
        mov     r0, sp
        bl      _runBlockA
        add     sp, #24
        pop     {r7, pc}
    

    这下看起来比之前的复杂多了。与从一个全局符号加载一个block不同,这看起来做了许多工作。看起来可能有点麻烦,但其实也非常简单。我们最好考虑重新整理这些方法,但请相信我这样做不会没有改变任何功能。编译器之所以这样安排它的指令顺序,是为了优化编译性能,减少流水线气泡。重新整理后的方法如下:

    _doBlockA:
            // 1
            push    {r7, lr}
            mov     r7, sp
    
            // 2
            sub     sp, #24
    
            // 3
            movw    r2, :lower16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
            movt    r2, :upper16:(L__NSConcreteStackBlock$non_lazy_ptr-(LPC1_0+4))
    LPC1_0:
            add     r2, pc
            ldr     r2, [r2]
            str     r2, [sp]
    
            // 4
            mov.w   r2, #1073741824
            str     r2, [sp, #4]
    
            // 5
            movs    r2, #0
            str     r2, [sp, #8]
    
            // 6
            movw    r1, :lower16:(___doBlockA_block_invoke_0-(LPC1_1+4))
            movt    r1, :upper16:(___doBlockA_block_invoke_0-(LPC1_1+4))
    LPC1_1:
            add     r1, pc
            str     r1, [sp, #12]
    
            // 7
            movw    r0, :lower16:(___block_descriptor_tmp-(LPC1_2+4))
            movt    r0, :upper16:(___block_descriptor_tmp-(LPC1_2+4))
    LPC1_2:
            add     r0, pc
            str     r0, [sp, #16]
    
            // 8
            movs    r0, #128
            str     r0, [sp, #20]
    
            // 9
            mov     r0, sp
            bl      _runBlockA
    
            // 10
            add     sp, #24
            pop     {r7, pc}
    

    这就是它所做的事:

    1. 方法开始。r7被压入栈,因为它即将被重写,而且作为一个寄存器必须在方法调用时候保存值。lr是一个链接寄存器,也被压入栈,保存了下一个指令的地址,好让方法返回时继续执行下一个指令。可以在方法结尾看到。 栈指针(sp)也被保存在r7中。

    2. 栈指针(sp)减去24,留出24字节的栈空间存储数据。

    3. 这一小块代码正在相对于程序计数器查找L__NSConcreteStackBlock$non_lazy_ptr符号,这样最后链接成功的二进制文件,不管代码结束于任何地方,它都可以正常工作(这句话有点绕,翻译的不好,需要好好理解一下)。这个值最后存储在栈指针指向的位置。

    4. 1073741824存储在sp + 4 的位置上。

    5. 0存储在sp + 8的位置上。现在可能情况比较清晰了。回顾上一篇文章中提到的Block_layout结构体,可以看出一个Block_layout结构体在栈上创建了!目前为止已经有了isa指针,flagsreserved值被设置了。

    6. ___doBlockA_block_invoke_0的地址存储在sp + 12位置。这就是block结构体的invoke参数。

    7. ___block_descriptor_tmp的地址存储在sp + 16位置。这就是block结构体的descriptor参数。

    8. 128存储在sp + 20的位置。啊!如果你回看Block_layout结构体你会发现里面只有5个值。那么存在这个结构体末尾的是什么呢?哈哈,别忘记了,这个128就是在这个block前定义的、被block捕获的值。所以这一定是存储它们使用变量的地方——在Block_layout最后。

    9. sp现在指向一个完全初始化的block结构体,它被放入r0寄存器,然后runBlockA被调用。(记住在ARM EABI中r0包含了方法的第一个参数)

    10. 最后sp + 24 已抵消最开始减去的24。然后分别从栈弹出两个值到r7pc中。r7抵消一开始压栈的操作,pc将获得方法开始时lr里面的值。这样有效地完成了方法返回的操作,让CPU继续(程序计数器pc)从方法返回的地方(链接寄存器lr)执行。

    哇哦!你还在跟着我学?太牛逼啦!

    这一小段的最后一部分是来看看invoke方法和descriptor长什么样。我们希望它们不要与第一篇文章中的全局block差太多。

    .align  2
        .code   16                      @ @__doBlockA_block_invoke_0
        .thumb_func     ___doBlockA_block_invoke_0
    ___doBlockA_block_invoke_0:
        ldr     r0, [r0, #20]
        b.w     _foo
    
        .section        __TEXT,__cstring,cstring_literals
    L_.str:                                 @ @.str
        .asciz   "v4@?0"
    
        .section        __TEXT,__objc_classname,cstring_literals
    L_OBJC_CLASS_NAME_:                     @ @"\01L_OBJC_CLASS_NAME_"
        .asciz   "\001P"
    
        .section        __DATA,__const
        .align  2                       @ @__block_descriptor_tmp
    ___block_descriptor_tmp:
        .long   0                       @ 0x0
        .long   24                      @ 0x18
        .long   L_.str
        .long   L_OBJC_CLASS_NAME_
    

    还真是相差不大。唯一的区别在于block descriptor的size值。现在它是24而不是20。因为block此时捕获了一个整形数值。我们已经看到在创建block结构体时,这额外的4字节被放在了最后。

    同样地,你在实际执行的方法__doBlockA_block_invoke_0中也会发现参数值从结构体末尾处(r0 + 20)读取出来,这就是block捕获的值。

    捕获对象类型的值会怎样?

    下面要考虑的是捕获的不再是一个整形,而是一个对象,比如NSString。欲知详情,请看下面代码:

    #import <dispatch/dispatch.h>
    
    typedef void(^BlockA)(void);
    void foo(NSString*);
    
    __attribute__((noinline))
    void runBlockA(BlockA block) {
        block();
    }
    
    void doBlockA() {
        NSString *a = @"A";
        BlockA block = ^{
            foo(a);
        };
        runBlockA(block);
    }
    

    我不再研究doBlockA的细节,因为变化不大。比较有意思的是它创建的block descriptor结构体。

     .section        __DATA,__const
        .align  4                       @ @__block_descriptor_tmp
    ___block_descriptor_tmp:
        .long   0                       @ 0x0
        .long   24                      @ 0x18
        .long   ___copy_helper_block_
        .long   ___destroy_helper_block_
        .long   L_.str1
        .long   L_OBJC_CLASS_NAME_
    

    注意现在有了名为___copy_helper_block____destroy_helper_block_的函数指针。这里是这些函数的定义:

    .align  2
        .code   16                      @ @__copy_helper_block_
        .thumb_func     ___copy_helper_block_
    ___copy_helper_block_:
        ldr     r1, [r1, #20]
        adds    r0, #20
        movs    r2, #3
        b.w     __Block_object_assign
    
        .align  2
        .code   16                      @ @__destroy_helper_block_
        .thumb_func     ___destroy_helper_block_
    ___destroy_helper_block_:
        ldr     r0, [r0, #20]
        movs    r1, #3
        b.w     __Block_object_dispose
    

    我猜这些方法是在block拷贝和销毁的时候调用,它们一定是在持有或释放被block捕获的对象。看起来拷贝函数用了两个参数,因为r0r1被寻址,它们两可能有有效的数据。销毁函数好像就一个参数。所有复杂的操作貌似都是_Block_object_assign_Block_object_dispose干的。这部分代码在block runtime里。

    如果你想了解更多关于block runtime的代码,可以去http://compiler-rt.llvm.org下载源码,重点看看runtime.c

    下一篇我们将研究一下Block_Copy的原理。

    相关文章

      网友评论

        本文标题:(译)窥探Blocks(2)

        本文链接:https://www.haomeiwen.com/subject/poxopxtx.html