Go语言 go:linkname 用法示例 | Konica 的自留地

Go语言 go:linkname 用法示例

前言

先看Go文档的说明:

The //go:linkname directive instructs the compiler to use “importpath.name” as the object file symbol name for the variable or function declared as “localname” in the source code. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported “unsafe”.

go:linkname 是 Go 语言支持的一种指令,这个指令告诉编译器, 使用导入包的函数或者变量,作为当前包的函数或者变量在目标文件符号表中的符号。由于这个指令直接修改符合表,故不受函数或者变量访问权限的限制。

本文将举例说明 go:linkname 指令的比较hack的用法。

用法

GOPATH 的 src 目录结构如下:

goref/
    pkga/
        pkga.go
    main.go
    main.s

引用导入包的变量

goref/pkga/pkga.go 代码如下:

1
2
3
package pkga
var a = 1

goref/main.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"fmt"
_ "goref/pkga"
_ "unsafe"
)
//go:linkname a goref/pkga.a
var a int
func main() {
fmt.Println(a)
}

运行结果:

1

这里需要注意,go:linkname 指令必须在 var 之前,如果这样写:

1
2
3
4
var (
//go:linkname a goref/pkga.a
a int
)

是不能正常工作的!必须得这样:

1
2
3
4
//go:linkname a goref/pkga.a
var (
a int
)

修改导入包的变量

goref/pkga/pkga.go 代码如下:

1
2
3
4
5
6
7
package pkga
var a = 1
func Geta() int {
return a
}

goref/main.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
"goref/pkga"
_ "unsafe"
)
//go:linkname a goref/pkga.a
var a int
func main() {
a = 4
fmt.Println(pkga.Geta())
a++
fmt.Println(pkga.Geta())
}

运行结果:

4
5

这里需要注意,main包中不能直接在语句 var a inta 赋值,如果改成 var a = 4,则连接器会报错:

# goref
2019/02/10 12:14:14 duplicate symbol goref/pkga.a (types 28 and 28) in main and /Users/apple/Library/Caches/go-build/2a/2a48e7dd65bee2403ccf05bc434242ce87ee52507dbdbaa63d332827a54617aa-d(_go_.o)

可以引用当前包的变量

goref/main.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
_ "unsafe"
)
var a = 1
//go:linkname a2 main.a
var a2 int
func main() {
fmt.Println(a, a2)
}

运行结果:

1 1

此用法可模拟 C++ 语言中的引用。

但是只能引用一次

例如, 以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
_ "unsafe"
)
var a = 1
//go:linkname a2 main.a
var a2 int
//go:linkname a3 main.a
var a3 int
func main() {
fmt.Println(a, a2, a3)
}

是不能生成目标文件的,连接器报错如下:

# command-line-arguments
duplicate main.a
<autogenerated>:1: symbol main.a listed multiple times

修改一下代码, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
_ "unsafe"
)
var a = 1
//go:linkname a2 main.a
var a2 int
//go:linkname a3 main.a2
var a3 int
func main() {
fmt.Println(a, a2, a3)
}

运行结果:

1 1 0

a3 引用不到 a 的值,所以说只能引用一次。

引用导入包的私有成员函数

由于函数的操作与上文介绍的变量的操作大同小异,故此部分直开始介绍,如何引用导入包的私有成员函数。

goref/pkga/pkga.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package pkga
import (
"fmt"
)
type Student struct {
Name string
}
func (s *Student) say() {
fmt.Println(s.Name)
}

goref/main.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"goref/pkga"
_ "unsafe"
)
//go:linkname say goref/pkga.(*Student).say
func say(s *pkga.Student)
func main() {
s := pkga.Student{
Name: "shi",
}
say(&s)
}

运行结果:

shi

核心在于 func (s *Student) say() 实际上是 func say(s *Student) 的语法糖。这个函数在符号表中,表示为 goref/pkga.(*Student).say

如果是要引用私有对象的私有成员函数呢, 也没有关系

goref/pkga/pkga.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package pkga
import (
"fmt"
)
type student struct {
Name string
}
func (s *student) say() {
fmt.Println(s.Name)
}

goref/main.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
_ "goref/pkga"
_ "unsafe"
)
type st struct {
d string
}
//go:linkname say goref/pkga.(*student).say
func say(*st)
func main() {
s := st{
d: "shi",
}
say(&s)
}

运行结果:

shi

关键在于,构造一个成员类型完全一样的结构体, 如上述的

type student struct {
    Name string
}

为此构造结构体:

type st struct {
    d string
}

名称可以不一样,但是类型必须完全一样(例如这里的 string), 否则可能会出现非预期错误。

总结

本文将举例说明 go:linkname 指令的比较hack的用法,可以看到,这个指令主要是突破外部包函数和变量访问权限的限制,运用不当可能会出现非预期错误。除非万不得已,否则就不建议使用了。