Go 語(yǔ)言 unsafe.Pointer

2023-03-14 17:00 更新

原文鏈接:https://gopl-zh.github.io/ch13/ch13-02.html


13.2. unsafe.Pointer

大多數(shù)指針類型會(huì)寫成*T,表示是“一個(gè)指向T類型變量的指針”。unsafe.Pointer是特別定義的一種指針類型(譯注:類似C語(yǔ)言中的void*類型的指針),它可以包含任意類型變量的地址。當(dāng)然,我們不可以直接通過(guò)*p來(lái)獲取unsafe.Pointer指針指向的真實(shí)變量的值,因?yàn)槲覀儾⒉恢雷兞康木唧w類型。和普通指針一樣,unsafe.Pointer指針也是可以比較的,并且支持和nil常量比較判斷是否為空指針。

一個(gè)普通的*T類型指針可以被轉(zhuǎn)化為unsafe.Pointer類型指針,并且一個(gè)unsafe.Pointer類型指針也可以被轉(zhuǎn)回普通的指針,被轉(zhuǎn)回普通的指針類型并不需要和原始的*T類型相同。通過(guò)將*float64類型指針轉(zhuǎn)化為*uint64類型指針,我們可以查看一個(gè)浮點(diǎn)數(shù)變量的位模式。

package math

func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }

fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"

通過(guò)轉(zhuǎn)為新類型指針,我們可以更新浮點(diǎn)數(shù)的位模式。通過(guò)位模式操作浮點(diǎn)數(shù)是可以的,但是更重要的意義是指針轉(zhuǎn)換語(yǔ)法讓我們可以在不破壞類型系統(tǒng)的前提下向內(nèi)存寫入任意的值。

一個(gè)unsafe.Pointer指針也可以被轉(zhuǎn)化為uintptr類型,然后保存到指針型數(shù)值變量中(譯注:這只是和當(dāng)前指針相同的一個(gè)數(shù)字值,并不是一個(gè)指針),然后用以做必要的指針數(shù)值運(yùn)算。(第三章內(nèi)容,uintptr是一個(gè)無(wú)符號(hào)的整型數(shù),足以保存一個(gè)地址)這種轉(zhuǎn)換雖然也是可逆的,但是將uintptr轉(zhuǎn)為unsafe.Pointer指針可能會(huì)破壞類型系統(tǒng),因?yàn)椴⒉皇撬械臄?shù)字都是有效的內(nèi)存地址。

許多將unsafe.Pointer指針轉(zhuǎn)為原生數(shù)字,然后再轉(zhuǎn)回為unsafe.Pointer類型指針的操作也是不安全的。比如下面的例子需要將變量x的地址加上b字段地址偏移量轉(zhuǎn)化為*int16類型指針,然后通過(guò)該指針更新x.b:

gopl.io/ch13/unsafeptr

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

// 和 pb := &x.b 等價(jià)
pb := (*int16)(unsafe.Pointer(
    uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"

上面的寫法盡管很繁瑣,但在這里并不是一件壞事,因?yàn)檫@些功能應(yīng)該很謹(jǐn)慎地使用。不要試圖引入一個(gè)uintptr類型的臨時(shí)變量,因?yàn)樗赡軙?huì)破壞代碼的安全性(譯注:這是真正可以體會(huì)unsafe包為何不安全的例子)。下面段代碼是錯(cuò)誤的:

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

產(chǎn)生錯(cuò)誤的原因很微妙。有時(shí)候垃圾回收器會(huì)移動(dòng)一些變量以降低內(nèi)存碎片等問(wèn)題。這類垃圾回收器被稱為移動(dòng)GC。當(dāng)一個(gè)變量被移動(dòng),所有的保存該變量舊地址的指針必須同時(shí)被更新為變量移動(dòng)后的新地址。從垃圾收集器的視角來(lái)看,一個(gè)unsafe.Pointer是一個(gè)指向變量的指針,因此當(dāng)變量被移動(dòng)時(shí)對(duì)應(yīng)的指針也必須被更新;但是uintptr類型的臨時(shí)變量只是一個(gè)普通的數(shù)字,所以其值不應(yīng)該被改變。上面錯(cuò)誤的代碼因?yàn)橐胍粋€(gè)非指針的臨時(shí)變量tmp,導(dǎo)致垃圾收集器無(wú)法正確識(shí)別這個(gè)是一個(gè)指向變量x的指針。當(dāng)?shù)诙€(gè)語(yǔ)句執(zhí)行時(shí),變量x可能已經(jīng)被轉(zhuǎn)移,這時(shí)候臨時(shí)變量tmp也就不再是現(xiàn)在的&x.b地址。第三個(gè)向之前無(wú)效地址空間的賦值語(yǔ)句將徹底摧毀整個(gè)程序!

還有很多類似原因?qū)е碌腻e(cuò)誤。例如這條語(yǔ)句:

pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯(cuò)誤!

這里并沒有指針引用new新創(chuàng)建的變量,因此該語(yǔ)句執(zhí)行完成之后,垃圾收集器有權(quán)馬上回收其內(nèi)存空間,所以返回的pT將是無(wú)效的地址。

雖然目前的Go語(yǔ)言實(shí)現(xiàn)還沒有使用移動(dòng)GC(譯注:未來(lái)可能實(shí)現(xiàn)),但這不該是編寫錯(cuò)誤代碼僥幸的理由:當(dāng)前的Go語(yǔ)言實(shí)現(xiàn)已經(jīng)有移動(dòng)變量的場(chǎng)景。在5.2節(jié)我們提到goroutine的棧是根據(jù)需要?jiǎng)討B(tài)增長(zhǎng)的。當(dāng)發(fā)生棧動(dòng)態(tài)增長(zhǎng)的時(shí)候,原來(lái)?xiàng)V械乃凶兞靠赡苄枰灰苿?dòng)到新的更大的棧中,所以我們并不能確保變量的地址在整個(gè)使用周期內(nèi)是不變的。

在編寫本文時(shí),還沒有清晰的原則來(lái)指引Go程序員,什么樣的unsafe.Pointer和uintptr的轉(zhuǎn)換是不安全的(參考 Issue7192 ). 譯注: 該問(wèn)題已經(jīng)關(guān)閉),因此我們強(qiáng)烈建議按照最壞的方式處理。將所有包含變量地址的uintptr類型變量當(dāng)作BUG處理,同時(shí)減少不必要的unsafe.Pointer類型到uintptr類型的轉(zhuǎn)換。在第一個(gè)例子中,有三個(gè)轉(zhuǎn)換——字段偏移量到uintptr的轉(zhuǎn)換和轉(zhuǎn)回unsafe.Pointer類型的操作——所有的轉(zhuǎn)換全在一個(gè)表達(dá)式完成。

當(dāng)調(diào)用一個(gè)庫(kù)函數(shù),并且返回的是uintptr類型地址時(shí)(譯注:普通方法實(shí)現(xiàn)的函數(shù)盡量不要返回該類型。下面例子是reflect包的函數(shù),reflect包和unsafe包一樣都是采用特殊技術(shù)實(shí)現(xiàn)的,編譯器可能給它們開了后門),比如下面反射包中的相關(guān)函數(shù),返回的結(jié)果應(yīng)該立即轉(zhuǎn)換為unsafe.Pointer以確保指針指向的是相同的變量。

package reflect

func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)