博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深度围观block:第一集&第二集
阅读量:4108 次
发布时间:2019-05-25

本文共 14226 字,大约阅读时间需要 47 分钟。

深度围观block:第一集&第二集

转自

 深度围观block:第一集&第二集

本文由破船译自galloway!

 深度围观block:第一集

 

 
 小引
还记得之前的两篇文章吗: 和 ,里面介绍了Objective-C生成的汇编代码。本文介绍的内容也跟汇编相关,只不过是与block相关,如果对汇编有不了解的,可以先去看看那两篇带有启蒙性质的文章。本文将从汇编的角度来介绍block相关知识。另外,如果你对block还不了解的话,建议你先去看看我的上一篇文章: 。
 
目录:
简介
基础知识
深入一个简单示例
源码在这里
何去何从
 
正文
简介
今天我们从编译器的角度观察一下block内部是如何工作的。这里说的block是指苹果为C语言增加的具有闭包性(closure)的一个功能,block已经是clang/LLVM编译器所支持的一部分了。我一直在想block是什么,以及它是如何奇迹般的出现在Objective-C对象中(开发者可以像处理实例对象一样,对block进行copy、retain、release)。本文我首先深入的介绍一点关于block的那些事。
 
基础知识
用过block的开发者都知道,下面的代码就是一个block:
void(^block)(void) = ^{  
    NSLog(@"I'm a block!"); 
}; 
 
上面的代码中创建了一个名为
block的变量,并把一个简单的block代码赋值给这个变量。代码很简单,不是吗?不!!!在这里我想要搞清楚编译器对这点代码都做了些什么。
 
更进一步,下面的代码我给block传递了一个变量:
void(^block)(int a) = ^{  
    NSLog(@"I'm a block! a = %i", a); 
}; 
 
而下面的代码是从block中返回一个值:
int(^block)(void) = ^{  
    NSLog(@"I'm a block!"); 
    return 1; 
}; 
 
作为一个封闭的包,block将所处的上下文封装到了block中:
int a = 1;  
void(^block)(void) = ^{ 
    NSLog(@"I'm a block! a = %i", a); 
}; 
 
编译器对上面这些代码具体是如何处理的——这才是我所感兴趣的。
 
深入一个简单示例
首先我的思路是看看编译器是如何编译一个非常简单的block。来看看如下代码:
#import 
 
  
typedef void(^BlockA)(void); 
  
__attribute__((noinline)) 
void runBlockA(BlockA block) { 
    block(); 
  
void doBlockA() { 
    BlockA block = ^{ 
        // Empty block 
    }; 
    runBlockA(block); 
 
之所以要用上面这样的代码,是因为我想看看block是如何创建的,以及如何调用一个block。如果block的创建和调用都在一个函数里面,那么优化器(optimiser)可能会对代码做优化处理,导致我们看不到任何感兴趣的东西,所以我给
runBlockA函数添加了
noinline,这样优化器就不会在
doBlockA函数中对runBlockA的调用做内联优化处理。
 
上面代码通过编译器编译之后(armv7,03),会得到如下汇编指令:
    .globl  _runBlockA  
    .align  2 
    .code   16                      @ @runBlockA 
    .thumb_func     _runBlockA 
_runBlockA: 
@ BB#0: 
    ldr     r1, [r0, #12] 
    bx      r1 
 
上面的汇编代码是对应runBlockA函数——这相当的简单。注意观察之前的源码,可以知道这个函数只是简单的调用了block。在ARM EABI中,将
r0(寄存器r0)设置为第一个参数。第一条指令(r1)是将存储在地址为
r0 + 12的值装载到寄存器
r1中。这可以理解为指针的解引用——读12个字节到寄存器中。然后跳转到这个地址执行后面的指令。注意,这里使用了r1,而r0没有被修改,仍然是原来的block。所以这里很有可能是利用第一个参数来调用block。
 
据此,可以确定block在结构中的一些排序规则:block被当做执行的函数时存储在某个结构中,并占据了12个字节。当传递一个block时,指向这些结构的一个指针被传递进来了。
 
下面来看看
doBlockA函数:
    .globl  _doBlockA  
    .align  2 
    .code   16                      @ @doBlockA 
    .thumb_func     _doBlockA 
_doBlockA: 
    movw    r0, :lower16:(___block_literal_global-(LPC1_0+4)) 
    movt    r0, :upper16:(___block_literal_global-(LPC1_0+4)) 
LPC1_0: 
    add     r0, pc 
    b.w     _runBlockA 
 
OK,上面的代码也不复杂——这是关于pc(program counter)的相关加载。你可以将其看做是把变量___block_literal_global的地址加载到r0中。然后调用runBlockA函数。因为从之前的源码中,可以知道我们把block传递给了runBlockA,所以这里的___block_literal_global一定就是那个被传递的block对象了。
 
到目前为止,我们对上面的源码的运作有一些眉目了!不过这里的___block_literal_global是什么呢?继续看汇编代码,可以找到如下这样的内容:
.align  2                       @ @__block_literal_global  
lock_literal_global: 
.long   __NSConcreteGlobalBlock 
.long   1342177280              @ 0x50000000 
.long   0                       @ 0x0 
.long   ___doBlockA_block_invoke_0 
.long   ___block_descriptor_tmp 
 
Cool!上面的汇编代码看起来像是一个结构体。在结构体中又5个值,每个值有4个字节(long)。这肯定就是
RunBlockA调用中涉及到的那个block对象。再细看一下,12个字节所在处就像一个函数指针:_
__doBlockA_block_invoke_0。这也是runBlockA函数中跳转执行的那个分支(bx r1)。
 
那么上面的汇编代码中
__NSConcreteGlobalBlock又是何物?OK,现在先不介绍这个,后面会做介绍哦!下面我们来看看另外两个感兴趣的东西:
___doBlockA_block_invoke_0
___block_descriptor_tmp,这两个东东同样出现在了汇编代码中:
    .align  2  
    .code   16                      @ @__doBlockA_block_invoke_0 
    .thumb_func     ___doBlockA_block_invoke_0 
___doBlockA_block_invoke_0: 
    bx      lr 
  
    .section        __DATA,__const 
    .align  2                       @ @__block_descriptor_tmp 
___block_descriptor_tmp: 
    .long   0                       @ 0x0 
    .long   20                      @ 0x14 
    .long   L_.str 
    .long   L_OBJC_CLASS_NAME_ 
  
    .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   "\001" 
 
上面的代码中
___doBlockA_block_invoke_0看起来有点像block的实现部分,只不过这里的block是空的,所以会立即返回(刚开始我们就期望编译一个空的block哦)。
接着看看
___block_descriptor_tmp。这里可以看到另外一个数据结构——有4个值。其中第2个是20,这表示
___block_literal_global的大小。接着是一个名为.str的C字符串,它的值为v4@?0,看起来有点像某个类型的编码形式。这可能是block 类型的编码(例如返回void和不携带任何参数)。上面代码中别的一些值我暂时还不清楚。
 
源码在这里
没错,这里有源代码!这是LLVM中compiler-rt项目的一部分。查看代码,我发现在Block_private.h文件中,有如下相关代码:
struct Block_descriptor {  
    unsigned long int reserved; 
    unsigned long int size; 
    void (*copy)(void *dst, void *src); 
    void (*dispose)(void *); 
}; 
  
struct Block_layout { 
    void *isa; 
    int flags; 
    int reserved; 
    void (*invoke)(void *, ...); 
    struct Block_descriptor *descriptor; 
    /* Imported variables. */ 
}; 
 
这看起来很熟悉吧!其中
Block_layout结构体就是
___block_literal_global,而
Block_descriptor结构体则是__block_descriptor_tmp。细看Block_descriptor中的第2个变量size正如我之前描述的一样(表示___block_literal_global的大小)。在Block_descriptor中的第3和第4个值有点奇怪。这看起来有点想函数指针,但是在上面的汇编代码中看起来更像是两个字符串。现在我忽略掉这个细节。
 
Block_layout中的isa肯定就是
__NSConcreteGlobalBlock,这也将确定block如何能够模拟Objective-C对象。如果
__NSConcreteGlobalBlock是一个Class,那么Objective-C消息派送系统会将block对象当做一个普通的对象来处理。这跟如何处理toll-free bridging工作类似。更多相关toll-free bridging信息,可以阅读Mike Ash写的一篇优秀文章。
 
将所有的代码片段拼凑起来,编译器做的工作内容看起来如下所示:
#import 
 
  
__attribute__((noinline)) 
void runBlockA(struct Block_layout *block) { 
    block->invoke(); 
  
void block_invoke(struct Block_layout *block) { 
    // Empty block function 
  
void doBlockA() { 
    struct Block_descriptor descriptor; 
    descriptor->reserved = 0; 
    descriptor->size = 20; 
    descriptor->copy = NULL; 
    descriptor->dispose = NULL; 
  
    struct Block_layout block; 
    block->isa = _NSConcreteGlobalBlock; 
    block->flags = 1342177280; 
    block->reserved = 0; 
    block->invoke = block_invoke; 
    block->descriptor = descriptor; 
  
    runBlockA(&block); 
 
 
 

深度围观block:第二集
今天翻译了第二篇,这个翻译是比较痛苦(其实不止这篇,所有的都是), 不比单纯的阅读,许多地方需要查阅资料,并细心的遣词造句,还得注意词不达意的地方(例如文中的A block that captures scope我翻译为block的拷贝范围,总感觉缺了一些作者原意,功力有限啊)。所以,我劝大家要是能看原文尽量去看原文吧,我这翻译的权当参考。
 
目录
介绍
block类型
block的拷贝范围
block拷贝对象的类型
何去何从
 
正文
介绍
接着-深度围观block:第一集,继续从编译器的角度深度围观block。在本文中,将介绍block并不是一成不变的,以及block在栈上的构成。
 
block类型
在第一篇文章中,我们已经看到block有一个
_NSConcreteGlobalBlock这样的类。由于所有变量都是已知的,所以在编译期间,block的结构(structure)和描述(descriptor)都将全部被初始化。关于block这里有几种不同的类型,每种类型都有对应的类。为了简单起见,这里只考虑其中三种:
 
_NSConcreteGlobalBlock是定义一个全局的block,在编译器就已经完成相关初始化任务。这种类型的block不会涉及到任何拷贝,例如一个空的block。
_NSConcreteStackBlock是一个分配在栈上的block。这里是所有最终被拷贝到堆(heap)上的block的开始。
_NSConcreteMallocBlock是分配到堆(heap)上的block。拷贝完一个block之后,这就会结束。当block的引用计数变为0,该block就会被释放。
 
block拷贝范围
这次我们来看看另外一些代码,如下所示:
#import 
 
  
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); 
 
为了让block拷贝一些内容,上面的代码中调用了foo函数,并给这个函数传递了一个变量。再说一下,本文涉及到的汇编代码是与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 push到栈上面——因为r7会被覆盖,而r7寄存器中的内容在跨函数调用时是需要用到的。
lr是链接寄存器(link register),该寄存器中存储着当这个函数返回时需要执行下一条指令的地址。接着mov这条指令的作用是把栈指针保存到r7
寄存器中。
 
2.从栈指针所处位置开始减去24,也就是在栈空间上开辟24字节来存储数据。
 
3.这里涉及到的代码是为了对符号
L__NSConcreteStackBlock$non_lazy_ptr进行寻址,由于跟pc(program counter)相关联,所以无论代码处于二进制文件中任何位置,当最终链接时,都能对该符号做到正确的寻址。
 
4.将值
1073741824存储到栈指针 + 4 的位置。
 
5.将值
0存储到栈指针 + 8 的位置。现在,将要发生什么可能已经变得逐渐清晰了——在栈上创建了一个
Block_layout结构的对象!到现在为止,已经设置了该结构的3个值:
isa指针,flags和reserved值
 
6.将
___doBlockA_block_invoke_0存储至栈指针 + 12的位置。这是block结构中的
invoke
 
7.将
___block_descriptor_tmp存储至栈指针 + 16的位置。这是block结构中的
descriptor
 
8.将值
128存储到栈指针 + 20的位置。如果回头看看Block_layout结构,可以看到里面只应该有5个值。那么在这个block结构体后面存储的
128是什么呢?——注意到这个128实际上就是在block中拷贝的变量的值。所以这肯定就是存储block使用到的值的地方——在
Block_layout结构尾部。
 
9.现在栈指针指向了已经完成初始化之后的block结构,在这里的汇编指令是将栈指针装载到
r0中,然后调用
runBlockA函数。(记住:在ARM EABI中,
r0中存储的内容被当做函数的第一个参数)。
 
10.最后将栈指针加上24,这样就能够把最开始减去的24(在栈上开辟的24位空间)收回来。接着将栈中的两个值pop到
r7
pc寄存器中。这里pop到
r7中的,跟最开始从
r7中push至栈中的内容是一致的,而pc的值则是最开始push 
lr到栈中的值,这样当函数返回时,可以让CPU能够正确的继续执行后续指令。
 
下面我们再看看block中的invoke函数和descriptor。希望跟第一集中的不要有太大差别。如下汇编代码:
    .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的结构需要24个字节,而不再是标准的20个字节了。在之前的代码中,我们已经分析了在创建block时,多出的4个字节被添加到block结构的尾部。
 
在实际的block函数中,例如
___doBlockA_block_invoke_0,可以看到从block结构尾部读取出相关值,如r0 + 20,就是在block中拷贝的变量。
 
block拷贝对象的类型
下面我们来看看如果block拷贝的是别的对象类型(例如 NSString),而不是integer,会发生什么呢?如下代码:
#import 
 
  
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_ 
opy_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_ 
estroy_helper_block_: 
ldr     r0, [r0, #20] 
movs    r1, #3 
b.w     __Block_object_dispose 
 
这里我先假设当block被拷贝和销毁时,都会调用这里的函数。那么被block拷贝的对象肯定会发生reatain和release。上面的代码中,可以看出如果r0和r1包含有效数据时,拷贝函数接收两个参数(r0和r1)。而销毁函数接收一个参数。可以看出所有的拷贝和销毁任务都应该是由
__Block_object_assign
__Block_object_dispose两个函数完成的。这两个函数位于block的运行时代码中(是LLVM里面
compiler-rt工程的一部分)。
 
如果你希望了解一下block运行时相关代码,可以来这里下载源码:http://compiler-rt.llvm.org。特别关注一下里面的
runtime.c文件。
何去何从
在下一集中我将调查Block_copy相关代码,并看看相关工作处理情况,以此来深度围观一下block运行时。通过下一集的学习,你也将会深入了解拷贝和销毁函数(也就是本文中我们刚刚看到的在block拷贝对象时使用的函数)。

转载地址:http://vyvsi.baihongyu.com/

你可能感兴趣的文章
stm32的spi
查看>>
iar查看运行时间
查看>>
IAR 的精确延时程序
查看>>
STM32 MDK工程中使用printf
查看>>
STM32的两个.bin文件如何合并?
查看>>
合并BIN文件的两种方法
查看>>
网线中哪几根真正有用?
查看>>
STM32的SPI时钟
查看>>
UltraEdit 操作小技巧--一次性修改多列
查看>>
IAR的有用的快捷键
查看>>
FTP工具取消被动模式的办法,cuteftp/flashfxp 被动模式!
查看>>
关于pasv模式中,数据端口由谁指定
查看>>
norflash芯片内执行(XIP)
查看>>
STM32 中断向量表的位置 、重定向
查看>>
STM32 启动解析,启动代码,__main main
查看>>
STM32 keil mdk启动代码发分析
查看>>
stm32 库文件_line 函数
查看>>
深入理解SP、LR和PC
查看>>
stm32应用部分发生中断,PC跳转到IAP中断向量表处后,如何跳转到应用部分中断函数入口的
查看>>
选择ARM7还是cortex-M3?
查看>>