Go 里面的 unsafe 包詳解

AWTJohnson 8年前發布 | 20K 次閱讀 Go語言 Google Go/Golang開發

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
    </li>
  • 和一種類型:

    • 類型Pointer * ArbitraryType
    • </ul> </li> </ul>

      這里,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 }</code></pre>

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

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

      再來一點 unsafe.Pointer 和 uintptr

      這里有一些關于unsafe.Pointer和uintptr的事實:

      • uintptr是一個整數類型。
        • 即使uintptr變量仍然有效,由uintptr變量表示的地址處的數據也可能被GC回收。
        </li>
      • unsafe.Pointer是一個指針類型。
        • 但是unsafe.Pointer值不能被取消引用。
        • 如果unsafe.Pointer變量仍然有效,則由unsafe.Pointer變量表示的地址處的數據不會被GC回收。
        • </ul> </li>
          • unsafe.Pointer是一個通用的指針類型,就像 int等。</li> </ul> </li> </ul>

            由于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}
            

            }</code></pre>

            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() }</code></pre>

            編譯器很難檢測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]
            

            }</code></pre>

            合法用例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)
            

            }</code></pre>

            結論

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

             

             

            來自:http://mp.weixin.qq.com/s?__biz=MjM5OTcxMzE0MQ==&mid=2653369781&idx=1&sn=cc7acb61fe49fa70aaa198d0e98754de&chksm=bce4d5af8b935cb97893e2478577908513d220bda0d02d6d70ccfd4160608ac6afd8e05dee10&scene=0

             

 本文由用戶 AWTJohnson 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!