Swift - 类与结构体(下)

1. struct结构体方法调用探究
  • 探究方法调用的过程,我们知道在 OC 中,调用一个方法的本质是消息传递,底层通过 objc_msgSend函数去查找方法并调用。而 Swift是一门静态语言,没有运行时的机制,那原生的 Swift 方法又是如何调用的呢?
    我们通过汇编先来看一下,调用结构体的方法时,底层是如何调用的。代码如下:
    image.png

汇编代码分析方法调用的过程


image.png

可以发现,在 Swift 中,调用一个结构体的方法是直接拿到函数的地址直接调用,包括初始化方法,没有 OC 那么复杂的流程。
需要注意的是,结构体的类方法调用也和实例方法的调用一样,都是直接拿函数的地址调用。在 Swift 中声明一个类方法需要在 func 前加上 static 关键字。
Swift 是一门静态语言,许多东西在运行的时候就可以确定了,所以才可以直接拿到函数的地址进行调用,这个调用的形式也可以称作静态派发。

2. class类方法调用探究
  • 新建一个 Swift 项目,使用真机运行


    image.png
  • 添加断点,跟进汇编
    image.png

    blr 指令是跳转到某地址(无返回),也就是那么这个过程中的 x8,x9 寄存器存的值就是函数的地址。
//第 33行。
0x100ccef18 <+124>: ldr    x8, [x0]

这句代码的意思是,取 x0 的地址,存到 x8,注意,这里是取 x0 地址的开始,往后算 8 个字节。那么这个时候 x8 的地址是谁的地址呢,是 metadata 的地址。
那咋就知道是 metadata 呢,把断点打在第 34 行,重新运行后,读取 x8 的值:

image.png

那此时 x8 就是 metadata 的地址,接下来断点走到第 37 行在 lldb输入si跳进去。42,47 行的代码一样的操作,看看是不是就是对应 func1,func2,func3方法。
image.png

确实是 func1,func2,func3方法。虽然也是拿到函数的地址调用,但是很明显,系统是通过拿到 LGTeacher 的实例对象,并且拿到 metadata 的地址后,通过内存平移的方式,拿到函数地址再进行调用。
函数的地址是连续存储的,不像 OC,是存放在无序的哈希表里。那么,函数的地址存放在哪里呢?

  • 虚函数表的引入,生成 ViewController.sil 文件后,我们打开文件,看到文件的最底部。
swiftc -emit-sil -target x86_64-apple-ios13.5-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ViewController.sil

生成 ViewController.sil 文件后,我们打开文件,看到文件的最底部。

image.png

sil_vtable 是啥,根据感觉翻译的话,vtable 应该是虚表或者叫虚函数表,在 C++ 类的函数是放在虚函数表中的,那么 Swift 类的方法就是会不会存储在 vtable 里面呢。

  • 源码查找虚函数表
    swift 类的本质,它里面有一个 metadatametadata 里有一个成员变量,这个成员变量应该是这样:
var typeDescriptor: UnsafeMutableRawPointer

这个成员量存放的是对自己的描述,类、结构体、枚举内部都有这个成员变量。
找到源码中typeDescriptor 的定义,查找流程为:

找到 HeapObject
HeapObject 中找到 HeapMetadata
继续跟进,HeapMetadataTargetHeapMetadata的别名。
找到 TargetHeapMetadata 结构体。

那么找到 TargetHeapMetadata 后其内部有一个成员变量,如下:

TargetSignedPointer * __ptrauth_swift_type_descriptor> Description;

image.png

此时,发现 Description 是一个为 TargetClassDescriptor 的类,并且继承了一堆的东西。
image.png

其中的结构可以去慢慢对比,对比出来的结果,TargetClassDescriptor 大概长这样:

class TargetClassDescriptor {
    var flags: UInt32
    var parent: UInt32
    var name: Int32
    var accessFunctionPointer: Int32
    var fieldDescriptor: Int32
    var superClassType: Int32
    var metadataNegativeSizeInWords: UInt32
    var metadataPositiveSizeInWords: UInt32
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32
    var Offset: UInt32
    // var size: UInt32
    //V-Table
}

那么 TargetClassDescriptor 其实有一个别名为 ClassDescriptor,其定义如下:

using ClassDescriptor = TargetClassDescriptor;

我们全局搜索 ClassDescriptor,找到了GenMeta.cpp 文件,通过名字,可以猜到 GenMeta.cpp 文件的代码就是生成元数据的地方。

image.png

我们进入到 GenMeta.cpp 文件,直接定位到 ClassContextDescriptorBuilder 这个类-内容的描述建立者,这个类就是创建 metadataDescriptor 的类。
image.png

在类中找到 layout 这个方法:

void layout() {
    super::layout();
    addVTable();
    addOverrideTable();
    addObjCResilientClassStubInfo();
    maybeAddCanonicalMetadataPrespecializations();
}

在里面调用了 super::layout(),我们来看一下父类的实现:

void layout() {
    asImpl().computeIdentity();

    super::layout();
    asImpl().addName();
    asImpl().addAccessFunction();
    asImpl().addReflectionFieldDescriptor();
    asImpl().addLayoutInfo();
    asImpl().addGenericSignature();
    asImpl().maybeAddResilientSuperclass();
    asImpl().maybeAddMetadataInitialization();
}
void layout() {
    asImpl().addFlags();
    asImpl().addParent();
 }

到这里,基本就和 TargetClassDescriptor 类的成员变量对应起来了。那我们的重点当然还是 addVTable 函数,这个函数就是将 Swift 类的方法添加到虚函数表,其实现如下:

void addVTable() {
    LLVM_DEBUG(
    llvm::dbgs() << "VTable entries for " << getType()->getName() << ":\n";
    for (auto entry : VTableEntries) {
        llvm::dbgs() << "  ";
        entry.print(llvm::dbgs());
        llvm::dbgs() << '\n';
    }
    );

    // Only emit a method lookup function if the class is resilient
    // and has a non-empty vtable, as well as no elided methods.
    if (IGM.hasResilientMetadata(getType(), ResilienceExpansion::Minimal)
    && (HasNonoverriddenMethods || !VTableEntries.empty()))
    IGM.emitMethodLookupFunction(getType());

    if (VTableEntries.empty())
    return;

    auto offset = MetadataLayout->hasResilientSuperclass()
       ? MetadataLayout->getRelativeVTableOffset()
       : MetadataLayout->getStaticVTableOffset();
    B.addInt32(offset / IGM.getPointerSize());
    B.addInt32(VTableEntries.size());

    for (auto fn : VTableEntries) {
        emitMethodDescriptor(fn);
    }
}

前面的代码可以先不用管,我们注意看最后的几行。计算 offset 之后,调用了 addInt32 函数,这个函数就是去计算添加方法到虚函数表的偏移量,具体可以看源码实现。最后 for 循环,添加函数的指针。
这个偏移量是 TargetClassDescriptor 这个结构中的成员变量所有内存大小之和,并且在最后还拿到了 VTableEntries.size()
也就意味着,虚函数表的内存地址,是 TargetClassDescriptor 中的最后一个成员变量,并且,添加方法的形式是追加到数组的末尾。所以这个虚函数表是按顺序连续存储类的方法的指针。

  • Mach-O 文件查找方法内存结构地址
    在运行项目之后,会生成一个可执行文件,用 MachOView 这个工具打开它。
    image.png

swift5_types 这里存放的是结构体、枚举、类的 Descriptor,那么我们可以在 swift5_types 这里找到类的 Descriptor 的地址信息。
前面的四个字节 90 FB FF FF 就是 LGteacherDescriptor 信息,那用 90 FB FF FF 加上前面的 0000BBDC 得到的就是 Descriptor 在当前 Mach-O文件的内存地址。
它们怎么相加呢,iOS 属于小端模式,所以 28 FC FF FF 要从右边往左读。也就是:

FFFFFC28 + 0000BC4C = 0x10000B874

image.png

那么 0x100000000Mach-O 文件中虚拟内存的基地址,如图所示。
image.png

我们用 0x10000B874 - 0x100000000 = B874 就是LGTeacher在整个 Data 区的内存地址。我们找到 TEXT, const
image.png

如图所示,这个就是B874 的首地址,也是意味着,它后面的数据是 TargetClassDescriptor 的数据,所以我们可以在这里拿到 LGTeacher 的虚函数表 - LGTeacher 方法的地址。
计算 TargetClassDescriptorVTable 前面的数据大小,求得偏移量。一共 12 个 4 字节(48字节)的成员变量,12 个四字节的成员变量再加上 size(4字节)得到 52 字节,在往后的 24 字节就是 func1,func2,func3 方法的结构地址(一个函数地址占 8 字节)。如图所示:

image.png

如图中所示,B8A8func1 结构在Mach-O 文件的地址。

  • 验证 Mach-O 分析的函数地址是否和程序运行的一致
    ASLR 是一个随机偏移地址,这个随机偏移地址的目的是为了给应用程序一个随机内存地址。

我们打一个断点,程序运行起来后,输入LLDB 命令: image list。如图所示:

image.png

image list 是列出应用程序运行的模块,我们找到第一个,其内存地址为 0x0000000100a70000,那我们可以把这个地址当作应用程序的基地址。
接下来在源码中找到这么一个结构体。TargetMethodDescriptorSwift 的方法在内存中的结构,Impl不是真正的 imp,而是相对指针 offset

struct TargetMethodDescriptor {
    /// Flags describing the method.
    // 占 4 字节,Flags 标识是什么方法。
    MethodDescriptorFlags Flags;

    /// The method implementation.
    // 不是真正的 imp,这里存储的是相对指针,offset。
    TargetRelativeDirectPointer Impl;

    // TODO: add method types or anything else needed for reflection.
};

到这里,TargetMethodDescriptor 结构体的地址就可以确定了,那么要找到函数地址,还需要偏移Flags + Impl,得到的就是函数的地址。 综合以上的逻辑开始计算:

// 应用程序的基地址:0x0000000100a70000,func1 结构地址:`B8A8,Flags:0x4,offset:94 B2 FF FF 
// 注意!小端模式要从右往左,所以为 FF FF B2 94
0x100a70000 + B8A8 + 0x4 + FF FF B2 94 = 0x200A76B40

// 接下来需要减掉 Mach-O 文件的虚拟地址 0x100000000,得到的就是函数的地址。
0x200A76B40 - 0x100000000 = 0x100A76B40 

打开汇编调试,读取汇编中 func1 的地址,验证 0x100A76B40是否就是 func1 的地址。如图所示:

image.png

通过汇编验证0x100A76B40 就是 func1 的函数地址。到这里就完全验证了 Swift 类的方法确实是存放在 VTable - 虚函数表里面的。

你可能感兴趣的:(Swift - 类与结构体(下))