Golang標準庫unsafe源碼解讀

當你閱讀Golang源碼時一定遇到過unsafe.Pointeruintptrunsafe.Sizeof等,是否很疑惑它們到底在做什麼?如果不瞭解這些底層代碼在發揮什麼作用,一定也無法瞭解上層應用構建的來由瞭,本篇我們來剖析下Golang標準庫的底層包unsafe!

unsafe包

我們基於Go1.16版本進行剖析,按照包的簡介內容描述是:unsafe包含的是圍繞Go程序安全相關的操作,導入unsafe包後構建的功能可能不被Go相關兼容性支持。

這裡和Java中的unsafe包功能類似,unsafe包中功能主要面向Go語言標準庫內部使用,一般業務開發中很少用到,除非是要做基礎能力的鋪建,對該包的使用應當是非常熟悉它的特性,對使用不當帶來的負面影響也要非常清晰。

unsafe構成

type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

可以看到,包的構成比較簡單,下面我們主要結合源碼中註釋內容來展開剖析和學習。

type ArbitraryType int

Arbitrary翻譯: 隨心所欲,任意的

type ArbitraryType int

ArbitraryType沒有什麼實質作用,它表示任意一種類型,實際上不是unsafe包的一部分。它表示任意Go表達式的類型。

type Pointer *ArbitraryType

type Pointer *ArbitraryType

Pointerunsafe包的核心。

靈活轉換

它表示指向任意類型的指針,有四種特殊操作可用於類型指針,而其他類型不可用,大概的轉換關系如下:

  • 任何類型的指針值都可以轉換為Pointer
  • Pointer可以轉換為任何類型的指針值
  • 任意uintptr可以轉換為Pointer
  • Pointer也可以轉換為任意uintptr

潛在的危險性

正是因為它有能力和各種數據類型之間建立聯系完成轉換,Pointer通常被認為是較為危險的,它能允許程序侵入系統並讀取和寫入任意內存,使用時應格外小心!!!

源碼註釋中列舉瞭提到瞭一些正確錯誤使用的例子。它還提到更為重要的一點是:不使用這些模式的代碼可能現在或者將來變成無效。即使下面的有效模式也有重要的警告。試圖來理解下這句話的核心就是,它不能對你提供什麼保證!

對於編碼的正確性還可以通過運行Golang提供的工具“go vet”可以幫助找到不符合這些模式的指針用法,但“go vet”並不能保證代碼一定一定是有效的。

go vetgolang中自帶的靜態分析工具,可以幫助檢測編寫代碼中一些隱含的錯誤並給出提示。比如下面故意編寫一個帶有錯誤的代碼,fmt.Printf%d需要填寫數值類型,為瞭驗證go vet效果,故意填寫字符串類型看看靜態分析效果。

代碼樣例:
func TestErr(t *testing.T) {
  fmt.Printf("%d","hello world")
}
運行:
`go vet unsafe/unsafe_test.go`
控制臺輸出提示: 
unsafe/unsafe_test.go:9:2: Printf format %d has arg "hello world" of wrong type string

✅ 正確的使用姿勢

以下涉及Pointer的模式是有效的,這裡給出幾個例子:

  • (1) 指針 *T1 轉化為 指針 *T2. T1、T2兩個變量共享等值的內存空間佈局,在不超過數據范圍的前提下,可以允許將一種類型的數據重新轉換、解釋為其他類型的數據。

下面我們操作一個樣例:聲明並開辟一個內存空間,然後基於該內存空間進行不同類型數據的轉換。

代碼如下:

// 步驟:
// (1) 聲明為一個int64類型
// (2) int64 -> float32
//(3) float32 -> int32
func TestPointerTypeConvert(t *testing.T) {
   //  (1) 聲明為一個int64類型
   int64Value := int64(20)
   // int64數據打印
   fmt.Println("int64類型的值:", int64Value)
   //打印:int64類型的值: 20
   fmt.Println("int64類型的指針地址:", &int64Value)
   //打印:int64類型的指針地址: 0xc000128218
   // (2) int64 -> float32
   float32Ptr := (*float32)(unsafe.Pointer(&int64Value))
   fmt.Println("float32類型的值:", *(*float32)(unsafe.Pointer(&int64Value)))
   //打印:float32類型的值: 2.8e-44
   fmt.Println("float32類型的指針地址:", (*float32)(unsafe.Pointer(&int64Value)))
   //打印:float32類型的指針地址: 0xc000128218
   // (3) float32 -> int32
   fmt.Println("int32類型的指針:", (*int32)(unsafe.Pointer(float32Ptr)))
   //打印:int32類型的指針: 0xc000128218
   fmt.Println("int32類型的值:", *(*int32)(unsafe.Pointer(float32Ptr)))
   //打印:int32類型的值: 20
}

小結 Pointer利用能夠和不同數據類型之間進行轉換的靈活特性,可以有效進行完成數據轉換、指針復制的功能

(2) Pointer 轉換為 uintptr(不包括返回的轉換)

  • 將指針轉換為uintptr將生成指向的值的內存地址,該地址為整數。
  • 這種uintptr通常用於打印。將uintptr轉換回指針通常無效,uintptr是整數,而不是引用。
  • 將指針轉換為uintptr將創建一個沒有指針語義的整數值。即使uintptr包含某個對象的地址,如果對象移動,垃圾收集器不會更新uintptr的值,uintptr也不會阻止對象被回收。
  • 其餘模式枚舉從uintptr到指針的唯一有效轉換。

(3) Pointer 轉換為 uintptr(包含返回的轉換,使用算術) 如果變量p指向一個分配的對象,它可以通過該對象轉換為uintptr,添加偏移量,並轉換回指針。

// (1) 聲明一個數組,持有兩個元素
// (2) 輸出第1個元素指針信息
// (3) 輸出第2個元素指針信息
// (4) 通過第一個元素指針地址加上偏移量可以得到第二個元素地址
// (5) 還原第二個元素的值
func TestUintptrWithOffset(t *testing.T) {
  // (1) 聲明一個數組,持有兩個元素
  p := []int{1,2}
  // (2) 輸出第1個元素指針信息
  fmt.Println("p[0]的指針地址:",&p[0])
  // p[0]的指針地址 0xc0000a0160
  ptr0 := uintptr(unsafe.Pointer(&p[0]))
  fmt.Println(ptr0)
  // 824634376544
  // (3) 輸出第2個元素指針信息
  fmt.Println("p[1]的指針地址:",&p[1])
  // p[1]的指針地址 0xc0000a0168
  ptr1 := uintptr(unsafe.Pointer(&p[1]))
  fmt.Println(ptr1)
  // 824634376552
  // (4) 通過第一個元素指針地址加上偏移量可以得到第二個元素指針地址
  offset := uintptr(unsafe.Pointer(&p[0])) + 8 //int類型占8字節
  ptr1ByOffset := unsafe.Pointer(offset)
  fmt.Println("p[0]的指針地址 + offset偏移量可以得到p[1]的指針地址:",ptr1ByOffset)
  // p[0]的指針地址 + offset偏移量可以得到p[1]的指針地址 0xc0000a0168
  // (5) 還原第二個元素的值
  fmt.Println("通過偏移量得到的指針地址還原值:",*(*int)(ptr1ByOffset))
  // 通過偏移量得到的指針地址還原值:2
}

小結

最常見的用途是訪問結構或數組元素中的字段:

  • 從指針添加、減去偏移量都是可操作的
  • 使用&^對指針進行舍入也是有效的,通常用於對齊
  • 要保證內存偏移量指向正確,指向有效的原始分配的對象的偏移量上

❌ 錯誤的使用姿勢

與C中不同的是,將指針指向到其原始分配結束之後是無效的:

//❌ 無效:分配空間外的端點
func TestOverOffset(t *testing.T) {
   // 聲明字符串變量str
   str := "abc"
   // 在str的內存偏移量基礎上增加瞭額外的一個偏移量得到一個新的內存偏移量,該內存地址是不存在的
   newStr := unsafe.Pointer(uintptr(unsafe.Pointer(&str)) + unsafe.Sizeof(str))
   // 這裡由於不存在該內存偏移量的對象,肯定求不到值,這裡的表現是一直阻塞等待
   fmt.Println(*(*string)(newStr))
}

註意,兩個轉換必須出現在同一個表達式中,它們之間隻有中間的算術運算。

//❌ 無效:在轉換回指針之前,uintptr不能存儲在變量中
u := uintptr(p)
p = unsafe.Pointer(u + offset)
//推薦如下這種方式,不要依靠中間變量來傳遞uintptr
p = unsafe.Pointer(uintptr(p) + offset)

請註意,指針必須指向已分配的對象,因此它不能是零。

//❌ 無效:零指針的轉換
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)
  • (4) 調用syscall.Syscall時將指針轉換為uintptr syscall包中的Syscall函數將其uintptr參數直接傳遞給操作系統,然後操作系統可能會根據調用的詳細信息,將其中一些重新解釋為指針。也就是說,系統調用實現隱式地將某些參數從uintptr轉換回指針。

如果必須將指針參數轉換為uintptr以用作參數,則該轉換必須出現在調用表達式本身之中:

syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

編譯器處理在程序集中實現的函數調用的參數列表中轉換為uintptr的指針,方法是安排保留引用的已分配對象(如果有),並在調用完成之前不移動,即使僅從類型來看,調用期間似乎不再需要該對象。

要使編譯器識別此模式,轉換必須出現在參數列表中:

//❌ 無效:在系統調用期間隱式轉換回指針之前,uintptr不能存儲在變量中,和上面提到的問題類似
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))

(5) 從uintptrPointer,包含反射(Reflect)、反射值指針(Reflect.Value.Pointer)、反射值地址(Reflect.Value.UnsafeAddr)的轉換結果

reflect的值方法名為PointerUnsafeAddr,返回類型為uintptr,而不是unsafe。防止調用者在不首先導入“unsafe”的情況下將結果更改為任意類型的指針。然而,這意味著結果是脆弱的,必須在調用後立即在同一表達式中轉換為Pointer

p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

與上述情況一樣,在轉換之前存儲結果是無效的

//❌ 無效:在轉換回指針之前,uintptr不能存儲在變量中,和上面提到的問題類似
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))

(6)reflect.SliceHeaderreflect.StringHeader的數據字段與Pointer的轉換 與前一種情況一樣,reflect.SliceHeaderreflect.StringHeader將字段數據聲明為uintptr,以防止調用方在不首先導入“unsafe”的情況下將結果更改為任意類型。

然而,這意味著SliceHeaderStringHeader僅在解釋實際切片(slice)或字符串值(string)的內容時有效。

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n

在此用法中,hdr.Data實際上是引用字符串頭中底層指針的另一種方式,而不是uintptr變量本身。

一般來說,reflect.SliceHeaderreflect.StringHeader應該僅用作那些指向實際為切片(slice)、字符串(string)的*reflect.SliceHeader*reflect.StringHeader,而不是普通的結構體。程序不應聲明或分配這些結構類型的變量。

// ❌ 無效: 直接聲明的Header不會將數據作為引用。
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p可能已經被回收

func Sizeof(x ArbitraryType) uintptr

Sizeof返回類型v本身數據所占用的字節數。返回值是“頂層”的數據占有的字節數。例如,若v是一個切片,它會返回該切片描述符的大小,而非該切片底層引用的內存的大小。

Go語言中非聚合類型通常有一個固定的大小
引用類型或包含引用類型的大小在32位平臺上是4字節,在64位平臺上是8字節

類型 分類 大小
bool 非聚合 1個字節
intN, uintN, floatN, complexN 非聚合 N/8個字節(例如float64是8個字節)
int, uint, uintptr 非聚合 1個機器字 (32位系統:1機器字=4字節; 64位系統:1機器字=8字節)
*T 聚合 1個機器字
string 聚合 2個機器字(data,len)
[]T 聚合 3個機器字(data,len,cap)
map 聚合 1個機器字
func 聚合 1個機器字
chan 聚合 1個機器字
interface 聚合 2個機器字(type,value)
type Model struct {
   //Field...
}
func TestSizeOf(t *testing.T) {
   boolSize := false
   intSize := 1
   int8Size := int8(1)
   int16Size := int16(1)
   int32Size := int32(1)
   int64Size := int64(1)
   arrSize := make([]int, 0)
   mapSize := make(map[string]string, 0)
   structSize := &Model{}
   funcSize := func() {}
   chanSize := make(chan int, 10)
   stringSize := "abcdefg"
   fmt.Println("bool sizeOf:", unsafe.Sizeof(boolSize))
   //bool sizeOf: 1
   fmt.Println("int sizeOf:", unsafe.Sizeof(intSize))
   //int sizeOf: 8
   fmt.Println("int8 sizeOf:", unsafe.Sizeof(int8Size))
   //int8 sizeOf: 1
   fmt.Println("int16 sizeOf:", unsafe.Sizeof(int16Size))
   //int16 sizeOf: 2
   fmt.Println("int32 sizeOf:", unsafe.Sizeof(int32Size))
   //int32 sizeOf: 4
   fmt.Println("int64 sizeOf:", unsafe.Sizeof(int64Size))
   //int64 sizeOf: 8
   fmt.Println("arrSize sizeOf:", unsafe.Sizeof(arrSize))
   //arrSize sizeOf: 24
   fmt.Println("structSize sizeOf:", unsafe.Sizeof(structSize))
   //structSize sizeOf: 8
   fmt.Println("mapSize sizeOf:", unsafe.Sizeof(mapSize))
   //mapSize sizeOf: 8
   fmt.Println("funcSize sizeOf:", unsafe.Sizeof(funcSize))
   //funcSize sizeOf: 8
   fmt.Println("chanSize sizeOf:", unsafe.Sizeof(chanSize))
   //chanSize sizeOf: 8
   fmt.Println("stringSize sizeOf:", unsafe.Sizeof(stringSize))
   //stringSize sizeOf: 16
}

func Offsetof(x ArbitraryType) uintptr

Offsetof返回類型v所代表的結構體字段f在結構體中的偏移量,它必須為結構體類型的字段的形式。換句話說,它返回該結構起始處與該字段起始處之間的字節數。

內存對齊 計算機在加載和保存數據時,如果內存地址合理地對齊的將會更有效率。由於地址對齊這個因素,一個聚合類型的大小至少是所有字段或元素大小的總和,或者更大因為可能存在內存空洞。\

內存空洞 編譯器自動添加的沒有被使用的內存空間,用於保證後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊

下面通過排列bool、string、int16類型字段的不同順序來演示下內存對齊時填充的內存空洞。

type BoolIntString struct {
   A bool
   B int16
   C string
}
type StringIntBool struct {
   A string
   B int16
   C bool
}
type IntStringBool struct {
   A int16
   B string
   C bool
}
type StringBoolInt struct {
   A string
   B bool
   C int16
}
func TestOffsetOf(t *testing.T) {
   bis := &BoolIntString{}
   isb := &IntStringBool{}
   sbi := &StringBoolInt{}
   sib := &StringIntBool{}
   fmt.Println(unsafe.Offsetof(bis.A)) // 0
   fmt.Println(unsafe.Offsetof(bis.B)) // 2
   fmt.Println(unsafe.Offsetof(bis.C)) // 8
   fmt.Println("")
   fmt.Println(unsafe.Offsetof(isb.A)) // 0
   fmt.Println(unsafe.Offsetof(isb.B)) // 8
   fmt.Println(unsafe.Offsetof(isb.C)) // 24
   fmt.Println("")
   fmt.Println(unsafe.Offsetof(sbi.A)) // 0
   fmt.Println(unsafe.Offsetof(sbi.B)) // 16
   fmt.Println(unsafe.Offsetof(sbi.C)) // 18
   fmt.Println("")
   fmt.Println(unsafe.Offsetof(sib.A)) // 0
   fmt.Println(unsafe.Offsetof(sib.B)) // 16
   fmt.Println(unsafe.Offsetof(sib.C)) // 18
}

以上是針對單個結構體內的內存對齊的測試演示,當多個結構體組合在一起時還會產生內存對齊,感興趣可以自行實踐並打印內存偏移量來觀察組合後產生的內存空洞。

func Alignof(x ArbitraryType) uintptr

Alignof返回類型v的對齊方式(即類型v在內存中占用的字節數);若是結構體類型的字段的形式,它會返回字段f在該結構體中的對齊方式。

type Fields struct {
   Bool    bool
   String  string
   Int     int
   Int8    int8
   Int16   int16
   Int32   int32
   Float32 float32
   Float64 float64
}
func TestAlignof(t *testing.T) {
   fields := &Fields{}
   fmt.Println(unsafe.Alignof(fields.Bool)) // 1
   fmt.Println(unsafe.Alignof(fields.String))// 8
   fmt.Println(unsafe.Alignof(fields.Int)) // 8
   fmt.Println(unsafe.Alignof(fields.Int8)) // 1
   fmt.Println(unsafe.Alignof(fields.Int16)) // 2
   fmt.Println(unsafe.Alignof(fields.Int32))  // 4
   fmt.Println(unsafe.Alignof(fields.Float32))  // 4
   fmt.Println(unsafe.Alignof(fields.Float64))  // 8
}

不同類型有著不同的內存對齊方式,總體上都是以最小可容納單位進行對齊的,這樣可以在兼顧以最小的內存空間填充來換取內存計算的高效性。

以上就是Golang標準庫unsafe源碼解讀的詳細內容,更多關於Golang標準庫unsafe的資料請關註WalkonNet其它相關文章!

推薦閱讀: