当前位置 博文首页 > 1024星球:Swift系列七 - 汇编分析值类型

    1024星球:Swift系列七 - 汇编分析值类型

    作者:1024星球 时间:2021-05-26 18:23

    通过汇编分下值类型的本质。

    一、值类型

    值类型赋值给varlet或者给参数传参,是直接将所有内容拷贝一份。类似于对文件进行复制粘贴操作,产生了全新的文件副本,属于深拷贝(deep copy)。

    示例:

    func testStruct() {
        struct Point {
            var x: Int
            var y: Int
        }
        
        var p1 = Point(x: 10, y: 20)
        print("before:p1.x:\(p1.x),p1.y:\(p1.y)")
        var p2 = p1
        print("before:p2.x:\(p2.x),p2.y:\(p2.y)")
        p2.x = 30
        p2.y = 40
        print("after:p1.x:\(p1.x),p1.y:\(p1.y)")
        print("after:p2.x:\(p2.x),p2.y:\(p2.y)")
    }
    /*
     输出:
     before:p1.x:10,p1.y:20
     before:p2.x:10,p2.y:20
     after:p1.x:10,p1.y:20
     after:p2.x:30,p2.y:40
     */
    

    通过上面的示例可以看出,给p2重新赋值确实没有影响到p1的值。

    1.1. 内存分析

    我们也可以通过内存看下上面示例中变量地址是否发生改变,如果生成了新的地址值,则说明是深拷贝。

    func testStruct() {
        struct Point {
            var x: Int
            var y: Int
        }
        
        var p1 = Point(x: 10, y: 20)
        var p2 = p1    
        print(Mems.ptr(ofVal: &p1))
        print(Mems.ptr(ofVal: &p2))
    }
    /*
     输出:
     0x00007ffeefbff4c0
     0x00007ffeefbff490
     */
    

    打印结果显示:p2p1的内存地址是不同的,所以修改p2不会影响p1

    1.2. 汇编分析(局部变量)

    第一步:示例代码:

    第二步:进入汇编代码后先查找立即数:

    第三步:进入p1的初始化方法中:

    第四步:继第三步finish后,继续回到之前的汇编:

    movq   %rax, -0x10(%rbp)
    movq   %rdx, -0x8(%rbp)
    movq   %rax, -0x20(%rbp)
    movq   %rdx, -0x18(%rbp)
    movq   $0x1e, -0x20(%rbp)
    movq   $0x28, -0x18(%rbp)
    

    通过上面分析得出:

    • p1的变量x内存地址:rbp-0x10

    • p1的变量y内存地址:rbp-0x8

    • 且p1的两个变量相差rbp-0x8-(rbp-0x10) = 8个字节;

    • p1的内存地址是rbp-0x10

    • 0x1e赋值给rbp-0x20的地址,和上面的rax赋值给rbp-0x20是同一个地址,并且仅仅修改了一次。

    所以,通过汇编也可以有力的证明值类型传递是深拷贝。

    扩展:%edi%esi是局部变量,将来传给形参后会变成%rdi%rsi

    1.3. 汇编分析(全局变量)

    第一步:示例代码:

    第二步:查看汇编:

    进入init方法发现和上面的1.2分析基本一致,rdi给了raxrsi给了rdx

    第三步:继续往后面看call之后的代码:

    rip就是下一条指令的地址。
    rax:10
    rdx:20
    
    0x100000ba4 <+52>:  movq   %rax, 0x664d(%rip)
    把rax给了地址:0x100000bab + 0x664d = 0x1000071f8
    
    0x100000bab <+59>:  movq   %rdx, 0x664e(%rip) 
    把rdx给了地址:0x100000bb2 + 0x664e = 0x100007200
    
    0x100000bb2 <+66>:  movq   %rcx, %rdi
    
    观察发现:rdx和rax刚好相差了0x100007200 - 0x1000071f8 = 8个字节。
    
    --------------------------------------------------------
    
    0x100000bce <+94>:  movq   0x6623(%rip), %rax
    把地址 0x100000bd5 + 0x6623 = 0x1000071f8 给了rax
    
    0x100000bd5 <+101>: movq   %rax, 0x662c(%rip)
    把rax给了地址:0x100000bdc + 0x662c = 0x100007208
    
    0x100000bdc <+108>: movq   0x661d(%rip), %rax 
    把地址 0x100000be3 + 0x661d = 0x100007200 给了rax
    
    0x100000be3 <+115>: movq   %rax, 0x6626(%rip)
    把rax给了地址:0x100000bea + 0x6626 = 0x100007210
    
    0x100000bea <+122>: leaq   -0x18(%rbp), %rdi
    
    --------------------------------------------------------
    观察发现:
    0x1000071f8就是上面的10,0x100007200就是上面的20
    就是说,
    把0x1000071f8里面的值(10)取出来赋值给了另外一块内存地址
    0x100007208;
    把0x100007200里面的值(20)取出来赋值给了另外一块内存地址0x100007210
    并且,
    0x100007210和0x100007208相差8个字节。
    

    通过上面的分析可以得出,p1的内存地址就是0x1000071f8,p2的内存地址是0x100007208。也可以证明值类型是深拷贝。

    经验:

    • 内存地址格式为:0x486f(%rip),一般是全局变量,全局区(数据段);
    • 内存地址格式为:-0x8(%rbp),一般是局部变量,栈空间。
    • 内存地址格式为:0x10(%rax),一般是堆空间。

    规律:

    • 全局变量意味着内存地址是固定的;
    • 局部变量的地址依赖rbp,而rbp右依赖于rsprsp是外部传进来的(即函数调用)。

    1.4. 赋值操作

    Swift标准库中,为了提升性能,StringArrayDictionarySet采取了Copy On Write的技术。

    Copy On Write: 当需要进行内存操作(写)时,才会进行深度拷贝。

    对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值。

    建议:不需要修改的,尽量定义为let

    1.4.1. 示例代码一(字符串):

    var str1 = "idbeny"
    var str2 = str1
    str2.append("1024星球")
    print(str1)
    print(str2)
    /*
     输出:
     idbeny
     idbeny1024星球
     */
    

    1.4.2. 示例代码二(数组):

    var arr1 = ["1", "2", "3"]
    var arr2 = arr1
    arr2.append("4")
    arr1[0] = "one"
    print(arr1)
    print(arr2)
    /*
     输出:
     ["one", "2", "3"]
     ["1", "2", "3", "4"]
     */
    

    1.4.3. 示例代码三(字典):

    var dict1 = ["name": "大奔", "age": 20] as [String : Any]
    var dict2 = dict1
    dict1["name"] = "idbeny"
    dict2["age"] = 30
    print(dict1)
    print(dict2)
    /*
     输出:
     ["name": "idbeny", "age": 20]
     ["name": "大奔", "age": 30]
     */
    

    二、引用类型

    引用赋值给varlet或者给函数传参,是将内存地址拷贝一份。

    类似于制作一个文件的替身(快捷方式),指向的是同一个文件。属于浅拷贝(shallow copy)。

    2.1. 内存分析

    示例代码:

    class Size {
        var width: Int
        var height: Int
        init(width: Int, height: Int) {
            self.width = width
            self.height = height
        }
    }
    
    func test() {
        var s1 = Size(width: 10, height: 20)
        var s2 = s1
        print("s1指针的内存地址:",Mems.ptr(ofVal: &s1))
        print("s1指针指向的内存地址:",Mems.ptr(ofRef: s1))
        print("s2指针的内存地址:",Mems.ptr(ofVal: &s2))
        print("s2指针指向的内存地址:",Mems.ptr(ofRef: s2))
    }
    test()
    /*
     输出:
     s1指针的内存地址: 0x00007ffeefbff478
     s1指针指向的内存地址: 0x000000010061fe80
     s2指针的内存地址: 0x00007ffeefbff470
     s2指针指向的内存地址: 0x000000010061fe80
     */
    

    示例代码在内存中的表现:

    思考: s2.width = 11; s2.height = 22,代码执行后,s1.widths1.height分别是多少?

    s2.width == 11, s2.height == 22,因为修改的是指针指向的内存地址保存的数据,而s1s2指向的是同一块内存。

    2.2. 汇编分析

    第一步:示例代码:

    第二步:查看初始化方法函数的返回值:

    通过lldb指令得到rax的地址:

    (lldb) register read rax
    输出:rax = 0x0000000100599840
    

    再通过View Memory查看rax保存的数据有哪些:

    第三步:找到p1p2

    函数地址rax给了局部变量-0x10(%rbp),所以-0x10(%rbp)就是p1,同理-0x28(%rbp)是p2。

    第四步:查看s2widthheight是如何被修改的:

    • 前面通过movq %rax, -0x28(%rbp)把函数返回值rax给了-0x28(%rbp)
    • 之后又通过movq -0x28(%rbp), %rdx把函数返回值给了rdx
    • 经过(%rdx), %rsi0x68(%rsi), %rsi中转后,把rdx给了rsi
    • $0xb, %edi其实是把值11给了edi(即rdx)。

    所以,width和height其实修改的是同一块内存地址。

    2.3. 赋值操作

    示例代码:

    class Size {
        var width: Int
        var height: Int
        init(width: Int, height: Int) {
            self.width = width
            self.height = height
        }
    }
    var s1 = Size(width: 10, height: 20)
    s1 = Size(width: 11, height: 22)
    

    在内存中的表现:

    s1刚开始指向堆空间02,后又指向堆空间01。当堆空间02没有强指针指向时就会被销毁。

    三、值类型、引用类型的let

    使用let时,
    结构体:

    • 结构体整体不能被覆盖;
    • 结构体成员值也不能修改。

    引用类型:

    • 指针是不能重新指向新内存的。
    • 指针指向的内存数据是可以修改的。

    bk