从汇编代码理解 Block 的内存结构
- 2020 年 4 月 10 日
- 筆記
❓ 在断点调试 iOS 程序碰到 block 作为函数的形参时,如果想知道该 block 本身的函数签名信息和函数体地址时,有哪些办法?
? 当然是在源码里面直接查看 block 的声明和调用了!
❗️ 但如果源码不可见呢?在分析第三方闭源库或友商 App 的某些逻辑实现时,就只有汇编代码可用
☕️ 本文将通过汇编代码入手探讨 block 的内存结构,并尝试还原 block 的函数签名信息和函数体真实地址
环境准备
如果拿真实案例来做分析,势必会有一系列繁琐的前期准备步骤,而且一时也想不起来有哪些友商 App 刚好可以做 demo,所以本文将使用本地环境做模拟,并直接从最关键的步骤开始
-
打开 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
-
为了尽量模拟真实环境也为了去掉汇编中不必要的指令,在 Xcode 的 Scheme 中将
Run -> Build Configuration
修改为 Release ;然后在[self doBlockTest:testBlock];
这行下断点 -
点
Run
,等运行到断点位置后,勾选 Xcode 菜单Debug -> Debug Workflow -> Always Show Disassembly
打开汇编指令视图:
调用前的分析
从现在开始,我们假装忘记了刚刚写的源码,尝试从汇编代码中得到 block 的基本信息。
-
从第 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
的调用 -
按住
Control
键,通过点Step info
单步执行到第 19 行。objc_msgSend
原型为objc_msgSend(id self, SEL op,...)
,本例中有三个参数,按照约定这三个参数将依次放在x0
、x1
、x2
中。所以x0
为ViewController
实例,x1
为 selector,x2
为 block 结构体指针。读取寄存器和打印x0
可验证:
block 类型形参的分析
前面的更多是汇编基础的回顾,现在即将进入重点
-
点
Step over
进入到真正被调用函数,如图:
按照刚刚的分析,x2
里面就是这个 block 形参,直接po
该形参是没有函数体地址和函数签名等信息的:
-
尝试还原 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
指向的地址上执行 -
根据上下文,第 2 行中的
x2
中就是 block 形参指针(0x0000000100584090
),在第 3 行中,取了该指针指向内容并偏移0x10
处的地址 P 赋值给了x3
,在第 7 行中执行了x3
指向的内容(也是一个地址),说明 P 是一个函数(函数指针)可被执行,这正是 block 的特性。参考 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 的实现里面 -
获取 block 的函数签名
函数签名在Block_descriptor_1
类型结构体descriptor
的signature
成员变量上,我们目标是通过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_helper
和dispose_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
),和源码一致。 -
函数体地址正确性验证
还记得第 4 步在0x00000001005822b4
设置的断点吗?点Continue
或 lldb 输入c
,让程序继续执行,结果在0x00000001005822b4
处被断住了:
而且断住的位置在ViewController.m:19
,查看源码发现刚好就是 block 的实现:
PS:本人汇编小白,难免有理解不到位或者错误的地方,希望大佬多多指教!