Golang: unsafe 包真的不安全嗎?

在 Go 中,一般不建議使用 unsafe 包,因爲它「不安全」。本文探討這個問題。(由 Go 語言中文網公衆號注)

unsafe 包詳解

在烏克蘭的利沃夫舉行的 Lviv Golang community event[1] 中,我發表了一個關於 unsafe 包的演講,這個演講中我嘗試回答了標題中提到的問題:unsafe 包究竟有多 unsafe。

unsafe 包的名字就能感受到 Go 研發團隊的警告:使用這個包的代價將是巨大的。我覺得這個包名起的非常巧妙,它完美地符合了 《Effective Go》中對包名的所有建議。在使用 unsafe 包的時候,我們應當嚴格遵循 Go 研發團隊的文檔和建議。這個包的官方概述就只有簡單的一段話:

unsafe 包裏面包含了一些能讓你踐踏 Go 語言的類型安全特性的操作。golang.org[2]

附帶一段簡單的警告:

引用 unsafe 包可能會導致你代碼的不具備可移植性,並且不再受到 Go 1 兼容性規約的保護。golang.org[3]

函數功能的描述看起來非常的抽象,我們來瞅一眼這些 "unsafe" 的操作:

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

其中有一個 ArbitraryType 類型:

只是爲了文檔記錄的目的而存在,實際上它沒有參與到 unsafe 包的實現。這個類型代表了任意的 Go 語言表達式。

所以實際上 unsafe 包就只包含三個函數和一個類型,既然就這麼點東西,那我們試着把這個包全部過一遍。現在我們手頭上已經有了 Go 研發團隊給我的文檔和源碼,下一步要怎麼做?這時候不妨重溫一句名人名言:

多說無益,放碼過來 —— Linus Torvalds[4]

好,既然如此我們就直接看源碼吧……

Gif

神奇的事情發生了——這個 unsafe 包壓根就沒有源碼 [5] 呀。(Go 語言中文網公衆號注:builtin 也沒有源碼)它有函數的簽名和類型定義,但是沒有實現的代碼:無論是 Go 還是彙編的代碼都沒有。之所以會出現這個情況,是因爲 unsafe 包的功能需要在層次更低的編譯器層面實現,所以這個包其實是內置在編譯器裏面實現的,這個 .go 文件只是爲了達到文檔記錄的目的。所以我在上文反覆強調要嚴格遵循 Go 研發團隊的文檔和建議,因爲你也只能看到這些文檔。廢話不多說,先來看看 Sizeof 函數吧。

func Sizeof(x ArbitraryType) uintptr

函數接受某個變量,然後返回 uintptr 類型的結果。這個函數的名字可以看出,這個函數返回某個變量的大小。爲了理解方便,請允許我用幾個圖示來可視化一下這些概念。衆所周知,我們的 Go 程序需要內存來完成各種功能,其中就包含使用內存來保存變量。下面我將用這些標籤來表示內存:

🎁 - 1 個字節的內存

📦 - 1 個字節的被佔用的內存

🥡 - 1 個字節的被佔用但實際沒有作用的內存(後面會詳細解釋這個)

⬆️ - 指向內存地址的指針

下面我使用這些標籤來展示這個結構的內存佈局:

type X struct {
  n1 int16
  n2 int16
}

它在內存中的佈局是這樣的

Sizeof memory usage

X 結構體有兩個字段,其中每一個都佔 2 個字節,所以整個結構體佔用 size(n1) + size(n2) + size(X) = 2 + 2 + 0 = 4。顯然,下面語句是成立的:

unsafe.Sizeof(X) == 4 // true

func Offsetof(x ArbitraryType) uintptr

這個函數就有點難度了,函數簽名和上面的函數是同樣的,但是它返回的是 offset(偏移值)。我再次使用標籤來解釋這個機制——還是用剛纔的 X 結構體,還是同樣的兩個字段:

type X struct {
  n1 int16
  n2 int16
}

現在我們已經知道他在內存裏面是怎樣佈局的了,這一次我們來看看每個字段各佔多少個字節,內存分配的情況如下圖:

Offsetof memory usage

不難猜到,內存的佈局是這樣的:第一個字段 X.n1 佔了前 2 個字節,而第二個字段 X.n2 佔了接下來的 2 個字節。所以下面兩個語句都是成立的:

unsafe.Offsetof(X.n1) == 0 // true
unsafe.Offsetof(X.n2) == 2 // true

func Alignof(x ArbitraryType) uintptr

這個函數是最好玩的一個,因爲要透徹瞭解這個函數,你需要了解 alignment(數據結構對齊)[6] 是怎麼回事。簡單來說,它讓數據在內存中以某種的佈局來存放,使該數據的讀取能夠更加的快速。這個接收一個變量作爲參數,並返回這個變量的對齊字節。爲了更加直觀,我們需要修改一下上面的例子:

type X struct {
  n1 int8
  n2 int16
}

可以看到現在 n1 的類型變成了 int8,這會有什麼變化嗎,我們先看看 Sizeof, 因爲 n1 只佔 1 個字節了,所以合理地推測,X 結構體的大小會變成 3,因爲:size(X) = size(n1) + size(n2) = 1 + 2 = 3。但是現實真的如此嗎 ?

……

不是的,因爲 alignment 的緣故,X 結構體在內存的結構如下:

Alignof memory usage

由於 alignment 機制的要求,n2內存起始地址應該是自身大小的整數倍,也就是說它的起始地址只能是 0、2、4、6、8 等偶數,所以 n2 的起始地址沒有緊接着 n1 後面,而是空出了 1 個字節。最後導致結構體 X 的大小是 4 而不是 3。機智的讀者可能會想到:n1n2 換個位置會怎樣呢?這樣一來,n2 的起始地址是 0,而 n1 的其實地址是 2,這麼一來結構體 X 的大小就變成 3 了吧?答案是…… 不對的。原因還是因爲 alignment,因爲 alignment 除了要求字段的其實地址應該是自身大小的整數倍,還要求整個結構體的大小,是結構體中最大的字段的大小的整數倍,這使得結構體可以由多個內存塊組成,其中每個內存塊的大小都等於最大的字段的大小。我們可以利用這個知識來減少結構體的內存佔用。考察以下代碼:

type First struct {
 a int8
 b int64
 c int8
}

type Second struct {
 a int8
 c int8
 b int64
}

fmt.Println("Big brain time: ", unsafe.Sizeof(First{}) == unsafe.Sizeof(Second{}))

上面兩個結構體大小不同,是因爲 First 結構體由三個大小爲 8 字節的內存塊組成:Sizeof(First.a) + 7 個空閒的字節 + Sizeof(First.b) + Sizeof(First.c) + 7 個空閒的字節 = 24 字節。而 Second 結構體只包含  2 個 大小爲 8 字節的內存塊:Sizeof(Second.a) + Sizeof(Second.b) + 6 個空閒的字節 + Sizeof(Second.b) = 16 字節。下次你定義結構體的時候可以用上這個小知識🙂。

下面的代碼片段總結了上述三個函數的用法:

var x struct {
 a int64
 b bool
 c string
}

fmt.Println("Size of x: ", unsafe.Sizeof(x))
fmt.Println("Size of x.c: ", unsafe.Sizeof(x.c))

fmt.Println("Alignment of x.a: ", unsafe.Alignof(x.a))
fmt.Println("Alignment of x.b: ", unsafe.Alignof(x.b))
fmt.Println("Alignment of x.c: ", unsafe.Alignof(x.c))

fmt.Println("\nOffset of x.a: ", unsafe.Offsetof(x.a))
fmt.Println("Offset of x.b: ", unsafe.Offsetof(x.b))
fmt.Println("Offset of x.c: ", unsafe.Offsetof(x.c))

上述的三個方法都是在編譯期 [7] 執行的,這意味着只要它們在編譯器沒有報錯,在運行時不會有問題發生。但是我們的下一位嘉賓 unsafe.Pointer 可就沒那麼好惹了,它有可能會發生 運行時的錯誤)。我將會在本文的第二部分詳細介紹 unsafe.Pointer 以及使用它的過程中容易出現的問題。


via: https://www.dnahurnyi.com/is-unsafe-...unsafe-pt.-1/

作者:Denys Nahurnyi[8] 譯者:Alex-liutao[9] 校對:@unknwon[10]

本文由 GCTT[11] 原創編譯,Go 中文網 [12] 榮譽推出。轉載請聯繫我們授權!

參考資料

[1]

Lviv Golang community event: https://www.facebook.com/events/470065893928934/482981832637340/?notif_t=admin_plan_mall_activity&notif_id=1580732874088578

[2]

golang.org: https://golang.org/pkg/unsafe/#pkg-overview

[3]

golang.org: https://golang.org/pkg/unsafe/#pkg-overview

[4]

Linus Torvalds: https://lkml.org/lkml/2000/8/25/132

[5]

沒有源碼: https://golang.org/src/unsafe/unsafe.go

[6]

alignment(數據結構對齊): https://zh.wikipedia.org/wiki / 數據結構對齊

[7]

編譯期: https://en.wikipedia.org/wiki/Compile_time

[8]

Denys Nahurnyi: https://www.dnahurnyi.com/

[9]

Alex-liutao: https://github.com/Aelx-liutao

[10]

@unknwon: https://github.com/unknwon

[11]

GCTT: https://github.com/studygolang/GCTT

[12]

Go 中文網: https://studygolang.com/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rIqkKNUecvnZ6gadThf4gg