Swift(十二)-类型属性&MachO的属性查找

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战

类型属性

我们在之前提到的属性实际上都是实例属性,在Swift中与之对应的还有类型属性又称为类属性。实例属性由类的实例调用,类型属性则直接由来调用;类型属性使用static或者class关键字来声明。使用static关键字声明的属性也被称为静态属性;对于类的计算属性,如果允许子类对其计算方法进行覆写,则需要使用class关键字来声明;

  • 类型属性其实就是一个全局变量
  • 类型属性只会被初始化一次

类型属性分析

那么如何声明一个类型属性呢?我们给一个正常的属性age添加上static关键字,它就变成了类型属性:

class Person {
    static var age: Int = 18
}
复制代码

我们访问类型属性时,可以直接使用类名进行访问:

image.png

那么类型属性与我们正常的属性有什么区别呢?我们可以通过SIL文件分析类型属性:

image.png

SIL文件中我们看到,类型属性还是一个存储属性,但是比起一般的存储属性多了一个static,而且age属性变成了一个全局变量;通过main函数中的实现:

image.png

根绝注释,我们看到Person.age.unsafeMutableAddressor其实是在访问age属性的内存地址,此时调用的是函数s4main6PersonC3ageSivau,我们定位到此函数的实现:

image.png

在该函数中,通过函数s4main6PersonC3age_WZ拿到了一个内存地址,经过指针转换,最终返回了age属性的内存地址,我们看一下函数s4main6PersonC3age_WZ的具体实现:

image.png

这里构建了一个Int类型的结构体添加到内存中(存放到全局变量age中),也就是在初始化全局变量age

类型属性中gcd的运用

我们在上文中SIL文件中看到在获取age属性的内存地址时,通过builtin "once"调用了s4main6PersonC3age_WZ函数,那么s4main6PersonC3age_WZ是如何确保只调用了一次呢?我们将代码降级为IR代码,在IR代码中查看该函数的实现:

image.png

IR代码中使用到了s4main6PersonC3ageSivau,根据命令行,我们可以看到其就是Person.age.unsafeMutableAddressor:

image.png

并且,其最终是通过swift_once调用的,我们在Swift源码中可以找到swift_once的实现如下:

image.png

在该函数中,最终调用了gcd中的dispatch_once_t来确保全局变量age属性只被初始化一次;

单例类的实现

那么,我们结合staticlet就能在Swift中实现单例类:

image.png

当然这样还不完美,我们需要将指定初始化器私有化,以确保在外部只能通过shared来访问:

image.png

属性在MachO中的位置

我们在之前已经分析过,Swift类中Metadata的数据结构如下:

struct Metadata{
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer
    var iVarDestroyer: UnsafeRawPointer
}
复制代码

其中的typeDescriptor存储在MachO文件中的__swift5_types中,其数据结构如下:

struct TargetClassDescriptor {
    ContextDescriptorFlags Flags;
    TargetRelativeContextPointer<Runtime> Parent;
    TargetRelativeDirectPointer<Runtime, const char, /*nullable*/ false> Name;
    TargetRelativeDirectPointer<Runtime, MetadataResponse(...),
                              /*Nullable*/ true> AccessFunctionPtr;
    TargetRelativeDirectPointer<Runtime, const reflection::FieldDescriptor,
                              /*nullable*/ true> Fields;
    TargetRelativeDirectPointer<Runtime, const char> SuperclassType;
    uint32_t MetadataNegativeSizeInWords;
    uint32_t MetadataPositiveSizeInWords;
    uint32_t NumImmediateMembers;
    uint32_t NumFields;
    uint32_t FieldOffsetVectorOffset;
}
复制代码

而在该数据结构中Fields存储我们的属性信息;我们通过MachO文件来验证;

首先,我们在Swift源码中找到FieldDescriptor的结构信息:

class FieldDescriptor {
  const RelativeDirectPointer<const char> MangledTypeName;
  const RelativeDirectPointer<const char> Superclass;
  const FieldDescriptorKind Kind;
  const uint16_t FieldRecordSize;
  const uint32_t NumFields;
  const FieldRecords<FieldRecord>
}    
复制代码
  • MangledTypeName:混写之后的类型名称;
  • NumFields:当前属性个数;
  • FieldRecords:属性的信息

FieldRecords是一个数组,其中的元素为FieldRecord,其数据结构如下:

class FieldRecord {  
  const FieldRecordFlags Flags;
  const RelativeDirectPointer<const char> MangledTypeName;
  const RelativeDirectPointer<const char> FieldName;
}
复制代码
  • MangledTypeName:属性的类型信息;
  • FieldName:属性的名称;

基于以上数据结构我们就能够通过MachO文件找到属性在MachO中的位置;

image.png

我们将以上代码生成MachO文件:

image.png

此处存储的是TargetClassDescriptor,我们通过计算0xFFFFFEC4 + 0x3F5C = 0x100003E20,我们减去虚拟地址之后,得到0x3E20,我们在MachO中定位到该地址:

image.png

根据TargetClassDescriptor的数据结构,我们想要找FieldDescriptor需要向后偏移4*4字节:

image.png

image.png

此处开始存放的就是FieldDescriptor的偏移地址,我们通过0x3E30 + 0x00000104 = 0x3F34,我们定位到该偏移地址在MachO中的位置:

image.png

此处存放的就是FieldDescriptor结构体的内容,根据其数据结构,我们可以分析得到想要到找FieldRecords需要向后偏移16字节

image.png

后边这些连续的存储空间就是我们需要查找的FieldRecords的结构体;根据FieldRecords的数据结构可以知道0xFFFFFFDF偏移地址是第一个属性的名字,我们通过计算0xFFFFFFDF + 0x3F44 + 0x8(0x4F44向后偏移的8字节) = 0x100003F2B,我们在MachO中定位到3F2B

image.png

我们在此处找到ageage1;其中61676531分别对应age1ASCII码

猜你喜欢

转载自juejin.im/post/7061518341994709005