从汇编理解 Block 的内存结构
❓ 在断点调试 iOS 程序碰到 block 作为函数的形参时,如果想知道该 block 本身的函数签名信息和函数体地址时,有哪些办法?
🤪 当然是在源码里面直接查看 block 的声明和调用了!
❗️ 但如果源码不可见呢?在分析第三方闭源库或友商 App 的某些逻辑实现时,就只有汇编代码可用
☕️ 本文将通过汇编代码入手探讨 block 的内存结构,并尝试还原 block 的函数签名信息和函数体真实地址
环境准备
如果拿真实案例来做分析,势必会有一系列繁琐的前期准备步骤,而且一时也想不起来有哪些友商 App 刚好可以做 demo,所以本文将使用本地环境做模拟,并直接从最关键的步骤开始
打开 Xcode,新建一个
Single View App
,将下面的代码贴入ViewController.m
中,准备好证书和 64bit 真机。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23#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 的内存布局衍生而来的面试题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct 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 中定义的枚举掩码进行&
操作来判断,枚举掩码定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15enum {
// 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 函数体的潜规则(将上述源码编译为 C++ 的就能知道),后续两个的参数依次为:@
(NSString *
) 和q
(long long
),和源码一致。- 函数体地址正确性验证
还记得第 4 步在0x00000001005822b4
设置的断点吗?点Continue
或 lldb 输入c
,让程序继续执行,结果在0x00000001005822b4
处被断住了:
而且断住的位置在ViewController.m:19
,查看源码发现刚好就是 block 的实现:
PS:本人汇编初学,若发现理解不到位或者错误的地方,望指教!
Ref
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!