美文网首页
swift中 class与struct的方法调用

swift中 class与struct的方法调用

作者: 蜗牛炒饭 | 来源:发表于2021-12-31 21:44 被阅读0次

    swift为了实现快这么一个终极目标。在许多地方做了大量的优化。简直可以说是集现代编程语言之长。而这一点在swift中的方法调用尤为突出。我们来探究一下swift中的方法调用。

    swift中函数调用方试

    swift语言中总共有三种方法调用方式:
    1.通过内存地址直接调用
    2.通过v-table这么一个结构类似数组的函数表调用
    3.就是我们非常熟悉的send_msg()消息派发。
    他们的调用效率是1>2>3。动态性则是3>2>1。然后swift在任何时候都会优先的尽量使用内存直接调用,不能够使用内存地址直接调用的时候使用函数表调用。那么什么时候不能够使用内存直接调用了,很简单函数可能会被重写时就不能够使用内存直接调用了,而第三种消息表派发就是需要使用到runtime的时候会使用

    stuct的方法调用

    image.png

    stuct一把都是采用的函数地址调用。在汇编代码中bl表示跳转到地址。这就效率最高的函数调用方式不用查找,撸起地址直接干。但是上面我们看的func2方法添加了一个mutating的关键字,要知道它的不同我们进入到sil中,能更详细的看的swift中做了什么

    struct Person {
      func func1()
      mutating func func2()
      init()
    }
    .....
    // Person.func1()
    sil hidden [ossa] @$s14ViewController6PersonV5func1yyF : $@convention(method) (Person) -> () {
    // %0 "self"                                      // user: %1
    bb0(%0 : $Person):
      debug_value %0 : $Person, let, name "self", argno 1 // id: %1
    
    .....
    // Person.func2()
    sil hidden [ossa] @$s14ViewController6PersonV5func2yyF : $@convention(method) (@inout Person) -> () {
    // %0 "self"                                      // user: %1
    bb0(%0 : $*Person):
      debug_value_addr %0 : $*Person, var, name "self", argno 1 // id: %1
    

    我截取了部分关键的sil代码。在这段代码里面 func1()和func2()第一个不同点在@convention(method) (@inout Person) 中,func2()方法多了一个@inout,第二个不同点在Person, var, name "self", argno 1。里面的Person取得是地址。
    inout 的作用很简单可以让let 定义的变量可以修改

    func func3(age:inout Int){
            age = 20
            print(age)
        }
    

    我们都知道在swift是不可以直接修改参数值的如果你要强行修改就会报这个错


    image.png

    但是如果我添加了inout关键字就可以修改这个let定义的变量了。调用inout修饰的参数的方法。参数需要传地址。像这样调用func3(age: &leoAge)。
    那么在sil中func2()方法这里swift隐式的添加@inout的目的也就很明显了。意思就是可以修改传递进来的Person的参数。也就是struct本身。这就是mutaiting修饰的方法可以修改struct变量的原因。
    struct中只有内存直接调用与函数表调用两种调用方式。因为要是使用消息派发必需继承自NSObject对象。

    函数表调用

    函数表的方式调用函数,在下面的截图中我们可以看到func2()的blr后面跟的不是函数地址而是一个变量。那这个变量swift是怎样存储的了?我们知道它是怎样存储的就知道了它是怎样调用的了。我们尝试着来找到函数表中函数地址的存储方式。

    image.png

    下面我们需要查看macho文件格式,我们先简单介绍一下macho文件的格式
    Macho文件格式是这样的


    image.png

    它主要分为三个部分:1.Header 2.Load commands 3Data区
    header中表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信 息,文件头信息影响后续的文件结构
    Load commands是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表 等。
    Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据 的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类 型。在装载在内存中时,也是根据 Segment 做内存映射的。

    我们在看一下swift的类的构成,有助于我们在machoc中查找我们需要的信息。swift中类都有一个元数据结构,这个数据结构是一个Metadata 的struct。通过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,不管是Class、Struct、Enumd都有Descriptor.用来描述它自身。类的描述对象如下

    struct 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
    ...
    }
    

    我们都知道struct是值类型。如果我们现在能找到typeDescriptor这个指针地址,通过指针偏移就能访问到struct中值类型的变量的值或者引用类型的指针。那么v-table这个函数表我们推测它与位于size的后面。因为它如果不在这个地方我们考虑不到它能存放在什么地方了。下面我们来验证一下。如果能找到就说明确实是这样的。

    1.我们用machoview打开我们编译后的可执行文件。

    image.png

    我们直接定位到数据段中的__TEXT,__swift5_types中。__TEXT,__swift5_types就是存放TargetClassDescriptor的地方。pFile是虚拟内存地址,我们来计算一下前面8位的值。0x0000BBDC+0x9CFBFFFF,注意这个地方的0x9CFBFFFF是小段地址所以应该是0xFFFFFB9C。我们得到的值是0x10000B778。然后这个值我们需要减去程序本身虚拟内存得地址,这个值在Load Commands中的LC_SEGMENT_64(__TEXT)中。


    image.png

    可以看到这个值是0x100000000。我们现在得到的值是0x0000B778,我们到data段中,在Section64(__TEXT,__const)中我们能看到0x0000B778位于的内存区间。


    image.png
    那么B778应该就是在0xB770偏移8位,那么0x80000050就应该是我们typeDescriptor的地址。

    swift 方法的结构

    image.png

    struct TargetMethodDescriptor就是swift中函数的数据结构。其中第一个flags描述了函数的类型。


    image.png

    然后我们根据TargetClassDescriptor的结构在来偏移13个字节我们在地址0xB7B0找到了我们的TargetRelativeDirectPointer的偏移值。0x00000010ffffc5cc,根据TargetMethodDescriptor的数据结构,前面4个字节存放的是方法类型所以我们的函数本身的impl这个地方的偏移值应该是0xFFFFC5CC。我们当前程序的ASLR的地址为0x1025d0000

    0xFFFFC5CC+0x0000B7b0 = 0x100007d7c
    0x100007d7c+0x1025d0000 = 0x2025D7D7C
    0x2025D7D7C - Vm地址(0x10000000) = 0x1025D7D7C
    下面看代码

    image.png

    我们我们断点到leo.fun2()这里查看汇编代码,blr x8 其x8寄存器存放的就是func2()的地址。我们使用register read x8读取x8的地址 0x00000001025d7d7c。与我们计算得到的地址是一样的说明我们上面的推论是正确的。

    消息派发

    class Person:NSObject{
        
        func func2(){
            
        }
        
        @objc dynamic func func3(){
            
        }
        
        @objc func func4(){
            
        }
        
        dynamic func func5(){
            
        }
    }
    
    
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            let leo = Person()
            leo.func2()
            leo.func3()
            leo.func4()
            leo.func5()
        }
    }
    
    

    我在Person中又添加了func3()、func4()、func5()方法。同时对他们分别使用@objc 、dynamic修饰
    然后我们看sil代码


    image.png

    在Person的sil_vtable中func3()方法没有添加到vtable这个函数表中。因为func3()使用了@objc + danamic 修饰。


    image.png
    在sil中 这个地方就很明显的标识出来了他们调用方法的不同。func3()使用的是objc_method,而虚函数表使用的是class_method.
    swift中的消息派发主要是为了兼容runtime的机制。所以使用消息派发的swift方法也就可以使用我们runtime的黑魔法了。

    相关文章

      网友评论

          本文标题:swift中 class与struct的方法调用

          本文链接:https://www.haomeiwen.com/subject/uxlbqrtx.html