Swift系列七 – 彙編分析值類型

通過彙編分下值類型的本質。

一、值類型

值類型賦值給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時,
結構體:

  • 結構體整體不能被覆蓋;
  • 結構體成員值也不能修改。

引用類型:

  • 指針是不能重新指向新內存的。
  • 指針指向的內存數據是可以修改的。

Tags: