Go unsafe.Pointer uintptr原理和玩法

本文轉至:https://www.cnblogs.com/sunsky303/p/11820500.html

unsafe.Pointer

這個類型比較重要,它是實現定位和讀寫的內存的基礎,Go runtime大量使用它。官方文檔對該類型有四個重要描述:
(1)任何類型的指針都可以被轉化爲Pointer
(2)Pointer可以被轉化爲任何類型的指針
(3)uintptr可以被轉化爲Pointer
(4)Pointer可以被轉化爲uintptr

大多數指針類型會寫成T,表示是“一個指向T類型變量的指針”。unsafe.Pointer是特別定義的一種指針類型(譯註:類似C語言中的void類型的指針),它可以包含任意類型變量的地址。當然,我們不可以直接通過*p來獲取unsafe.Pointer指針指向的真實變量的值,因爲我們並不知道變量的具體類型。和普通指針一樣,unsafe.Pointer指針也是可以比較的,並且支持和nil常量比較判斷是否爲空指針。
一個普通的T類型指針可以被轉化爲unsafe.Pointer類型指針,並且一個unsafe.Pointer類型指針也可以被轉回普通的指針,被轉回普通的指針類型並不需要和原始的T類型相同。

package main

import (
   "fmt"
   "unsafe"
   "reflect"
)
type W struct {
   b byte
   i int32
   j int64
}

//通過將float64類型指針轉化爲uint64類型指針,我們可以查看一個浮點數變量的位模式。
func Float64bits(f float64) uint64 {
   fmt.Println(reflect.TypeOf(unsafe.Pointer(&f)))  //unsafe.Pointer
   fmt.Println(reflect.TypeOf((*uint64)(unsafe.Pointer(&f))))  //*uint64
   return *(*uint64)(unsafe.Pointer(&f))
}
func Uint(i int)uint{
   return *(*uint)(unsafe.Pointer(&i))
}
type Uint6 struct {
   low [2]byte
   high uint32
}
//func (u *Uint6) SetLow() {
// fmt.Printf("i=%d\n", this.i)
//}
//
//func (u *Uint6) SetHigh() {
// fmt.Printf("j=%d\n", this.j)
//}
func writeByPointer(){
   uint6 := &Uint6{}
   lowPointer:=(*[2]byte)(unsafe.Pointer(uint6))
   *lowPointer = [2]byte{1,2}
   //unsafe.Offsetof會計算padding後的偏移距離
   //必須將unsafe.Pointer轉化成 uintptr類型才能進行指針的運算,uintptr 與 unsafe.Pointer 之間可以相互轉換。
   highPointer:=(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(uint6))+unsafe.Offsetof(uint6.high)))
   fmt.Printf("addr %x addr %x size %v size %v size %v align %v offset %v \n", uintptr(unsafe.Pointer(uint6)),uintptr(unsafe.Pointer(uint6))+unsafe.Sizeof(uint6.low),unsafe.Sizeof([2]byte{1,2}),unsafe.Sizeof(uint6.low), unsafe.Sizeof(uint6.high), unsafe.Alignof(uint6.low), unsafe.Offsetof(uint6.high))
   *highPointer = uint32(9)
   //藉助於 unsafe.Pointer,我們實現了像 C 語言中的指針偏移操作。可以看出,這種不安全的操作使得我們可以在任何地方直接訪問結構體中未公開的成員,只要能得到這個結構體變量的地址。
   // fmt.Printf("%+v %v %v %v \n", uint6, &uint6,&uint6.low[0], &uint6.high)
   fmt.Printf("%+v %p %p %v \n", uint6, uint6, &uint6.low[0], &uint6.high)
	// 輸出結果【轉者加的註釋,並修改了代碼,可以直觀瞭解指針首地址指向哪裏】
	// &{low:[1 2] high:9} 0xc0000a2008 0xc0000a2008 0xc0000a200c
	// 結構體值,結構體指針地址,&uint6.low[0]指針地址[結構體指針首地址],&uint6.high指針地址
}
type T struct {
   t1 byte
   t2 int32
   t3 int64
   t4 string
   t5 bool
}
func main() {
   fmt.Printf("%#x  %#b \n", Float64bits(11.3), Float64bits(4)) // "0x3ff0000000000000"
   var intA int =99
   uintA:=Uint(intA)
   fmt.Printf("%#v %v  %v \n", intA, reflect.TypeOf(uintA), uintA)
   var w W = W{}
   //在struct中,它的對齊值是它的成員中的最大對齊值。
   fmt.Printf("%v, %v, %v, %v, %v, %v, %v, %v\n", unsafe.Alignof(w), unsafe.Alignof(w.b), unsafe.Alignof(w.i), unsafe.Alignof(w.j), unsafe.Sizeof(w),unsafe.Sizeof(w.b),unsafe.Sizeof(w.i),unsafe.Sizeof(w.j), )

   fmt.Println(unsafe.Alignof(byte(0)))
   fmt.Println(unsafe.Alignof(int8(0)))
   fmt.Println(unsafe.Alignof(uint8(0)))
   fmt.Println(unsafe.Alignof(int16(0)))
   fmt.Println(unsafe.Alignof(uint16(0)))
   fmt.Println(unsafe.Alignof(int32(0)))
   fmt.Println(unsafe.Alignof(uint32(0)))
   fmt.Println(unsafe.Alignof(int64(0)))
   fmt.Println(unsafe.Alignof(uint64(0)))
   fmt.Println(unsafe.Alignof(uintptr(0)))
   fmt.Println(unsafe.Alignof(float32(0)))
   fmt.Println(unsafe.Alignof(float64(0)))
   //fmt.Println(unsafe.Alignof(complex(0, 0)))
   fmt.Println(unsafe.Alignof(complex64(0)))
   fmt.Println(unsafe.Alignof(complex128(0)))
   fmt.Println(unsafe.Alignof(""))
   fmt.Println(unsafe.Alignof(new(int)))
   fmt.Println(unsafe.Alignof(struct {
      f  float32
      ff float64
   }{}))
   fmt.Println(unsafe.Alignof(make(chan bool, 10)))
   fmt.Println(unsafe.Alignof(make([]int, 10)))
   fmt.Println(unsafe.Alignof(make(map[string]string, 10)))

   t := &T{1, 2, 3, "", true}
   fmt.Println("sizeof :")
   fmt.Println(unsafe.Sizeof(*t))
   fmt.Println(unsafe.Sizeof(t.t1))
   fmt.Println(unsafe.Sizeof(t.t2))
   fmt.Println(unsafe.Sizeof(t.t3))
   fmt.Println(unsafe.Sizeof(t.t4))
   fmt.Println(unsafe.Sizeof(t.t5))
	//這裏以0x0作爲基準內存地址。打印出來總共佔用40個字節。
	// t.t1 爲 char,對齊值爲 1,0x0 % 1 == 0,從0x0開始,佔用一個字節;
	// t.t2 爲 int32,對齊值爲 4,0x4 % 4 == 0,從 0x4 開始,佔用 4 個字節;
	// t.t3 爲 int64,對齊值爲 8,0x8 % 8 == 0,從 0x8 開始,佔用 8 個字節;
	// t.t4 爲 string,對齊值爲 8,0x16 % 8 == 0,從 0x16 開始, 佔用 16 個字節(string 內部實現是一個結構體,包含一個字節類型指針和一個整型的長度值);
	// t.t5 爲 bool,對齊值爲 1,0x32 % 8 == 0,從 0x32 開始,佔用 1 個字節。從上面分析,可以知道 t 的對齊值爲 8,最後 bool 之後會補齊到 8 的倍數,故總共是 40 個字節。

   fmt.Println("Offsetof : ")
   fmt.Println(unsafe.Offsetof(t.t1))
   fmt.Println(unsafe.Offsetof(t.t2))
   fmt.Println(unsafe.Offsetof(t.t3))
   fmt.Println(unsafe.Offsetof(t.t4))
   fmt.Println(unsafe.Offsetof(t.t5))

   writeByPointer()
   //CPU看待內存是以block爲單位的,就像是linux下文件大小的單位IO block爲4096一樣,
   //是一種犧牲空間換取時間的做法, 我們一定要注意不要浪費空間,
   //struct類型定義的時候一定要將佔用內從空間小的類型放在前面, 充足利用padding, 才能提升內存、cpu效率
}

輸出結果:

go run PLAY.go
unsafe.Pointer
*uint64
unsafe.Pointer
*uint64
0x402699999999999a 0b100000000010000000000000000000000000000000000000000000000000000 
99 uint 99 
8, 1, 4, 8, 16, 1, 4, 8
1
1
1
2
2
4
4
8
8
8
4
8
4
8
8
8
8
8
8
8
sizeof :
40
1
4
8
16
1
Offsetof : 
0
4
8
16
32
addr c00008e038 addr c00008e03a size 2 size 2 size 4 align 1 offset 4 
&{low:[1 2] high:9} 0xc00008a010 0xc00008e038 0xc00008e03c

image.png

uintptr

uintptr是golang的內置類型,是能存儲指針的整型,在64位平臺上底層的數據類型是

typedef unsigned long long int  uint64;
typedef uint64          uintptr;

一個unsafe.Pointer指針也可以被轉化爲uintptr類型,然後保存到指針型數值變量中(注:這只是和當前指針相同的一個數字值,並不是一個指針),然後用以做必要的指針數值運算。(uintptr是一個無符號的整型數,足以保存一個地址)這種轉換雖然也是可逆的,但是將uintptr轉爲unsafe.Pointer指針可能會破壞類型系統,因爲並不是所有的數字都是有效的內存地址。
許多將unsafe.Pointer指針轉爲原生數字,然後再轉回爲unsafe.Pointer類型指針的操作也是不安全的。比如下面的例子需要將變量x的地址加上b字段地址偏移量轉化爲*int16類型指針,然後通過該指針更新x.b:

package main

import (
    "fmt"
    "unsafe"
)

func main() {

    var x struct {
        a bool
        b int16
        c []int
    }

    /**
    unsafe.Offsetof 函數的參數必須是一個字段 x.f, 然後返回 f 字段相對於 x 起始地址的偏移量, 包括可能的空洞.
    */

    /**
    uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
    指針的運算
    */
    // 和 pb := &x.b 等價
    pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
    *pb = 42
    fmt.Println(x.b) // "42"
}

上面的寫法儘管很繁瑣,但在這裏並不是一件壞事,因爲這些功能應該很謹慎地使用。不要試圖引入一個uintptr類型的臨時變量,因爲它可能會破壞代碼的安全性(注:這是真正可以體會unsafe包爲何不安全的例子)。

下面段代碼是錯誤的:

// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

產生錯誤的原因很微妙。有時候垃圾回收器會移動一些變量以降低內存碎片等問題。這類垃圾回收器被稱爲移動GC。當一個變量被移動,所有的保存改變量舊地址的指針必須同時被更新爲變量移動後的新地址。從垃圾收集器的視角來看,一個unsafe.Pointer是一個指向變量的指針,因此當變量被移動是對應的指針也必須被更新;但是uintptr類型的臨時變量只是一個普通的數字,所以其值不應該被改變。上面錯誤的代碼因爲引入一個非指針的臨時變量tmp,導致垃圾收集器無法正確識別這個是一個指向變量x的指針。當第二個語句執行時,變量x可能已經被轉移,這時候臨時變量tmp也就不再是現在的&x.b地址。第三個向之前無效地址空間的賦值語句將徹底摧毀整個程序!

總結

第一是 unsafe.Pointer 可以讓你的變量在不同的指針類型轉來轉去,也就是表示爲任意可尋址的指針類型。第二是 uintptr 常用於與 unsafe.Pointer 打配合,用於做指針運算,和C (*void)指針一樣。

unsafe是不安全的,所以我們應該儘可能少的使用它,比如內存的操縱,這是繞過Go本身設計的安全機制的,不當的操作,可能會破壞一塊內存,而且這種問題非常不好定位。

當然必須的時候我們可以使用它,比如底層類型相同的數組之間的轉換;比如使用sync/atomic包中的一些函數時;還有訪問Struct的私有字段時;該用還是要用,不過一定要慎之又慎。

還有,整個unsafe包都是用於Go編譯器的,不用運行時,在我們編譯的時候,Go編譯器已經把他們都處理了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章