首頁 > 軟體

Golang標準庫unsafe原始碼解讀

2022-08-11 18:01:42

引言

當你閱讀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標準庫檔案

《Go語言聖經》底層程式設計章節

以上就是Golang標準庫unsafe原始碼解讀的詳細內容,更多關於Golang標準庫unsafe的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com