iOS - Mach-O验证Swift和OC存储信息

2,294 阅读8分钟

前言

本文旨在借助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存储结构、调用顺序流程基本结束,本文仅作为知识分享、技术研究,如有疏漏之处,敬请指正。。