你不知道的Go unsafe.Pointer uintptr原理和玩法


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

 

 

uintptr

// uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr 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編譯器已經把他們都處理了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM