OC方法调用在汇编中的展示
项目中新建一个Person
类:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
+ (instancetype)person;
@end
@implementation Person
+ (instancetype)person {
return [[Person alloc] init];
}
@end
我们直接在main函数里调用工厂方法person
,断点看下调用方法的汇编
Demo`main:
...
0x1025b6038 <+24>: adrp x8, 7
0x1025b603c <+28>: add x8, x8, #0x3c8 ; =0x3c8
-> 0x1025b6040 <+32>: ldr x0, [x8]
0x1025b6044 <+36>: adrp x8, 7
0x1025b6048 <+40>: add x8, x8, #0x3c0 ; =0x3c0
0x1025b604c <+44>: ldr x1, [x8]
0x1025b6050 <+48>: bl 0x1025b63a0 ; symbol stub for: objc_msgSend
...
我们知道OC调用方法的本质就是消息发送,所以我们看到汇编里调用的objc_msgSend
方法。objc_msgSend
方法有两个参数,分别是id
和SEL
类型,所以我们查下寄存器,这两个值分别是什么:
我们通过动态调试,很容易就能发现是Person
调用了person
方法。但如果静态分析 的话,只能自己算地址,然后找到对应的调用对象和方法名,不过好在有很多静态分析的工具。目前推荐两个Hopper Disassembler v4
和IDA
,IDA
非常贵,买断的话要1w块钱,Hopper
可以试用,如果只是研习的话,还是用Hopper
吧。
我们看下Hopper
的试用:
工具自动帮我们标明了adrp到的地址到底指的是什么。
alloc
、init
的优化
我们跳转到刚才person
方法的函数内部:
我们发现alloc
、init
的方法并不是想象中的消息发送,而是直接优化成了一个objc_alloc_init
的函数,这个是在iOS 11
以后才优化的,以前还是调用的objc_msgSend
。
block
在汇编中的展示
我们知道block
有3种:
-
NSGlobalBlock:放在常量区的代码块。如果
block
不包含外部变量(不包括全局变量和静态变量,因为全局变量和静态变量在编译时期就能确定相对固定的地址),那么会生成NSGlobalBlock
。 -
NSStackBlock:放在栈区的代码块。如果
block
包含外部变量,那么会生成NSStackBlock
-
NSMallocBlock:放在堆区的代码块。如果
NSStackBlock
调用copy
函数,那么会将NSStackBlock
拷贝到堆区,生成NSMallocBlock
。在ARC环境下,只要NSStackBlock
被强引用了,那么编译器自动帮你拷贝到了堆区,所以我们平常使用的block
大多数都是NSMallocBlock
。
我们看下从源码中看到block
的结构:
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
typedef void(*BlockInvokeFunction)(void *, ...);
接下来我们从汇编的角度探索下3种block
,以及逆向比较关注的如何找到block
的实现,也就是上方结构中的block
。
NSGlobalBlock
汇编构成
我们写一个简单的block
:
void(^block)(void) = ^(void) {
NSLog(@"block");
};
我们动态调试一下:
我们可以看到,这个block是一个__NSGlobalBlock__
。
接下来我们找下block
的实现,根据block
的结构看,invoke
在内存中处于17-24字节处,我们打印下内存看下:
我们反汇编找到了NSlog
打印,说明我们没有找错地方。
我们同样用汇编工具看下Hopper
看下:
工具直接帮我们标记出来是字面量(常量)block
,我们可以直接点注释跳转过去:
我们根据block
的结构,找到对应的实现invoke
,再次双击进去看下:
我们可以找到block
的实现,打印了block
字符串。
NSStackBlock
汇编构成
我们给刚才的block
里添加一个局部变量:
int a = 10;
void(^block)(void) = ^(void) {
NSLog(@"block--%d", a);
};
我们动态调试下汇编:
我们看到adrp
的3次,分别为block
的isa
、invoke
、descriptor
,然后
str
到了栈上,换而言之,汇编代码在栈上生成了一个block
。我们打印了isa
所在的地址(sp寄存器 + 0x8),内存内容正好能和block
的结构对上,而且从isa
中也可以看出,这个block
是__NSStackBlock__
。
我们瞟一眼反汇编工具Hopper
:
我们可以直接拿到block
的实现,所以如果我们实操反汇编别人的APP,那么直接从这边跳转看block
的实现就行了,不用看block
的完整结构了。
我们对比下前面的NSGlobalBlock
,NSGlobalBlock
在编译完直接存放在可执行文件内的,所以我们拿该block
,直接在常量区拿block
的首地址就行了。而NSStackBlock
类型的block
需要在栈上生成,但是block
结构中的有些变量的值也是存放在可执行文件内的。
NSMallocBlock
汇编构成
我们继续跑刚才的汇编代码,会发现一个函数objc_retainBlock
:
我分别打印了这个函数的参数与返回值,参数根据地址值知道就是刚才的NSStackBlock
,而函数的返回值得到了NSMallocBlock
,而且我们看到他们的函数实现invoke
地址是一样的。
这里之所以调用了objc_retainBlock
,是因为被栈引用了一下,我们可以看到stur x0, [x29, #-0x20]
,把返回值NSMallocBlock
的首地址存到了栈上,所以基本上我们平时写代码拿到的block
都是NSMallocBlock
。
我们看下源码中函数objc_retainBlock
到底干了点什么:
//
// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
//
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
objc_retainBlock
中调用了_Block_copy
函数,但我们看不到底层_Block_copy
做了什么。根据注释,我们了解到_Block_copy
是创建一个基于堆的块的副本或简单地添加一个引用到现有的一个,而且必须要和Block_release
成对使用。
但是我们在汇编代码中并没有看到Block_release
的调用,那么是如何销毁堆上的block
的呢?其实我们可以看到汇编代码最后调用了objc_storeStrong
,我们看下这个方法干了什么:
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
这个函数把新的对象obj
赋给变量*location
,并且obj
引用计数加一(objc_retain),而老的对象prev
引用计数减一(objc_release),而刚才汇编代码中新的变量obj
是:
0x100b71ffc <+116>: mov x9, #0x0
0x100b72000 <+120>: mov x1, x9
#0x0
是nil
,新的obj
是nil
,从而达到了block
的销毁。
block
在逆向中的实用
判断一个block
的参数及返回值,可以查看block
的签名,如何查看签名,还是看block
的结构:
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
我们可以看到签名signature
,在结构体Block_descriptor_3
中,Block_descriptor_1
、Block_descriptor_2
、Block_descriptor_3
在内存中是紧紧靠在一起的,所以找到Block_descriptor_1
就能拿到Block_descriptor_3
,而Block_descriptor_1
在Block_layout
是一个指针,寻址过去就能拿到了。
拿到了signature
后,我们可以靠lldb分析:
po [NSMethodSignature signatureWithObjCTypes:"v16@?0@8"]
最后可以查看官网的Type Encodings来看具体是什么类型的参数或者返回值