一文解惑 --go:linkname 指令
大家好,我是站長 polarisxu。
我之前寫過一篇文章:爲什麼 Go 標準庫中有些函數只有簽名,沒有函數體?,其中有一點就是 //go:linkname 這個指令。
Go 中類似的指令挺多的,比如 Go1.16 中的 //go:embed。前些天有人問我,爲什麼它用 //go:embed 不起作用?我一看,它是這麼寫的:// go:embed,不知道你看到問題了沒有?是的,指令是通過註釋的方式,但有三點要求,要特別注意:
-
//後不能有空格。有些人可能習慣//後不加空格。但一般認爲,//後應該加一個空格。不過 go 指令卻要求不能有空格,這是一個小 “坑”,得注意。所以上面那位朋友就是加了空格,導致出問題。(程序並不會報錯,只是沒有得到自己想要的結果) -
代碼和指令之間不能有空行或其他註釋。這一點應該還好,很多人不會用錯吧;
-
一般來說,使用指令需要導入相應的包。比如
//go:linkname指令要求導入 unsafe 包,一般會import _ "unsafe”,//go:embed指令,要求導入 embed 包。
有另外一位 Go 朋友「橘中祕士」微信私聊我:
大佬好,能不能寫一篇 linkname 的文章。目前已經有了一些初步概念,但是尚有一些疑團不是特別清晰。
//go:linkname localname remotename,其中 local 作爲佔位符 remote 作爲實現者或者 local 作爲實現者 remote 作爲佔位符都是可以的。目前理解的就是給 Symbol 添加了一個 Linkname,查找 Symbo l 的時候用 remote。
譬如 //go:linkname runtimeNano runtime.nanotime,runtimeNano 作爲佔位符 runtime.nanotime 提供實現,任何調用 runtimeNano 的地方實際替換爲對 runtime.nanotime 的調用,這種場景比較容易接受。
譬如 //go:linkname runtime_cmpstring runtime.cmpstring,runtime_cmpstring 提供實現 runtime.cmpstring 作爲佔位符,是不是這時符號表裏不存在 runtime_cmpstring 只有 runtime.cmpstring?
經過簡單溝通,他寫了一篇文章解決自己的困惑。希望對各位有幫助。以下是他寫的關於 //go:linkname 的文章(我做了一些調整)。
01 格式
//go:linkname local remote
remote 可以沒有,此時 remote 使用 local 的值,效果就是 local 被導出。
02 local 和 remote 同時爲函數
local 作爲佔位符,remote 作爲實現者
標準庫中的例子:
// 來自 time 包
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64
// 來自 runtime 包
//go:nosplit
func nanotime() int64 {
return nanotime1()
}
此時二進制文件中並沒有runtimeNano,直接轉化爲對runtime.nanotime的調用。
local 作爲實現者,remote 作爲佔位符
同樣來自標準庫。這裏存在函數沒有函數體,但是被反向引用。
// 在標準庫的一個 internal 中
//go:linkname runtime_cmpstring runtime.cmpstring
func runtime_cmpstring(a, b string) int {
l := len(a)
if len(b) < l {
l = len(b)
}
for i := 0; i < l; i++ {
c1, c2 := a[i], b[i]
if c1 < c2 {
return -1
}
if c1 > c2 {
return +1
}
}
if len(a) < len(b) {
return -1
}
if len(a) > len(b) {
return +1
}
return 0
}
// 來自 runtime
func cmpstring(string, string) int
此時二進制文件中並沒有runtime_cmpstring,對應的函數已經被命名爲runtime.cmpstring。也就是說,實現在 internal 包,但最終通過 runtime.cmpstring 來引用。
一個佔位符 + 一個彙編函數
// 在標準庫的一個 internal 中
//go:linkname abigen_runtime_memequal runtime.memequal
func abigen_runtime_memequal(a, b unsafe.Pointer, size uintptr) bool
注意runtime.memequal的實現並不在runtime包中,使用匯編實現的話並不要求必須在相應的包中。
# memequal(a, b unsafe.Pointer, size uintptr) bool
TEXT runtime·memequal(SB),NOSPLIT,$0-25
MOVQ a+0(FP), SI
MOVQ b+8(FP), DI
CMPQ SI, DI
JEQ eq
MOVQ size+16(FP), BX
LEAQ ret+24(FP), AX
JMP memeqbody<>(SB)
eq:
MOVB $1, ret+24(FP)
RET
03 local 和 remote 同時爲變量
兩個常規變量
//go:linkname overflowError runtime.overflowError
var overflowError error
//go:linkname divideError runtime.divideError
var divideError error
//go:linkname zeroVal runtime.zeroVal
var zeroVal [maxZero]byte
//go:linkname _iscgo runtime.iscgo
var _iscgo bool = true
//go:cgo_import_static x_cgo_setenv
//go:linkname x_cgo_setenv x_cgo_setenv
//go:linkname _cgo_setenv runtime._cgo_setenv
var x_cgo_setenv byte
var _cgo_setenv = &x_cgo_setenv
//go:cgo_import_static x_cgo_unsetenv
//go:linkname x_cgo_unsetenv x_cgo_unsetenv
//go:linkname _cgo_unsetenv runtime._cgo_unsetenv
var x_cgo_unsetenv byte
var _cgo_unsetenv = &x_cgo_unsetenv
一個佔位符 + 一個僞符號
//go:linkname runtime_inittask runtime..inittask
var runtime_inittask initTask
//go:linkname main_inittask main..inittask
var main_inittask initTask
注意是..inittask不是.inittask,而且.inittask只存在於編譯階段,任何包中都無法聲明該變量。
這裏額外解釋下 ..inittask 爲什麼兩個點。第一個點就是普通的 runtime. 這種調用方式,第二個點和 inittask 一起構成一個符號(變量)。注意,Go 中的變量是不允許以 . 開頭的,所以,這個叫僞符號,只在不編譯階段存在。
04 一個例子
研究 //go:linkname 是因爲如下的背景:
Java 裏有 InheritableThreadLocal,SpringWeb 在 ServletActionContext 裏使用它,達到在任何地方都能方便的獲取 HttpServletRequest。
Go 並沒有提供類似的機制,即使通過 stack 找到 goroutine id(99% 的文章都是這麼介紹的),再配合 sync.Map,也只是實現了一個比較粗糙的 ThreadLocal,在子協程裏仍然獲取不到父協程的內容。
g.label 雖然不是給這種場景準備的,但它具備了 InheritableThreadLocal 的一切要求,只要我們能夠訪問到 label 私有字段,我們就有了完整版的 InheritableThreadLocal。
下面這個例子是作者真實項目中用的。
在 runtime 和 runtime/pprof 包中有兩個函數:runtime_setProfLabel 和 runtime_getProfLabel。其中,runtime 包中的提供了實現,而 pprof 中的沒有提供實現。如果基於它們創建另外的函數,如下:
//go:linkname SetPointer runtime/pprof.runtime_setProfLabel
func SetPointer(ptr unsafe.Pointer)
//go:linkname GetPointer runtime/pprof.runtime_getProfLabel
func GetPointer() unsafe.Pointer
根據前面的分析,雖然runtime.runtime_setProfLabel/runtime.runtime_getProfLabel提供了函數實現,但是二進制文件中並不會出現(見下方代碼),此時想要調用必須通過runtime/pprof.runtime_setProfLabel/runtime/pprof.runtime_getProfLabel,這也是上面linkname到pprof而不是runtime的根本原因。
// 來自 runtime 包
//go:linkname runtime_setProfLabel runtime/pprof.runtime_setProfLabel
func runtime_setProfLabel(labels unsafe.Pointer) {
if raceenabled {
racereleasemerge(unsafe.Pointer(&labelSync))
}
getg().labels = labels
}
// 來自 runtime/pprof 包
func runtime_setProfLabel(labels unsafe.Pointer)
// 來自 runtime 包
//go:linkname runtime_getProfLabel runtime/pprof.runtime_getProfLabel
func runtime_getProfLabel() unsafe.Pointer {
return getg().labels
}
// 來自 runtime/pprof 包
func runtime_getProfLabel() unsafe.Pointer
05 總結
Go 中有不少指令,有些指令你可能不太需要關心,也不會用到。然而有些指令瞭解它們的意思,對閱讀相關代碼很有幫助。
這篇文章全面介紹了 //go:linkname 指令,不知道是否徹底解除了你的疑惑?歡迎留言交流!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/0rOBzMeFHKS0MLRtYGhHvw