本文主要理解OC对象反汇编,以及block常见类型的反汇编
OC反汇编
创建一个Person类,并在main函数中初始化一个Person对象
@interface Person : NSObject @property(nonatomic, copy) NSString *name; @property(nonatomic, assign) int age; + (instancetype)person; @end @implementation Person + (instancetype)person{ return [[self alloc] init]; } @end <!--main.m中--> int main(int argc, char * argv[]) { Person *p = [Person person]; return 0; }
运行,查看其汇编代码
1、静态调试
通过adrp+add获取地址,分别读取x0,x1
- 读取x0,读出来是Person:
x 0x100c68eb0
+po 0x0100c68f98
读取x1,读取出来是person方法:x 0x100c68e88
+ p (SEL)0x01c019aef5
2、动态调试
通过一步一步执行汇编,来验证x0、x1是否如静态调试的结果一致?
通过调试发现,是一致的,其实这里的x0、x1
就是 objc_msgSend
的隐藏参数(id self,SEL _cmd)
下面我们继续调试汇编
- 点击step into,直接进入
[Person person]
方法(注意:这里不同iOS版本,多看到的汇编代码是有所区别的)
- 从这里看到ios13.4系统的alloc、init并不会走objc_msgSend
ios11版本中,可以看到objc_msgSend,其本质是在调用init方法
动态调试进行验证,结果如下所示,是一致的
查看此时的x0,已经是一个实例对象,因为alloc开辟了内存,已经分配了空间,具体的内部实现可以查看这篇文章iOS-底层原理 02:alloc & init & new 源码分析
- 疑问:为什么版本不同,调用不一样呢?
- 在不同的版本下,系统在运行时是不一样的。因为系统对alloc 、init进行了优化
- 接着往下看,点击step out 跳出[Person person],此时返回值在x0中
执行到bl ... objc_storeStrong
,objc_storeStrong是OC中用strong修饰的对象底层都是调用这个函数,详情可以看这篇文章iOS-底层原理 10:strong©&weak底层分析 以及 方法签名和attribute简写含义。
疑问:我们此时并没有使用strong修饰?:此时的局部变量p在此时就相当于一个强引用,是默认的。且这个方法执行完成后,相当于销毁p
查看此时的x0、x1
,相当于objc_storeStrong(&p,nil)
,将nil进行retain,将nil等于p(即 p=nil),p进行释放
- 查看
objc_storeStrong
源码 - 目的:对一个strong修饰的对象进行retain +1,对一个老的对象进行release
- 为什么是指针? 因为函数是值传递,而函数内部需要修改p的值
/* - id *location 指向对象的指针 本质上是 &p(即局部变量地址) - id obj 对象 目的:对一个strong修饰的对象进行retain +1,对一个老的对象进行release 为什么是指针? 因为函数是值传递,而函数内部需要修改p的值 */ void objc_storeStrong(id *location, id obj) { //prev 相当于p ,因为location是 &p id prev = *location; //第二个参数 == 第一个参数,直接return if (obj == prev) { return; } //retain+1 objc_retain(obj); //修改p的值,指向第二个对象 *location = obj; //释放老对象 objc_release(prev); }
相当于
Person *p = p1; p = p2;//此时p1释放,p2retain+1
所以以上汇编中的objc_storeStrong(&p,nil)
的实现代码如下
objc_storeStrong(&p,nil){ id prev = p; if nil == p{ return; } objc_retain(nil); p = nil;//指针指向nil objc_release(p);//释放堆空间 }
下面来进行动态验证,发现Person对象指向nil
[[self alloc] init] 优化过程
- 在最初的版本(iOS9)中,相当于两次消息发送
objc_msgSend
- iOS11版本 是一次消息发送
objc_alloc + objc_msgSend
- iOS13.5.1以上版本,已经没有objc_msgSend,而是
objc_alloc_init
以上是LLDB动态调试Person *p = [Person person]; //objc_msgSend x0,x1
通过工具看复杂的OC代码
在上述OC代码的基础上增加一些代码,然后再来静态分析
int main(int argc, char * argv[]) { Person *p = [Person person]; //objc_msgSend x0,x1 p.name = @"CJL"; p.age = 18; return 0; }
- CMD + B 编译程序,生成mach-o文件,并找到该文件
- 通过Hopper反汇编mach-o文件,main函数的分析如下
双击objc_cls_ref_Person
,查看p的地址,是000000010000ce88
,是在Data段
通过MachOView
打开mach-o分析,查找000000010000ce88
,与Hopper中的显示是一致的
双击@selector(person)
,查看person方法的反汇编
双击0x10000cc68
双击“person”
,地址为 0x10000752a
在mach中查找0x10000752a
,所有方法的name都在CString
中
Block反汇编
定义一个block
int main(int argc, char * argv[]) { void(^block)(void) = ^(){ NSLog(@"block"); }; block(); return 0; }
反汇编分析block的目的是想快速定位block的invoke
,因为invoke中是实现代码,以下是block的汇编代码
查看x0
是什么?:是一个__block_literal_global
,是一个全局静态block
(即block不引用block外部变量
,在编译时期
就可以确定内存
的分配等操作,存在于可执行文件的常量区
),其他详情也可查看iOS-底层原理 30:Block底层原理这篇文章
- 以下是源码中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 };
然后动态调试查看block的内存结构
是否可以通过hopper查看 adrp + add 是一个block?
答案是可以的
双击___block_literal_global
双击0x0000000100006838
,查看invoke
双击0x0000000100008008
,查看descriptor,和Block的源码结构类似
如果block引用了外部变量呢?
定义一个block,其中block引用了外部变量,查看此时的汇编代码
int main(int argc, char * argv[]) { int a = 10; void(^block)(void) = ^(){ NSLog(@"block -- %d", a); }; block(); return 0; }
1、lldb调试
- 以下是代码的汇编
验证是否是block的isa指针
adrp x10, 2
获取指针地址ldr x10, [x10]
:取值
- 查看此时block的内存,找到invoke(由于invoke是代码实现,所以需要由
dis -s
(将代码的汇编打印出来)查看)
2、静态分析
- 通过hopper静态分析如下,以下是main函数的反汇编
双击___main_block_invoke
,跳转至invoke的具体实现(并没有在main函数中,是单独的实现)
双击___block_descriptor_36_e5_v8�?0l
,是一个单独的描述
总结
- [[self alloc] init] 优化过程
- 在最初的版本(iOS9)中,相当于两次消息发送
objc_msgSend
- iOS11版本 是一次消息发送
objc_alloc + objc_msgSend
- iOS13.5.1以上版本,已经没有objc_msgSend,而是
objc_alloc_init
- 反汇编分析方式:
- 通过
LLDB
动态调试 - 通过
Hopper + MachOView
静态分析