Golang中的unsafe標準庫包

tttlll發表於2016-11-13

2016/10/22, 老獏

unsafe標準庫包是Golang中一個比較特殊的包,為什麼這麼說?本問將詳細解釋其中緣由。

Go官方文件中的警告

unsafe包的文件這麼說:

Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.

Go 1 相容性指南這麼說:

Packages that import unsafe may depend on internal properties of the Go implementation. We reserve the right to make changes to the implementation that may break such programs.

兩段話大同小異,簡而言之就是使用unsafe包是危險的,Golang不保證此包的跨平臺和跨版本相容性。聽上去挺慎人,但是此包究竟有多危險?先讓我們看看此包在Golang中發揮的角色是什麼。

unsafe包的角色

到目前為止(Go1.7),unsafe包包含以下資源:

  • 3個函式:
    • func Alignof(variable ArbitraryType) uintptr
    • func Offsetof(selector ArbitraryType) uintptr
    • func Sizeof(variable ArbitraryType) uintptr
  • 和一個型別

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

不像絕大多數的函式,在Golang中,以上3個函式的呼叫將在編譯時而不是執行時被估值,因此這3個函式的返回值可以被賦給常量。換句話說,這3個函式是為編譯器服務的。

除了這3個函式,unsafe包中唯一的型別unsafe.Pointer也是為編譯器服務的。

由於安全原因,Golang不允許下列型別的值互相進行轉換:

  • 兩個不同指標型別,比如*int64 and *float64
  • 任何普通指標型別*Tuintptr

但是在unsafe.Pointer的幫助下,我們可以打破Golang型別系統和記憶體管理的安全機制,從而使得以上的轉換成為可能。這是怎麼辦到的呢?讓我們看看unsafe包中列出的和unsafe.Pointer有關的規則

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

這些規則和Go白皮書是一致的:

Any pointer or value of underlying type uintptr can be converted to a Pointer type and vice versa.

這些規則表明unsafe.Pointer如同c語言中的void*。是的,c語言中的void*很危險!

按照這些規則,對兩個不同的型別T1T2,指標型別*T1的值可以被轉換為unsafe.Pointer型別,然後轉換後的unsafe.Pointer型別的值可以繼續被轉換為*T2型別(或者uintptr型別);反之也是可以的。通過這種方式,Golang型別系統和記憶體管理的安全機制被繞過了。顯然,濫用這種方式是危險的。

看個例子:

package main

import (
        "fmt"
        "unsafe"
)
func main() {
        var n int64 = 5
        var pn = &n
        var pf = (*float64)(unsafe.Pointer(pn))
        // 到這裡,pn和pf指向同一個記憶體地址。
        fmt.Println(*pf) // 2.5e-323
        *pf = 3.14159
        fmt.Println(n) // 4614256650576692846
}

此例中的轉換或許並沒有太大實際意義,但此轉換是安全和合法的。至於為什麼安全,見下文。

更多關於unsafe.Pointer和uintptr的一些事實

下面是關於unsafe.Pointeruintptr的一些事實:

  • uintptr 是一個整數型別,
    • 即使一個uintptr值依然被程式使用中,此uintptr值所表示的指標所指的資料可能將被垃圾回收。
  • unsafe.Pointer是一個指標型別,
    • 但是unsafe.Pointer值不能被解引用;
    • 和普通指標一樣,如果一個unsafe.Pointer 值依然被程式使用中,此unsafe.Pointer 值所表示的指標所指的資料將確保不會被垃圾回收。
  • *unsafe.Pointer是一個普通指標,和*int類似。

既然uintptr是一個整數型別,那麼當然可以對uintptr值進行算術運算。利用這一點,我們可以繞開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包,Go團隊的主力開發之一Ian已經確認:

  • unsafe包中的函式原型今後保證不變;
  • 型別unsafe.Pointer將永遠存在。

所以,看上去unsafe包中的函式並不怎麼危險。Go團隊的甚至想把它們放到別的包中。使用這些函式唯一的不安全性是這些函式的呼叫在今後的版本中可能會返回不同的結果值。但這種不安全性很難說是一種危險。

這樣我們可以的得出結論:使用unsafe包的危險都是和使用unsafe.Pointer型別密切相關的。unsafe包的文件列出了一些合法和非法使用unsafe.Pointer型別的例子,這裡僅僅列出部分非法的情形:

package main

import (
        "fmt"
        "unsafe"
)

// 情形A:unsafe.Pointer和uintptr之間的來回轉換
//       未在同一個表示式中完成。
func illegalUseA() {
        fmt.Println("===================== 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"將對上一行給出一個警告:
        // possible misuse of unsafe.Pointer

        // unsafe包的文件https://golang.org/pkg/unsafe/#Pointer,
        // 認為上面分成兩行是非法的,
        // 但是目前的Go編譯器和執行時(1.7.3)
        // 沒有偵測到這個非法使用。
        // 
        // 然而,為了保證你的Go程式絕對安全的執行,
        // 最好還是請遵守unsafe包的文件中定的規則。

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

// 情形B:指標指向了非法地址
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]))
        // 現在p指向了a值記憶體塊的結尾,
        // 因為我們不清楚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)將偵測到下面的
        // 非法操作(其實和上面的非法操作是一樣的)並panic。
        p = unsafe.Pointer(&a)
        for i := 0; i <= len(a); i++ {
                *(*int)(p) = 123 // Go執行時(1.7.3)不會在此panic

                fmt.Println(i, ":", *(*int)(p))
                // 當i==4的時候,Go執行時(1.7.3)將在上一行panic
                // runtime error: invalid memory address or nil pointer dereference

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

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

編譯器很難檢測到非法的unsafe.Pointer使用。雖然"go vet"能發現一些潛在的非法的unsafe.Pointer使用,但也不能檢測到所有的非法使用。同樣,Go執行時也不能檢測到所有的非法unsafe.Pointer使用。非法unsafe.Pointer使用可能會是程式崩潰,或者使程式執行詭異(比如有時候表現正常,有時候卻很反常)。這就是為什麼說使用unsafe包是危險的。

*T1*T2之間的轉換

對於將*T1值轉換為unsafe.Pointer型別,繼而再轉換為*T2型別,unsafe包是這麼說的:

Provided that T2 is no larger than T1 and that the two share an equivalent memory layout, this conversion allows reinterpreting data of one type as data of another type.

直譯過來:如果型別T2的大小不比T1大並且這兩個型別有著等價的記憶體佈局,這種轉換允許其中一個型別的資料用另一個型別的資料來表示。(反正我是沒看太明白)

這裡對equivalent memory layout(記憶體佈局)的定義很含糊,而且好像Go團隊故意將其定義得很含糊。 這使得使用unsafe包更加的危險。

既然Go團隊不願意定義一個精確的規則,本文也不會做這個嘗試。這裡僅僅列出部分合法的例子。

合法使用1:在切片[]T[]MyT之間轉換

在下例中,我們使用int型別表示T

type MyInt int

在Golang的型別系統中,型別[]int[]MyInt底層型別均為它們自身。Golang中,底層型別不一樣的兩個非介面型別之間是不支援轉換的。但是在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包是為編譯器而不是執行時服務的;
  • 使用unsafe做為包名是為了讓程式設計師小心使用此包;
  • 使用unsafe包並非總是不推薦的,有時我們必須使用它;
  • Golang的型別系統設計同時兼顧安全性和效率,但安全性要優先於效率。所以有時候為了普遍安全性將犧牲一些效率。unsafe包使得經驗豐富的程式設計師在某些時候不影響安全性的同時繞開Golang的安全機制,從而避免一些正常使用中的效率犧牲。
  • 當然,再重申一遍,unsafe包很容易被濫用,因此使用之是危險的。

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

相關文章