1. 程式人生 > >【我的區塊鏈之路】- 說一說go中的unsafe包

【我的區塊鏈之路】- 說一說go中的unsafe包

【轉載請標明出處】https://blog.csdn.net/qq_25870633/article/details/83422886

在golang的原生庫中有一個叫做unsafe的包,該包主要是做對記憶體位移的一些操作。

首先我們來看下unsafe包的成員:

是不是很簡單,然後三個函式都是沒有 func body 的,這裡不是用 //go:linkname

的方式實現的,而是用了彙編

包名稱暗示unsafe包是不安全的,預示著我們在現實中如果有十足的瞭解才可以用這個包的成員,不然總會帶來奇奇怪怪的結果。

首先,我們看到ArbitraryType型別其實是int的別名。在golang中,對ArbitraryType賦予了特殊的意義,千萬不要死磕這個後邊的int型別。通常的說,我們把interface{}看作是任意型別,是比較浪蕩的型號,具有老少通吃的特點,那麼ArbitraryType這個型別,在golang系統中,是人獸皆不在話下的型別。比interface{}還要隨意。也就是說:ArbitraryType 實際上代表任意的Go表示式的結果型別

。ArbitraryType不是一個真正的型別,它只是一個佔位符。

另外,還定義了一個Pointer的型別,其實為 ArbitraryType 的指標。

提示一下:golang的指標型別長度與int型別長度,在記憶體中佔用的位元組數是一樣的喲。

下面我們來看四個與unsafe.Pointer型別的特殊轉換:

  1. 一個任意型別的指標值可以轉換成一個unsafe.Pointer型別值: p := unsafe.Pointer(&a)
  2. 一個unsafe.Pointer型別可以轉成一個任意型別的指標值: vptr := (*int)(p)
  3. 一個unsafe.Pointer型別可以轉換成 uintptr 型別: uptr := uintptr(p)
  4. 一個uintptr 型別可以轉成 unsafe.Pointer 型別: p := unsafe.Pointer(uptr)

注意:

  • 任意型別的指標值不能和uintptr 互轉。
  • unsafe.Pointer 型別使得程式繞過Go的型別系統檢查並直接在記憶體地址上進行讀寫
  • 像 *int 和 *float 在記憶體中的佈局是不一樣的。(*int)(&f32) 是行不通的,但是藉助unsafe.Pointer 作為中間型別是不會報錯的,但是 在同一段記憶體上的資料,我們分別作為 int的值和 float的值結果是不一樣的。 所以這時候 float 轉int之後得到的是一個不正確的值。
  • uintptr是一個整數型別。
  • 即使uintptr變數仍然有效,由uintptr變量表示的地址處的資料也可能被GC回收。
  • unsafe.Pointer是一個指標型別
  • 但是unsafe.Pointer值不能被取消引用。
  • 如果unsafe.Pointer變數仍然有效,則由unsafe.Pointer變量表示的地址處的資料不會被GC回收。
  • 由於uintptr是一個整數型別,uintptr值可以進行算術運算。 所以通過使用uintptr和unsafe.Pointer,我們可以繞過限制,* T值不能在Golang中計算偏移量

可知出於安全原因,Golang不允許以下之間的直接轉換:

  • 兩個不同指標型別的值,例如 int64和 float64。

  • 指標型別和uintptr的值。

但是藉助unsafe.Pointer,我們可以打破Go型別和記憶體安全性,並使上面的轉換成為可能

濫用這種方式是很危險的。

舉個例子:

package main

import (
    "fmt"
    "unsafe"
)
func main() {
    var n int64 = 5
    var pn = &n
    var pf = (*float64)(unsafe.Pointer(pn))
    // now, pn and pf are pointing at the same memory address
    fmt.Println(*pf) // 2.5e-323
    *pf = 3.14159
    fmt.Println(n) // 4614256650576692846
}

在這個例子中的轉換可能是無意義的,但它是安全和合法的(為什麼它是安全的?)。

關於unsafe包,Ian,Go團隊的核心成員之一,已經確認:

  • 在unsafe包中的函式的簽名將不會在以後的Go版本中更改,

  • 並且unsafe.Pointer型別將在以後的Go版本中始終存在。

所以,unsafe包中的三個函式看起來不危險。 go team leader甚至想把它們放在別的地方。 unsafe包中這幾個函式唯一不安全的是它們呼叫結果可能在後來的版本中返回不同的值。 很難說這種不安全是一種危險。

因此,資源在unsafe包中的作用是為Go編譯器服務,unsafe.Pointer型別的作用是繞過Go型別系統和記憶體安全。

下面我們再來說一說三個函式:

與Golang中的大多數函式不同,上述三個函式的呼叫將始終在編譯時求值,而不是執行時。 這意味著它們的返回結果可以分配給常量


// 返回變數對齊位元組數量
func Alignof(x ArbitraryType) uintptr

// 返回變數指定屬性的偏移量
/**
函式雖然接收的是任何型別的變數,
但是這個又一個前提,就是變數要是一個struct型別,
且還不能直接將這個struct型別的變數當作引數,
只能將這個struct型別變數的屬性當作引數
*/
func Offsetof(x ArbitraryType) uintptr

// 返回變數在記憶體中佔用的位元組數
/**
切記,如果是slice,則不會返回這個slice在記憶體中的實際佔用長度
*/
func Sizeof(x ArbitraryType) uintptr


通過分析發現,這三個函式的引數均是ArbitraryType型別,就是接受任何型別的變數。 

unsafe中,通過這兩個相容萬物的型別 (ArbitraryTypePointer),將其他型別都轉換過來,然後通過這三個函式,分別能取長度偏移量對齊位元組數,就可以在記憶體地址對映中,來回遊走盡情的去放縱。

另外提一點: 

uintptr這個型別,在golang中,位元組長度也是與int一致。通常Pointer不能參與運算,比如你要在某個指標地址上加上一個偏移量,Pointer是不能做這個運算的,那麼誰可以呢?就是uintptr型別了,只要將Pointer型別轉換成uintptr型別,做完加減法後,轉換成Pointer,通過*操作,取值,修改值,隨意。

unsafe.Pointer的騷操作:

type Person struct {
    Name    string    `json:"name"`
    Age     uint8     `json:"age"`
    Address string    `json:"address"`         
}


func main (){
    
    // 獲取 person 結構體的指標
    pp := &Person{"Robert",    32,    "Beijing"}

    // 獲取 person 的記憶體地址
    vptr := uintptr(unsafe.Pointer(pp))

    // 獲取 person 結構體中的Name所對應的記憶體地址
    // Offsetof()函式返回,記憶體中從儲存該結構體的起始位置到
    // 儲存其中某個欄位的值的起始位置之間的距離
    // 儲存偏移量(也就是這個距離)的單位為 byte,距離的型別為 uintptr
    nptr := vptr + unsafe.Offsetof(pp.Name)   
    
    /**
    事實上,同一個結構體型別的值,在記憶體的儲存佈局是固定的,就是說:
    對於同一個結構體型別和題的同一個欄位來說,這個儲存偏移量總是相同的
    
    從上面求偏移量的描述中我們也知道了 結構體的儲存其實是連續的一段記憶體,
    是不是聯想到了之前兩篇文章中 chan 和select 的儲存做法一樣?
    現有頭,然後關聯的東西記憶體都接在頭記憶體後面儲存
    */



    // 還原成Name欄位的指標型別值
    var namePtr *string = (*string)(unsafe.Pointer(nptr)) 
    
    fmt.Println(*namePtr)  // Robert
}

這裡有一個恆等式:

uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))

原則上,我們只要獲取了儲存某個值的記憶體地址,就可以通過一定運算得到儲存在其他記憶體地址上的值甚至程式

 

再看一些例子:

func main() {
    a := [4]int{0, 1, 2, 3}
    
    // 先求 a[1] 元素的Pointer 型別
    p1 := unsafe.Pointer(&a[1])

    // 根據 a[1] 元素的記憶體地址 + 2 * int型別的位元組數 == a[3] 的記憶體地址
    p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0]))
    *(*int)(p3) = 6
    fmt.Println("a =", a) // a = [0 1 2 6]

    // ...

    type Person struct {
        name   string
        age    int
        gender bool
    }

    who := Person{"John", 30, true}
    pp := unsafe.Pointer(&who)
    // 求出 name 的指標
    pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
    // 求 age 的指標
    page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
    // 求 gender 的指標
    pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
    *pname = "Alice"
    *page = 28
    *pgender = false
    fmt.Println(who) // {Alice 28 false}
}

一些 非法案例:

// case A: unsafe.Pointer和uintptr之間的轉換不會出現在同一個表示式中
func illegalUseA() {
   

    pa := new([4]int)

    // 將合法用途 p1:= unsafe.Pointer(uintptr(unsafe.Pointer(pa))+ unsafe.Sizeof(pa [0]))分成兩個表示式(非法使用):
    ptr := uintptr(unsafe.Pointer(pa))
    p1 := unsafe.Pointer(ptr + unsafe.Sizeof(pa[0]))
    // “go vet”會對上述行發出警告:
    //  可能濫用 unsafe.Pointer
    // unsafe 包文件,https://golang.org/pkg/unsafe/#Pointer,
    // 認為上面的分裂是非法的。 
    // 但是當前的Go編譯器和執行時(1.7.3)無法檢測到這種非法使用。 
    // 但是,為了使您的程式在以後的Go版本中執行良好,最好遵守不安全的軟體包文件。

    *(*int)(p1) = 123
    fmt.Println("*(*int)(p1)  :", *(*int)(p1)) //
}    

// case B: 指標指向未知地址
func illegalUseB() {
   
    a := [4]int{0, 1, 2, 3}
    p := unsafe.Pointer(&a)
    p = unsafe.Pointer(uintptr(p) + uintptr(len(a)) * unsafe.Sizeof(a[0]))
    // 現在p指向值a佔用的記憶體的末尾。 
    // 到目前為止,雖然p無效,但沒問題。 但如果我們修改p指向的值,則是非法的
    *(*int)(p) = 123
    fmt.Println("*(*int)(p)  :", *(*int)(p)) // 123 or not 123
    // 當前的Go編譯器/執行時(1.7.3)和“go vet”將不會檢測到此處的非法使用。

    // 但是,當前的Go執行時(1.7.3)將檢測以下程式碼的非法使用和恐慌。
    p = unsafe.Pointer(&a)
    for i := 0; i <= len(a); i++ {
        *(*int)(p) = 123 // Go runtime(1.7.3)在測試中從未出現過恐慌
        fmt.Println(i, ":", *(*int)(p))
        // 當i == 4時,在上一行的最後一次迭代中發生恐慌。 
        // 執行時錯誤:無效的記憶體地址或無指標取消引用

        p = unsafe.Pointer(uintptr(p) + unsafe.Sizeof(a[0]))
    }
}

func main() {
    illegalUseA()
    illegalUseB()
}

編譯器很難檢測Go程式中非法的unsafe.Pointer使用。 執行“go vet”可以幫助找到一些潛在的錯誤,但不是所有的都能找到。 同樣是Go執行時,也不能檢測所有的非法使用。 非法unsafe.Pointer使用可能會使程式崩潰或表現得怪異(有時是正常的,有時是異常的)。 這就是為什麼使用不安全的包是危險的。

再看:

type MyInt int

func main() {
    type MyInt int

    a := []MyInt{0, 1, 2}
    // b := ([]int)(a) // error: cannot convert a (type []MyInt) to type []int
    b := *(*[]int)(unsafe.Pointer(&a))

    b[0]= 3

    fmt.Println("a =", a) // a = [3 1 2]
    fmt.Println("b =", b) // b = [3 1 2]

    a[2] = 9

    fmt.Println("a =", a) // a = [3 1 9]
    fmt.Println("b =", b) // b = [3 1 9]
}

結論

  • unsafe包用於Go編譯器,而不是Go執行時。
  • 使用unsafe.Pointer並不總是一個壞主意,有時我們必須使用它。
  • Golang的型別系統是為了安全和效率而設計的。 但是在Go型別系統中,安全性比效率更重要。 通常Go是高效的,但有時安全真的會導致Go程式效率低下。 unsafe包用於有經驗的程式設計師通過安全地繞過Go型別系統的安全性來消除這些低效。
  • unsafe包可能被濫用並且是危險的。