Objective-C 如何hook block

利用 Objective-C 语言的 runtime 特性, 通过 Method Swizzling 技术可以实现 hook OC 类的某个方法,但是怎么 hook block 呢?比如想要修改 block 的实现我们该怎么做?

Block 是怎样实现的

首先 Block 也是 OC 对象,它的 isa 指针在初始化时指向 &_NSConcreteStackBlock 或者 &_NSConcreteGlobalBlock,Block 的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 的结构之后,再来看下它从定义到运行是如何实现的。以下面这段源码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// example
// file: main.m

int main(int argc, char * argv[]) {
@autoreleasepool {

int integer = 10;
void (^block)(int) = ^(int a) {
NSLog(@"block is called: %d",a);
};
block(integer);

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

终端定位到 main.m 文件目录,利用 Clang 命令,把 main.m 文件转化为 main.cpp 文件,并摘要如下:

1
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// file: main.cpp

#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

//
static __NSConstantStringImpl __NSConstantStringImpl__var_folders_tv_p2zj_t2s1jlb5sl6zrf6mlf40000gp_T_main_4d4c05_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"block is called: %d",19};


struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a) {

int var2 = 22;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_tv_p2zj_t2s1jlb5sl6zrf6mlf40000gp_T_main_4d4c05_mi_0,a+var2);
}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

int var1 = 11;
void (*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));


((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, var1);

return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
}
}

可以看到,经过 Clang 转化为c++代码之后,原先5行的源码增加了很多,但我们仍然可以从转化后的代码中找到原始代码的“痕迹”,尽管在形式上已经变得大不相同。

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

结构体 __block_impl 清晰的表明了 Block 的结构,Block 也是OC对象,所以它包含 isa 指针;Reserved 作为保留字段;函数指针 FuncPtr 指向 Block 的实现。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

结构体 __main_block_impl_0 包含两个变量以及一个构造函数。变量 impl__block_impl 类型;指针变量 Desc 指向 __main_block_desc_0 类型的结构体;构造函数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) 第一个参数 fp 是函数指针,指向Block的函数实现。

除了上面提到的三种结构体类型,从转换后的源码中还可以看到一个静态函数 __main_block_func_0 以及一个静态结构体变量 __main_block_desc_0_DATA。静态函数 __main_block_func_0 承载了 Block 要执行的真正任务;静态结构体变量 __main_block_desc_0_DATA 被初始化时的参数分别是 0 和 结构体 __main_block_impl_0 所占存储空间大小。



了解了 Block 相关的数据结构之后,我们开始看 main 函数中 Block 代码是如何调用运行的。首先是 Block 的定义代码:

1
2
int var1 = 11;
void (*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

去除类型转换,上面的代码等价于:

1
2
3
int var1 = 11;
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *block = &temp;

这样就容易理解了,即栈上生成的 __main_block_impl_0 类型结构体实例的指针,复制给 __main_block_impl_0 结构体指针变量 block

然后是 Block 的调用代码:

1
((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, var1);

去除类型转换,上面的代码等价于:

1
(*block ->impl.FuncPtr)(*block, var1)

这就是简单的使用函数指针进行函数调用。

所以,我们只要把 __main_block_impl_0 中的 impl 变量的 FuncPtr 函数指针修改掉,就可以达到替换 Block 所执行的函数的目的。

Hook Block 怎样实现?

上面得出结论,只要把 __main_block_impl_0 中的 impl 变量的 FuncPtr 函数指针修改掉,就可以达到替换 Block 所执行的函数的目的。那么怎么修改 Block 的函数指针呢?跟着下面这个问题来一起看下。

问题1:实现下面的函数,将Block的实现修改成打印 “Hello world.”

1
2
3
void HookBlockToPrintHelloWorld(id block) {

}

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//file:main.m

typedef struct hook_Block_Desc {
size_t reserved;
size_t Block_size;
} hook_Block_Desc;

struct hook_Block_impl { //自己定义一个 hook_Block_impl,因为系统的你调不到
void *isa;
int Flags;
int Reserved;
void *funPtr;
hook_Block_Desc *desc;
};

void fun(struct hook_Block_impl * impl, ...) {
printf("Hello world.\n");
}

void printHellpWorld(id block) {
struct hook_Block_impl *blockImpl = (__bridge struct hook_Block_impl *)(block);
blockImpl ->funPtr = &fun;
};


int main(int argc, char * argv[]) {
@autoreleasepool {

int var1 = 11;
void (^block)(int) = ^(int a) {
int var2 = 22;
NSLog(@"block is called: %d",a+var2);
};

printHellpWorld(block);
block(var1);

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

问题2: 实现下面的函数,将Block的实现修改成打印所有入参,并调用原始实现.

1
2
3
4
5
6
void(^block)(int a, NSString *b) = ^(int a, NSString *b) {
NSLog(@"block invoke");
}
HookBlockToPrintArguments(block);
block(123,@"aaa");
//这里输出"123, aaa" 和 "block invoke";

实现如下:

基本思路是将原始 Block 的实现函数,替换成我们自定义的函数,然后将参数打印,最后回调原始 Block 的实现函数。简单实现方法如下(只针对固定的入参,不具备通用性):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//file: main.m

typedef struct hook_Block_Desc {
size_t reserved;
size_t Block_size;
} hook_Block_Desc;

struct hook_Block_impl {
void *isa;
int Flags;
int Reserved;
void *funPtr;
hook_Block_Desc *desc;
};

//函数指针静态变量,保存 Block 的原始函数实现
static void (* orig_func)(void *block, int a, NSString *b);


void hookBlock(void *block, int a, NSString *b) {
NSLog(@"%d, %@",a, b);
orig_func(block, a, b);
};

void HookBlockToPrintArguments(id block) {
struct hook_Block_impl *blockImpl = (__bridge struct hook_Block_impl *)block;
orig_func = blockImpl->funPtr;
blockImpl->funPtr = &hookBlock;
}

int main(int argc, char * argv[]) {
@autoreleasepool {

int a = 123;
NSString *b = @"abc";
void (^block)(int, NSString *) = ^(int a, NSString *b) {
NSLog(@"block invoke.");
};

HookBlockToPrintArguments(block);
block(a, b);

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

下面是通用实现方法,因为要接收不确定的参数类型和个数,所以需要循环判断参数类型然后赋值,而且需要深拷贝一份原始 Block 作为参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
struct hook_Block_impl;

typedef struct hook_Block_Desc {
size_t reserved;
size_t Block_size;
void (*copy)(struct hook_Block_impl*, struct hook_Block_impl*);
void (*dispose)(struct hook_Block_impl*);
} hook_Block_Desc;

typedef struct hook_Block_impl {
void *isa;
int Flags;
int Reserved;
void *funPtr;
hook_Block_Desc *desc;
} hook_Block_impl;

hook_Block_impl *orig_Block;

void hookBlockArgs(hook_Block_impl *impl, ...) {
va_list arg_ptr;
va_start(arg_ptr, impl);
NSObject *tempBlock = CFBridgingRelease(orig_Block);

const char *_Block_signature(void *);
const char *hookBlockSignature = _Block_signature((__bridge void *)(tempBlock));
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:hookBlockSignature];
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:methodSignature];

[blockInvocation setArgument:&tempBlock atIndex:0];
NSMutableArray *argumentTypeArr = [[NSMutableArray alloc] init];
for (NSInteger i = 1; i < methodSignature.numberOfArguments; ++i) {
const char *ch = [methodSignature getArgumentTypeAtIndex:i];
NSString *arg = [NSString stringWithUTF8String:ch];
[argumentTypeArr addObject:arg];
}
NSInteger arg_count = 1;
for (NSString *argType in argumentTypeArr) {
if ([argType isEqualToString:@"i"]) {
int arg = va_arg(arg_ptr, int);
NSLog(@"int param %d",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"I"]) {
unsigned int arg = va_arg(arg_ptr, unsigned int);
NSLog(@"unsigned int param %d",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
}else if ([argType isEqualToString:@"f"]) {
double arg = va_arg(arg_ptr, double);
NSLog(@"float param %lf",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"d"]) {
double arg = va_arg(arg_ptr, double);
NSLog(@"double param %lf",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"B"]) {
int arg = va_arg(arg_ptr, int);
NSLog(@"BOOL param %d", arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"c"]) {
char arg = va_arg(arg_ptr, int);
NSLog(@"char param %c",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"l"]) {
long arg = va_arg(arg_ptr, long);
NSLog(@"long param %ld",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"L"]) {
unsigned long arg = va_arg(arg_ptr, unsigned long);
NSLog(@"long param %lu",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"q"]) {
long long arg = va_arg(arg_ptr, long long);
NSLog(@"long long param %lld",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"Q"]) {
unsigned long long arg = va_arg(arg_ptr, unsigned long long);
NSLog(@"unsigned long long param %llu",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if ([argType isEqualToString:@"*"]) {
void *arg = va_arg(arg_ptr, void *);
NSLog(@"pointer param %p",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else if (argType.length > 2) {
id arg = va_arg(arg_ptr, id);
NSLog(@"%@ %@",[arg class], arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
} else {
//unknow 指针
void *arg = va_arg(arg_ptr, void *);
NSLog(@"%p",arg);
[blockInvocation setArgument:&arg atIndex:(arg_count)];
}
arg_count += 1;
}
va_end(arg_ptr);
[blockInvocation invokeWithTarget:tempBlock]; //
};


void HookBlockToPrintArguments(id block) {
struct hook_Block_impl *blockImpl = (__bridge struct hook_Block_impl *)block;
//拷贝一份原始 Blcok 副本
orig_Block = malloc(blockImpl ->desc ->Block_size);
memcpy(orig_Block, blockImpl, blockImpl ->desc ->Block_size);
//将当前 Block 的函数指针指向我们自己定义的函数
blockImpl ->funPtr = &hookBlockArgs;
}

int main(int argc, char * argv[]) {
@autoreleasepool {

int a = 123;
NSString *b = @"abc";
void (^block)(int, NSString *) = ^(int a, NSString *b) {
NSLog(@"block invoke.");
};

HookBlockToPrintArguments(block);
block(a, b);

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

总结

以上介绍了 Block 的数据结构以及 OC 语言是怎样实现 Block 定义与调用的。理解了 Block 的原理,就能够实现 Hook Block,并针对提出的两个问题,做出了解答。