注释竟然还有特殊用途?一文解惑 //go:linkname 指令

开发 后端
Go 中类似的指令挺多的,比如 Go1.16 中的 //go:embed。前些天有人问我,为什么它用 //go:embed 不起作用?我一看,它是这么写的:// go:embed,不知道你看到问题了没有?

 

我之前写过一篇文章:为什么 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 
  • 1.

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() 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

此时二进制文件中并没有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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

此时二进制文件中并没有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 
  • 1.
  • 2.
  • 3.

注意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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

一个占位符+一个伪符号

//go:linkname runtime_inittask runtime..inittask 
var runtime_inittask initTask 
 
//go:linkname main_inittask main..inittask 
var main_inittask initTask 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

注意是..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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

根据前面的分析,虽然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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

05 总结

Go 中有不少指令,有些指令你可能不太需要关心,也不会用到。然而有些指令了解它们的意思,对阅读相关代码很有帮助。

这篇文章全面介绍了 //go:linkname 指令,不知道是否彻底解除了你的疑惑?欢迎留言交流!

本文转载自微信公众号「polarisxu」,可以通过以下二维码关注。转载本文请联系polarisxu公众号。

 

责任编辑:武晓燕 来源: polarisxu
相关推荐

2019-10-28 10:19:27

JVM 类加载器Java

2024-04-26 00:01:00

Go语言类型

2023-01-09 08:14:08

GoHttpServer

2022-11-09 09:15:31

ProtoBufGo语言

2023-03-14 09:03:20

Go语法脚本

2021-02-02 09:10:12

Go语言二进制

2021-09-27 07:39:52

Go初始化函数package

2021-12-20 07:59:07

Go语言结构体

2023-06-01 16:27:34

汇编语言函数

2023-11-25 09:41:34

GogRPCHandler

2021-01-26 05:19:56

语言Go Context

2024-03-26 00:17:51

Go语言IO

2020-08-27 07:34:50

Zookeeper数据结构

2020-03-31 14:40:24

HashMap源码Java

2024-04-12 12:19:08

语言模型AI

2020-10-26 09:18:50

RedisCluste

2023-12-22 19:59:15

2021-08-04 16:06:45

DataOps智领云

2022-03-24 08:51:48

Redis互联网NoSQL

2009-03-16 10:10:00

网卡设备故障IP地址
点赞
收藏

51CTO技术栈公众号