前言
本文旨在借助Hopper、MachOView工具,窥探类对象信息在内存中的存储内容及存储位置。包括Swift在Mach-O文件中虚函数表的存储结构等,并分析Swift与OC在函数调用方面的差异化。
OC类信息存储
1. 获取Hopper、MachOView对应的地址
新建一个工程OCTestDemo,创建一个继承自NSObject的类,在类中实现几个方法,并声明一个属性,仅此而已,前期工作就已经做好了。
通过runtime方法NSClassFromString(@"GZTestClass"),获取到类对象。也可以通过创建实例对象,再通过object_getClass拿到类对象,两者的内存地址是一样的。
通过最新的objc源码看,NSClassFromString这个方法拿到的就是类对象的地址。
通过LLDB指令:image list -o -f qrep 项目名,获取项目的偏移地址。
偏移过的类对象地址:0x1015b8630
偏移地址:0x15af000
MachOView对应地址:0x1015b8630 - 0x15af000 - 0x100000000 = 0x9630
不太熟悉的同学,可以自行搜下虚拟地址、偏移地址、真实地址三者之间的关系。
2. MachOView逐帧查找
拿到MachOView对应地址对应的地址之后,通过Products找到项目可执行文件,显示包内容找到Mach-O文件,并将Mach-O文件导入到MachOView中。
注:在AppStore下载的应用,可以通过砸壳,同样获取到项目的MachO文件,你的资源、数据等都暴露在这里,所以需要对Mach-O、资源等进行加密处理,具体加密实现可以艾特我,这里不过多介绍。
找到我们计算好的地址0x9630,可以发现:
类对象详细的信息就在存储在__DATA,__data段内。
值得一提的是,类对象的地址存储在上面的__DATA,__CONST,__objc_classlist段内。(遍历**__objc_classlist段****,就可以拿到所有的类信息数据)**
**
**
3. Hopper再次验证
通过Hopper可以再次验证,0x9630就是我们类对象信息的首地址。
4. 类对象的内存地址
我们已经找到了类对象的内存地址,那么如何找到类对象内部方法列表的内存地址呢。
那么我是借助了58的一个库WBBlades,该库对类信息定义了这样一个结构体(在第3步Hopper验证时也可以发现,data是第5个成员变量),那么从它的定义可以看出,方法列表是存储在第五个成员变量中,也就是第5个8字节。
struct class64
{
unsigned long long isa;
unsigned long long superClass;
unsigned long long cache;
unsigned long long vtable;
unsigned long long data;
};
5. 定位到方法列表的内存地址
那么0x9630的内存地址,第5个8字节的内存是0x100008D80,这个内存地址就存储着data数据。
找到这个内存地址,发现它存储在__DATA,__objc_const段,const只读的,那就能说明求出的这个是类信息的内存地址,对应着objc源码里面的objc_class结构体中bits通过位运算求出的class_ro_t的数据,程序编译完生成的只读的数据。
我们都知道,class_ro_t存储着方法列表、属性列表、协议列表等等,其实里面存储的东西很多。
objc源码对class_ro_t的定义:
可以看到成员变量还是挺多的,flags、start、size、reserved、name、method等等,这里不过多赘述。
58开源库WBBlades对此也有定义:
struct class64Info
{
unsigned int flags;//objc-runtime-new.h line:379~460
unsigned int instanceStart;
unsigned int instanceSize;
unsigned int reserved;
unsigned long long instanceVarLayout;
unsigned long long name;
unsigned long long baseMethods;
unsigned long long baseProtocols;
unsigned long long instanceVariables;
unsigned long long weakInstanceVariables;
unsigned long long baseProperties;
};
可以看到 name(类名)在第4个8字节,methods(方法)在第5个8字节。
那么我们就可以得出方法的内存地址:
类信息的内存地址:0x100008D80,找到这个地址。
flags:84,start:1,size:8,name(类方法名):0x1000032D3,methods:0x100008CA8
类名存储:
方法存储:
至此我们就已经获取到methods列表相关的信息了。
6. 获取impl的内存地址
上述已经得出结论:methods:0x100008CA8
借鉴58开源库WBBlades对method_t的定义:
struct method64_list_t
{
unsigned int entsize;
unsigned int count;
};
struct method64_t
{
unsigned long long name;
unsigned long long types;
unsigned long long imp;
};
可以知道impl在第4个8字节,所以impl的内存地址是0x100008CA8的第4个8字节,impl的地址就是:0x100001F40,在__TEXT,__text段,由此可以证明方法是存储在TEXT段的。
0x100001F40对应的就是机器码了,已经无法看到有用的信息了,需要对其进行反汇编才能看出来,这时候就需要借助Hopper了。
7. Hopper验证impl的内存地址
impl内存地址:0x100001F40,可以看出0x100001F40就是类信息中所有方法的列表。
至此,OC类信息的存储推导已经全部完成了,是不是很简单😄。。
Swift方法存储
1. 获取类信息真实地址
创建一个纯Swift项目,创建一个类SwiftTestClass,声明方法和变量,仅此而已。
将Mach-O文件导入到MachOView中,我们发现,Swift的Mach-O文件结构,跟OC的一样,只不过多了Swift5的段,那苹果可能是为了兼容OC的混编而做出的妥协。
需要知道,Swift的类信息内存地址是存储在__swift5_types中的,图中地址只存了4个字节,这样就节省了内存,Swift是做了优化。
那么类信息的真实地址是:文件的偏移地址+存储的地址 = 0x7CBC + 0xFFFFFE5C = 0x100007B18
找到这个地址0x100007B18,可以看到类信息内存地址是存储在TEXT_const段的,MachOView看不出什么了,需要从Hopper中查看。
反编译可以查看,很多数据已经出来了,flags、parent、name、accessorfunction指针等等。
那么Swift的函数调用,其实是类信息内存地址,调用其内部的accessorfunction指针,accessorfunction指针再去内部调取虚函数表vtable的调用顺序。
2. 窥探源码
上一步我们已经反编译出类信息的内存地址0x100007B18,发现它是一个结构体ClassDescriptor、ContextDescriptor,这两个结构体从其他三方库中没有发现对其进行定义,那就看下源码:
大概能看到 classContextDescriptor这个类里面,有添加vtable虚函数表的方法,通过flag可以判断是否有虚函数表。
总结:
本文章通过分析Mach-O文件内存地址的逐步调用,从OC创建类->编译->存储类->地址偏移->类信息存储->类信息调用等流程,以及Swift类信息的内存优化,虚函数表的地址直接调用,可见Swift对内存管理、性能提升,做了不少的工作。
OC、Swift存储结构、调用顺序流程基本结束,本文仅作为知识分享、技术研究,如有疏漏之处,敬请指正。。