Principio de bloque (1)

Qué es exactamente un bloque, comencemos con el código C++

Comience con la estructura de bloques más simple

imagen.png

clang -rewrite-objc main.m -o main.cpp && abrir main.cpp

imagen.png

imagen.png

Para que sea más fácil de leer, simplifiquemos el código.

imagen.png

Para facilitar la lectura adicional, la denominación se simplifica aquí, consulte el siguiente proceso simple

imagen.png

  • Combinado con clang para compilar el código C++ intermedio, a través de la creación de un bloque, combinado con la imagen de arriba, primero delinee un boceto en mente

    • Crear una estructura de dos niveles

      • BloqueCrear estructura

      • Estructura de bloque, miembro de BlockCreate

    • Construya parámetros a través de BlockCreate, cree una instancia del miembro BlockCreate Block::block

    • El retorno final es un puntero de estructura BlockCreate

    • A través de la primera dirección de la estructura BlockCreate, podemos obtener el miembro Block::block, la primera dirección de BlockCreate es la misma que la primera dirección del bloque miembro, porque el bloque se encuentra al principio del espacio de memoria BlockCreate

      Dado que puede obtener la primera dirección (dirección de bloque de miembro), también puede obtener la dirección del miembro Desc a través del desplazamiento de memoria

    • Al obtener la dirección del miembro Block::block, puede llamar al método miembro FuncPtr de Block::block, y FuncPtr es precisamente la dirección de entrada de la función divertida asignada cuando se crea una instancia del miembro Block::block a través de la construcción BlockCreate .

  • Asegúrese de comprender esta estructura de dos capas. Aunque no es el código fuente real, es muy útil para nosotros analizar el código fuente más adelante.

El ejemplo anterior no usa variables, podemos operarlo nuevamente de la forma anterior, y comparar la diferencia

imagen.png

imagen.png

Al acceder a una variable local fuera de la estructura personalizada

Encontrará que el código C++ generado por clang ha cambiado. Compare el proceso de creación de instancias anterior.

  • Hay un miembro más int a en la estructura BlockCreate

  • La construcción BlockCreate también tiene un parámetro adicional

  • Se accede a la variable a dentro de la función func a través de BlockCreate::*self de func(BlockCreate *self) para obtener una copia

  • Encontrará que hay 3 lugares donde existe la variable a

    • La variable local a dentro de la función principal

    • BlockCrete 结构体内的成员变量a

    • func方法内部的局部变量a

    其实这3个变量a分别是3个不同的变量了

把局部变量a改为static修饰,继续clang c++查看

imagen.png

imagen.png

用static修饰变量a,不一样了

BlockCreate构造传参,此时传递的是 a的地址,而BlockCreate成员 a也变成了 指针, func内部的局部变量a 也变成了 指针,func内部的a是通过 BlockCreate::*self 的指针a 赋值 给func内部的局部变量 指针a

所以static修饰a后,func内部访问的a其实还是 main函数内部的 指针a

把局部变量a改为 __block修饰,继续clang c++查看

imagen.png

imagen.png

希望你不会觉得懵,这次复杂了些

  • 出现了一个结构 __Block_byref_a_0

  • BlockCreate 成员Desc的结构内部多了两个 函数 copy & dispose

这里简单解释下

  • 普通的局部变量a 变成了一个结构 __Block_byref_a_0, a是这个结构的成员

    • 成员 void *__isa

    • 成员 __block_byref_a_0 *__forwarding;

    • 成员 int __flags;

    • 成员 int __size

    • 成员 int a

    在main里声明的__block修饰的局部变量, 地址赋值给了 __forwarding, 值赋给了 Block_byref结构里的成员a,注意这个设定, 虽然成员也叫a,只是起到一个接收值的作用,关键在于__forwarding 拿到了原来的a的指针

    先看下__block修饰的a究竟是怎么访问的

imagen.png

__forwarding 类型 __Block_byref_a_0 *,类似于链表节点,所以也是一个指向 __Block_byref_a_0 结构的指针 至于有什么用,暂存疑,后面源码接着分析

对比着看,其实很明显,不难理解

imagen.png

block源码 - libclosure-79 查看

源码入口该怎么查看呢,我们先通过汇编看下

imagen.png

既然retainBlock,说明block开辟了空间,进入查看

imagen.png

继续跳转 br x16

imagen.png

目前找到了_Block_copy这样一个符号,然后进入源码查看

imagen.png

你会看到一个结构Block_layout

imagen.png

Block_layout 就是前面通过clang c++代码 分析出的 两层结构BlockCreate成员 Block::block

__block 修饰变量 测试代码放进 block源码进行调试

imagen.png

这段代码是在block源码中测试的

imagen.png

这其实就是依照Block_layout 栈上的空间结构,在堆区创建了一个Block_layout结构

同时 新开辟的Block_layout结构->invoke 从原来栈上Block_layout->invoke拷贝过来

imagen.png

既然是堆上开辟空间创建的Block_layout结构,自然isa 指向 _NSConcreteMallocBlock (堆block)

block分析源码遇到问题

现在还有两块没探索到源码,就是 前面通过clang 编译生成的c++代码中__Block_byref_a_0这样的结构,还有一块是BlockCreate构造逻辑部分

那么接下来该何去何从?

我选择最原始的方式 汇编 + 下符号断点 + 结合clang c++代码分析

imagen.png

先把代码断到此处,防止dyld其他流程干扰

imagen.png

下符号断点 同时把前面分析过的 _Block_copy 符号也下下来,为了方便分析流程

跟着调试 进入 _Block_object_dispose:

imagen.png

回到之前clang编译出的c++代码看下

imagen.png

既然下到了符号_Block_object_dispose 那么同样也把符号 _Block_object_copy下下来继续调试

没有的话 就试试 _Block_object_assign, 之所以没有找到 _Block_object_copy符号,是因为那是由编译器决定的

成功断点符号 _Block_object_assign

imagen.png

找到头绪,自然我们又回到了源码

imagen.png

  • 看下源码注释

    When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point to do the assignment.

    当Blocks(可以理解为前面的有成员func的那个结构) 或者 Block_byref持有对象时候,这个入口就会被触发 执行赋值操作

imagen.png

  • __block int a = 10 类型为 BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK or BLOCK_FIELD_IS_BYREF

    执行 _Block_byref_copy()

_Block_byref_copy

在分析_Block_byref_copy流程之前,我们需要了解下Block_byref 是什么

imagen.png

从前面clang编译拿到的c++代码,可以看到,Block_byref 是对常规变量的封装,封装结构里还多了isa,__forwarding成员

imagen.png

源码中还存在 Block_byref_2 Block_byref_3 两个结构,暂且不表,后面会继续说明

我们可以做个假设,目前我们测试的实例 是block引用外部 __block修饰的变量,我们也是这么用的,既然block内部访问外部变量,那么也会对于这个变量的引用计数产生影响 flags就是存储引用计数的

_Block_byref_copy翻译

imagen.png

如果源byref结构已经在heap上,则不需要执行拷贝,引用计数+1

imagen.png

中间有一段内存偏移的代码,还没解析,继续

从源码中我们看到

Block_byref_2 *src2 = src + 1
Block_byref_3 *src3 = src2 + 2

那么 Block_byref Block_byref_2 Block_byref_3 是连续的内存结构,根据条件判断是否解析 Block_byref_2 Block_byref_3

认知遗留问题

找遍了源码 clang编译出的c++代码里 __main_block_impl_0 这样的结构并没有发现

imagen.png

byref_keep byref_destroy 究竟实现了什么功能

因为我们用的常规变量a测试 我们换成object看下

将变量a换为object测试

imagen.png

clang c++代码

imagen.png

imagen.png

从源码得知

imagen.png

编译阶段,Block_byref结构 flag被设置为 1 << 25, 标识是有 Block_byref_2结构的

imagen.png

imagen.png

131有什么意义

imagen.png

两个参数 + 40 什么意思

imagen.png

按照编译的逻辑,byref_keep 就是 object类型的对象的 拷贝

但是运行时会做修正 流程有差别

同样 byref_destroy:

imagen.png

以上为 Block_byref 逻辑,再通过clang得到的c++ 看下 Block_layout 的处理

imagen.png

imagen.png

再确认下 __block修饰的 object对象,在block体里 究竟是如何访问的

imagen.png

总结

  • __block 修饰变量之后,编译器会在栈上构建一个 栈Block_byref(包含变量指针)

  • 定义block,可以理解为编译器生成一个中间结构BlockCreate(这个名字是特意起的,知道是个结构,为了便于理解,你可以这么理解)

    • 同时编译器会在栈上初始化构建一个 栈Block_layout(包含func成员)
  • 执行BlockCreate构造方法

    • 通过Block_layout首地址偏移 得到 Block_copy函数地址, 执行Block_copy,把 栈Block_byref 拷贝 到堆Block_byref

    • 构造参数 栈Block_byref,通过Block_byref首地址偏移 得到 Block_byref_2(包含_Block_byref_copy 即byref拷贝函数)首地址, 执行 _Block_byref_copy函数, 把栈Block_byref 拷贝到 堆Block_byref

    • 继续上一步的位置 内存偏移 8字节,得到堆上开辟的 object内存空间首地址, 这里当然就存放 object对象了

    • 需要注意的一个细节 栈Block_byref 拷贝到 堆Block_byref之后,由于堆上是新的内存空间,那么栈与堆不就两个空间了吗,如何保障访问的是同一块内存?

      Creo que la solución es apuntar el reenvío en la pila Block_byref y el montón Block_byref al montón Block_byref después de copiar, es decir, el reenvío del montón apunta a sí mismo nuevamente

      Después de que __block modifica una variable, ya sea accediendo a la variable dentro del bloque de bloque o accediendo a la variable fuera del bloque, accede al espacio de almacenamiento dinámico a través del reenvío y luego accede a la variable en el espacio de destino, asegurando así que la variable a la que se accede es la misma pieza de espacio de memoria

    imagen.png

    imagen.png

imagen.png

  • Termina el ciclo de vida de la variable en poder de Block_byref, ejecuta _Block_object_dispose

    • Ejecute la función _Block_byref_release, encuentre la primera dirección de Block_byref_2 de acuerdo con el desplazamiento de la primera dirección de Block_byref, continúe compensando 8 bytes para obtener byref_destroy Ejecute el destructor para recuperar el espacio de memoria del montón
  • Finaliza el alcance de Block_layout o finaliza el ciclo de vida, ejecute _Block_release

    • Encuentre la primera dirección de Block_descriptor_2 de acuerdo con el primer desplazamiento de dirección de Block_layout, continúe compensando 8 bytes y luego dispose ejecuta el destructor para recuperar el espacio de memoria del montón Block_layout abierto en el montón

leer vista de registro

imagen.png

Breakpoint_Block_copy simbólico

imagen.png

Antes de ejecutar _Block_copy, el registro rax recibe parámetros (arm64 lee el registro x1)

imagen.png

Después de la ejecución, ret regresa y el registro rax almacena el valor de retorno

imagen.png

  • La variable a se cambia a __modificación de bloque

imagen.png

Debido a la modificación de __block, la dirección de la función de copia aparece en Block_layout y, a través de la copia, se ejecuta _Block_copy.

Sin la modificación de __block, no hay una función de eliminación de copias y _Block_copy se ejecuta de forma predeterminada

Esta diferencia es causada por la diferencia en las banderas cuando se construyen los parámetros.Antes de la modificación de __block, es 0, y después de la modificación de __block, 1 << 25

imagen.png

Supongo que te gusta

Origin juejin.im/post/7118386172414328868
Recomendado
Clasificación