从汇编代码理解 Block 的内存结构

  • 2020 年 4 月 10 日
  • 筆記

❓ 在断点调试 iOS 程序碰到 block 作为函数的形参时,如果想知道该 block 本身的函数签名信息和函数体地址时,有哪些办法?
? 当然是在源码里面直接查看 block 的声明和调用了!
❗️ 但如果源码不可见呢?在分析第三方闭源库或友商 App 的某些逻辑实现时,就只有汇编代码可用

☕️ 本文将通过汇编代码入手探讨 block 的内存结构,并尝试还原 block 的函数签名信息和函数体真实地址

环境准备

如果拿真实案例来做分析,势必会有一系列繁琐的前期准备步骤,而且一时也想不起来有哪些友商 App 刚好可以做 demo,所以本文将使用本地环境做模拟,并直接从最关键的步骤开始

  1. 打开 Xcode,新建一个 Single View App,将下面的代码贴入 ViewController.m 中,准备好证书和 64bit 真机。

    #import "ViewController.h"  typedef void(^HHBlock)(NSString *,NSInteger );    @interface ViewController ()  @end    @implementation ViewController    - (void)viewDidLoad  {      [super viewDidLoad];      void (^testBlock)(NSString *, NSInteger ) = ^(NSString *var1, NSInteger var2){          NSLog(@"var1 = %@, var2 = %@", var1, @(var2));      };      [self doBlockTest:testBlock];  }    - (void)doBlockTest:(HHBlock)block  {      block(@"v1",10086);  }    @end  
  2. 为了尽量模拟真实环境也为了去掉汇编中不必要的指令,在 Xcode 的 Scheme 中将 Run -> Build Configuration 修改为 Release ;然后在 [self doBlockTest:testBlock]; 这行下断点

  3. Run,等运行到断点位置后,勾选 Xcode 菜单 Debug -> Debug Workflow -> Always Show Disassembly 打开汇编指令视图:

调用前的分析

从现在开始,我们假装忘记了刚刚写的源码,尝试从汇编代码中得到 block 的基本信息。

  1. 从第 14 行开始分析:
    L14. nop: 空指令,什么也不做,猜测是和内存对齐有关
    L15. ldr x1, #0x3260: 将 #0x3260 指向的内存数据加载到寄存器 x1
    L16. adr x2, #0x1dfc: 将 #0x1dfc 指向的内存数据加载到寄存器 x2
    L17. 同第 14 行
    L18. mov x0, x19: 将 19 号寄存器的值复制到 0 号寄存器
    L19. bl: 调用 0x100582540 处的函数,即 objc_msgSend 的调用

  2. 按住 Control 键,通过点 Step info 单步执行到第 19 行。objc_msgSend 原型为objc_msgSend(id self, SEL op,...),本例中有三个参数,按照约定这三个参数将依次放在x0x1x2 中。所以 x0ViewController 实例,x1 为 selector,x2 为 block 结构体指针。读取寄存器和打印 x0 可验证:

block 类型形参的分析

前面的更多是汇编基础的回顾,现在即将进入重点

  1. Step over 进入到真正被调用函数,如图:

    按照刚刚的分析,x2 里面就是这个 block 形参,直接 po该形参是没有函数体地址和函数签名等信息的:

  2. 尝试还原 block 信息,按行分析:
    L3. ldr x3, [x2, #0x10]: 将 x2 指向的内容加上 16 个字节偏移后的地址,加载到x3
    L4. adr x1, #0x1d94: 将 #0x1d94 指向的内容加载到 x1
    L5. 同上
    L6. mov w2, #0x2766: 将 #0x2766 复制到 w2 的低 32 位上。0x2766 也就是十进制的 10086
    L7. br x3: 跳转到 x3 指向的地址上执行

  3. 根据上下文,第 2 行中的 x2 中就是 block 形参指针(0x0000000100584090),在第 3 行中,取了该指针指向内容并偏移 0x10 处的地址 P 赋值给了 x3,在第 7 行中执行了 x3 指向的内容(也是一个地址),说明 P 是一个函数(函数指针)可被执行,这正是 block 的特性。参考 block 的内存结构衍生出来的面试题

  4. 根据 llvm 官网对 block 实施标准的描述

    struct Block_literal_1 {      void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock      int flags;      int reserved;      void (*invoke)(void *, ...);      struct Block_descriptor_1 {          unsigned long int reserved;         // NULL          unsigned long int size;         // sizeof(struct Block_literal_1)          // optional helper functions          void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)          void (*dispose_helper)(void *src);             // IFF (1<<25)          // required ABI.2010.3.16          const char *signature;                         // IFF (1<<30)      } *descriptor;      // imported variables  };  

    block 结构体指针偏移 sizeof(void *) + sizeof(int) + sizeof(reserved) 后就是真正函数体 invoke 的地址。在 64bit 系统上指针类型和 int 类型分别占用 8 个字节和 4 个字节,考虑到结构体内存对齐,真正的函数体的偏移量为: 8 + 8 = 0x10,即 16 个字节,invoke 的地址为: 结构体指针地址 + offset(16 字节)
    接下来我们读取这个函数体地址,首先打印 block 结构体指针指向的内容:

    结构体内容开始于 0x00000001b365a288,函数体 invoke 地址开始于 16 字节后,占用 8 个字节,所以其地址为:0x00000001005822b4,我们在这里下一个断点:br s -a 0x00000001005822b4,等会验证下是不是刚好断在 block 的实现里面

  5. 获取 block 的函数签名
    函数签名在 Block_descriptor_1 类型结构体 descriptorsignature 成员变量上,我们目标是通过 signature 得到 NSMethodSignature 实例,进而得到 invoke 详细的函数签名信息。descriptor 的偏移量: offset(*invoke) + sizeof(*invoke),所以 descriptor 的地址为 0x0000000100584070。另外根据文档,并不是所有 block 都存在这个 signature 变量,需要通过 flags 与 block 中定义的枚举掩码进行 & 操作来判断,枚举掩码定义:

    enum {      // Set to true on blocks that have captures (and thus are not true      // global blocks) but are known not to escape for various other      // reasons. For backward compatibility with old runtimes, whenever      // BLOCK_IS_NOESCAPE is set, BLOCK_IS_GLOBAL is set too. Copying a      // non-escaping block returns the original block and releasing such a      // block is a no-op, which is exactly how global blocks are handled.      BLOCK_IS_NOESCAPE      =  (1 << 23),        BLOCK_HAS_COPY_DISPOSE =  (1 << 25),      BLOCK_HAS_CTOR =          (1 << 26), // helpers have C++ code      BLOCK_IS_GLOBAL =         (1 << 28),      BLOCK_HAS_STRET =         (1 << 29), // IFF BLOCK_HAS_SIGNATURE      BLOCK_HAS_SIGNATURE =     (1 << 30),  };  

    按照刚刚的方法,尝试获取 flags 的值:

    flags 的值为 0x50000000,判断后发现存在 signature:

    signature 近在咫尺!但是仔细看 block 文档的定义,排在 signature 前面的两个函数指针 copy_helperdispose_helper 有这句注释:optional helper functions,看来还需要判断这两个指针是否存在:

    嗯,不存在,所以 signature 的偏移量:offset(long int) + offset(long int),长度 sizeof(char *),接着打印 descriptor 的内容:

    所以 signature 的值为 0x0000000100583382,打印看看是什么:

    眼熟的 Type Encodings 字符串,转换成 NSMethodSignature 的实例看看:

    这个 block 函数体没有返回值(v->void),接收 3 个参数,第一个是 block ——这是所有 block 函数体的潜规则,后续两个的参数依次为:@(NSString *) 和 q(long long),和源码一致。

  6. 函数体地址正确性验证
    还记得第 4 步在 0x00000001005822b4 设置的断点吗?点 Continue 或 lldb 输入 c,让程序继续执行,结果在 0x00000001005822b4 处被断住了:
    而且断住的位置在 ViewController.m:19,查看源码发现刚好就是 block 的实现:

PS:本人汇编小白,难免有理解不到位或者错误的地方,希望大佬多多指教!

Ref