[譯]Go裡面的unsafe包詳解

astaxie發表於2016-11-13

The unsafe Package in Golang

Golang 的 unsafe 包是一個很特殊的包。 為什麼這樣說呢? 本文將詳細解釋。

來自 go 語言官方文件的警告

unsafe 包的文件是這麼說的:

匯入unsafe的軟體包可能不可移植,並且不受Go 1相容性指南的保護。

Go 1 相容性指南這麼說:

匯入unsafe軟體包可能取決於Go實現的內部屬性。 我們保留對可能導致程式崩潰的實現進行更改的權利。

當然包名稱暗示 unsafe 包是不安全的。 但這個包有多危險呢? 讓我們先看看 unsafe 包的作用。

Unsafe 包的作用

直到現在(Go1.7),unsafe 包含以下資源:

  • 三個函式:

    • func Alignof(variable ArbitraryType)uintptr
    • func Offsetof(selector ArbitraryType)uintptr
    • func Sizeof(variable ArbitraryType)uintptr
  • 和一種型別:

    • 型別 Pointer * ArbitraryType

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

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

(BTW,unsafe 包中的函式中非唯一呼叫將在編譯時求值。當傳遞給 len 和 cap 的引數是一個陣列值時,內建函式和 cap 函式的呼叫也可以在編譯時被求值。)

除了這三個函式和一個型別外,指標在 unsafe 包也為編譯器服務。

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

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

  • 指標型別和 uintptr 的值。

但是藉助 unsafe.Pointer,我們可以打破 Go 型別和記憶體安全性,並使上面的轉換成為可能。這怎麼可能發生?讓我們閱讀 unsafe 包文件中列出的規則:

  • 任何型別的指標值都可以轉換為 unsafe.Pointer。
  • unsafe.Pointer 可以轉換為任何型別的指標值。
  • uintptr 可以轉換為 unsafe.Pointer。
  • unsafe.Pointer 可以轉換為 uintptr。

這些規則與 Go 規範一致:

底層型別uintptr的任何指標或值都可以轉換為指標型別,反之亦然。

規則表明 unsafe.Pointer 類似於 c 語言中的 void *。當然,void *在 C 語言裡是危險的!

在上述規則下,對於兩種不同型別 T1 和 T2,可以使* T1 值與 unsafe.Pointer 值一致,然後將 unsafe.Pointer 值轉換為* T2 值(或 uintptr 值)。通過這種方式可以繞過 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 包中的作用是為 Go 編譯器服務,unsafe.Pointer 型別的作用是繞過 Go 型別系統和記憶體安全。

再來一點 unsafe.Pointer 和 uintptr

這裡有一些關於 unsafe.Pointer 和 uintptr 的事實:

  • uintptr 是一個整數型別。
    • 即使 uintptr 變數仍然有效,由 uintptr 變數表示的地址處的資料也可能被 GC 回收。
  • unsafe.Pointer 是一個指標型別。
    • 但是 unsafe.Pointer 值不能被取消引用。
    • 如果 unsafe.Pointer 變數仍然有效,則由 unsafe.Pointer 變數表示的地址處的資料不會被 GC 回收。
  • * unsafe.Pointer 是一個通用的指標型別,就像* int 等。

由於 uintptr 是一個整數型別,uintptr 值可以進行算術運算。 所以通過使用 uintptr 和 unsafe.Pointer,我們可以繞過限制,* T 值不能在 Golang 中計算偏移量:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    a := [4]int{0, 1, 2, 3}
    p1 := unsafe.Pointer(&a[1])
    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)
    pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
    page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
    pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
    *pname = "Alice"
    *page = 28
    *pgender = false
    fmt.Println(who) // {Alice 28 false}
}

unsafe 包有多危險

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

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

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

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

看起來所有的 unsafe 包的危險都與使用 unsafe.Pointer 有關。 unsafe 包 docs 列出了一些使用 unsafe.Pointer 合法或非法的情況。 這裡只列出部分非法使用案例:

package main

import (
    "fmt"
    "unsafe"
)

// case A: conversions between unsafe.Pointer and uintptr 
//         don't appear in the same expression
func illegalUseA() {
    fmt.Println("===================== illegalUseA")

    pa := new([4]int)

    // split the legal use
    // p1 := unsafe.Pointer(uintptr(unsafe.Pointer(pa)) + unsafe.Sizeof(pa[0]))
    // into two expressions (illegal use):
    ptr := uintptr(unsafe.Pointer(pa))
    p1 := unsafe.Pointer(ptr + unsafe.Sizeof(pa[0]))
    // "go vet" will make a warning for the above line:
    // possible misuse of unsafe.Pointer

    // the unsafe package docs, https://golang.org/pkg/unsafe/#Pointer,
    // thinks above splitting is illegal.
    // but the current Go compiler and runtime (1.7.3) can't detect
    // this illegal use.
    // however, to make your program run well for later Go versions,
    // it is best to comply with the unsafe package docs.

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

// case B: pointers are pointing at unknown addresses
func illegalUseB() {
    fmt.Println("===================== illegalUseB")

    a := [4]int{0, 1, 2, 3}
    p := unsafe.Pointer(&a)
    p = unsafe.Pointer(uintptr(p) + uintptr(len(a)) * unsafe.Sizeof(a[0]))
    // now p is pointing at the end of the memory occupied by value a.
    // up to now, although p is invalid, it is no problem.
    // but it is illegal if we modify the value pointed by p
    *(*int)(p) = 123
    fmt.Println("*(*int)(p)  :", *(*int)(p)) // 123 or not 123
    // the current Go compiler/runtime (1.7.3) and "go vet" 
    // will not detect the illegal use here.

    // however, the current Go runtime (1.7.3) will 
    // detect the illegal use and panic for the below code.
    p = unsafe.Pointer(&a)
    for i := 0; i <= len(a); i++ {
        *(*int)(p) = 123 // Go runtime (1.7.3) never panic here in the tests

        fmt.Println(i, ":", *(*int)(p))
        // panic at the above line for the last iteration, when i==4.
        // runtime error: invalid memory address or nil pointer dereference

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

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

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

轉換*T1 為 *T2

對於將* T1 轉換為 unsafe.Pointer,然後轉換為* T2,unsafe 包 docs 說:

如果T2比T1大,並且兩者共享等效記憶體佈局,則該轉換允許將一種型別的資料重新解釋為另一型別的資料。

這種 “等效記憶體佈局” 的定義是有一些模糊的。 看起來 go 團隊故意如此。 這使得使用 unsafe 包更危險。

由於 Go 團隊不願意在這裡做出準確的定義,本文也不嘗試這樣做。 這裡,列出了已確認的合法用例的一小部分,

合法用例 1:在 [] T 和 [] MyT 之間轉換

在這個例子裡,我們用 int 作為 T:

type MyInt int

在 Golang 中,[] int 和 [] MyInt 是兩種不同的型別,它們的底層型別是自身。 因此,[] int 的值不能轉換為 [] MyInt,反之亦然。 但是在 unsafe.Pointer 的幫助下,轉換是可能的:

package main

import (
    "fmt"
    "unsafe"
)

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]
}

合法用例 2: 呼叫 sync/atomic 包中指標相關的函式

sync / atomic 包中的以下函式的大多數引數和結果型別都是 unsafe.Pointer 或*unsafe.Pointer:

  • func CompareAndSwapPointer(addr * unsafe.Pointer,old,new unsafe.Pointer)(swapped bool)
  • func LoadPointer(addr * unsafe.Pointer)(val unsafe.Pointer)
  • func StorePointer(addr * unsafe.Pointer,val unsafe.Pointer)
  • func SwapPointer(addr * unsafe.Pointer,new unsafe.Pointer)(old unsafe.Pointer)

要使用這些功能,必須匯入 unsafe 包。 注意:* unsafe.Pointer 是一般型別,因此* unsafe.Pointer 的值可以轉換為 unsafe.Pointer,反之亦然。

package main

import (
    "fmt"
    "log"
    "time"
    "unsafe"
    "sync/atomic"
    "sync"
    "math/rand"
)

var data *string

// get data atomically
func Data() string {
    p := (*string)(atomic.LoadPointer(
            (*unsafe.Pointer)(unsafe.Pointer(&data)),
        ))
    if p == nil {
        return ""
    } else {
        return *p
    }
}

// set data atomically
func SetData(d string) {
    atomic.StorePointer(
            (*unsafe.Pointer)(unsafe.Pointer(&data)), 
            unsafe.Pointer(&d),
        )
}

func main() {
    var wg sync.WaitGroup
    wg.Add(200)

    for range [100]struct{}{} {
        go func() {
            time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)

            log.Println(Data())
            wg.Done()
        }()
    }

    for i := range [100]struct{}{} {
        go func(i int) {
            time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)
            s := fmt.Sprint("#", i)
            log.Println("====", s)

            SetData(s)
            wg.Done()
        }(i)
    }

    wg.Wait()

    fmt.Println("final data = ", *data)
}

結論

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

原文:http://www.tapirgames.com/blog/golang-unsafe

更多原創文章乾貨分享,請關注公眾號
  • [譯]Go裡面的unsafe包詳解
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章