//go:linkname
Posted February 22, 2022 by quy-le ‐ 12 min read
Compiler directives
In Go programming language, to permit access to an indentifier (variable, struct’s fields/methods, functions…) from another package, you must export it 1 by changing the first character of the identifier to UPPERCASE.
For example:
1// -- example1/hello/hello.go --
2package hello
3
4var msg = "private message" // msg can only be called/used inside the package `hello`.
5var Message = "Exported message" // Message can be accessed from another package by `hello.Message`.
6
7
8// -- example1/main.go --
9package main
10
11import (
12 "fmt"
13 "my-module/hello"
14)
15
16func main() {
17 fmt.Println(hello.Message) // "Exported message"
18 fmt.Println(hello.msg) // Compile error, `msg` is not exported so it's inaccessable outside of package `hello`.
19}
This is basic knowledge in Go, nothing special here.
And what if I tell you there is a way for the main
package to access the unexported hello.msg
variable?
1// -- example1/hello/hello.go --
2package hello
3
4var msg = "private message"
5var Message = "Exported message"
6
7
8// -- example1/main.go --
9package main
10
11import (
12 "fmt"
13 "example1/hello"
14 _ "unsafe"
15)
16
17//go:linkname linkedMsg example1/hello.msg
18var linkedMsg string
19
20func main() {
21 fmt.Println(hello.Message) // "Exported message"
22 fmt.Println(linkedMsg) // "private message"
23}
24
25// On Go playground: https://play.golang.org/p/8Yew6Hh50Gt
You can compile and run the example above without error
We can see the private message
printed out, which means main
package can access to the unexported hello.msg
variable now. That is because of the //go:linkname
directive at line #17.
go:linkname
(along with some others, e.g.: go:noescape
, go:inline
, go:norace
…) are compiler directives or commonly known as pragmas that we can use to tell Go compiler to do something different/special than normal compiling. The general form of Go directives is:
1//go:directive [params]
Note that there’s no space in between the double slashes and the go:directive
keyword, this is because of legacy but it also helps distinguish the directives with normal comments.
Caution
- Compiler directives/pragmas are not a part of the Go programming language. It’s implemented in the compiler but with no guarantee that the directive behavior won’t be changed or even removed in the future.
Practically, you would never need to use these directives in user code. But while reading Go source code, you will see these directives quite frequently so it’s nice to understand.
And maybe it can be used (with caution in mind, of course) to do clever things as I will show you in the end of this post. - I only cover
go:linkname
directive in this post.
For other directives, I cannot recommend more that you should go read the official Godoc and this wonderful post from Dave Cheney where he explained in details the historical purposes and usage of these directives.
//go:linkname
1//go:linkname localname [importpath.name]
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”.
- Godoc
In another explanation, whenever seeing go:linkname
directive, Go compiler will replace all the localname
in source code by a direct reference to the importpath.name
.
The reference is defined in the compiled binary so it doesn’t matter if the importpath.name
is exported in source code or not, we can always access it directly (more about this in below section).
go:linkname
can be used to reference to an unexported (private) variable or function.go:linkname
can beplaced on the either the source or destination package and don’t have to be placed right above the linking variable/function. But should place right above for better tracking and clarity.- Must import
unsafe
package. - The package where
go:linkname
directive is defined must be imported.
1// -- go.mod --
2module expl
3
4
5// -- hello/hello.go --
6package hello
7
8import (
9 _ "unsafe"
10)
11
12// linkname directive don't have to be directly above the linking source/destination.
13//go:linkname privBytes expl/ciao.linkedBytes
14var privInt64 int64 = 5
15var privBytes []byte = []byte("hello")
16
17func privAddFunc(a int64) int64 {
18 return a + privInt64
19}
20
21func GetInt64() int64 { return privInt64 }
22func GetBytes() []byte { return privBytes }
23
24
25// -- ciao/ciao.go --
26package ciao
27
28import (
29 _ "expl/hello"
30 _ "unsafe"
31)
32
33var (
34 // linkname directive can be placed on either source or destination package.
35 //go:linkname linkedInt64 expl/hello.privInt64
36 linkedInt64 int64
37 linkedBytes []byte
38)
39
40// Link to private function
41//go:linkname LinkedAddFunc expl/hello.privAddFunc
42func LinkedAddFunc(a int64) int64
43
44func GetInt64() int64 { return linkedInt64 }
45func GetBytes() []byte { return linkedBytes }
46
47
48// -- main.go --
49package main
50
51import (
52 "fmt"
53 "expl/ciao"
54 "expl/hello"
55)
56
57func main() {
58 fmt.Printf("hello.privInt64=%d, ciao.linkedInt64=%d\n", hello.GetInt64(), ciao.GetInt64()) // 5
59 fmt.Printf("hello.privBytes=%s, ciao.linkedBytes=%s\n", hello.GetBytes(), ciao.GetBytes()) // "hello"
60 fmt.Printf("ciao.linkedAddFunc(10)=%d\n", ciao.LinkedAddFunc(10)) // 15
61}
62
63// On Go playground: https://play.golang.org/p/xwGIIx97x7B
We can verify the linkname reference by two ways as below:
1. Verify linkname reference by Go code
1// -- example2/hello/hello.go --
2package hello
3
4var age int64 = 5
5
6func GetAge() int64 {
7 return age
8}
9
10
11// -- example2/main.go --
12package main
13
14import (
15 "fmt"
16 "example2/hello"
17 _ "unsafe"
18)
19
20//go:linkname age example2/hello.age
21var age int64
22
23func main() {
24 fmt.Println(age)
25 age = 10
26 fmt.Println(hello.GetAge())
27}
Run the example2 above:
We can see that in the code we only change the main.age
but that change also refect to the hello.age
, which suggest linkname is a reference.
2. Verify linkname reference by compiled Assembly
1$ go build -o expl3 main.go && go tool objdump -s 'main.main' expl3
2TEXT main.main(SB) example3/main.go
3 main.go:7 0x104ea20 48c705edd008000a000000 MOVQ $0xa, main.age(SB)
4 main.go:8 0x104ea2b c3 RET
At line #3, we see the instruction to set 10 (0xA) to the main.age
variable which has the address of 0x0104ea20
. Nothing special here.
Now try to use go:linkname to link the hello.age
variable to main.age
and see what’s the difference in Assembly code.
1// -- example3/main.go --
2package main
3
4import _ "unsafe"
5
6//go:linkname age example3/hello.age
7var age int64
8
9func main() {
10 age = 10
11}
12
13// -- example3/hello/hello.go --
14package hello
15
16var age int64 = 5
1$ go build -o expl3 main.go && go tool objdump -s 'main.main' expl3
2TEXT main.main(SB) example3/main.go
3 main.go:9 0x104ea20 48c705e5d008000a000000 MOVQ $0xa, example3/hello.age(SB)
4 main.go:10 0x104ea2b c3 RET
At line #3, we can see that now the main.age = 10
code has been translated directly to example3/hello.age = 10
.
In the code we did define the main.age
variable, but searching in the compiled file, I couldn’t locate the location of main.age
anymore. And along with the Assembly code above suggest that all the main.age
accesses are now referenced directly to the example3/hello.age
variable.
Which explained why on the Go code we only changed the local variable (main.age
) but it also reflect to the original variable (example3/hello.age
).
Exceptional uses
Place the
go:linkname
at source or destination file doesn’t change the linking behavior, but on the compiled code level, it’s different.go:linkname
always replace thelocalname
by theimpothpath.name
reference.Place the
go:linkname
in the destination package (ciao
).1// -- hello/hello.go -- 2var privInt64 int64 = 5 3 4// -- ciao/ciao.go -- 5//go:linkname linkedInt64 full/hello.privInt64 6var linkedInt64 int64 7 8// -- main.go -- 9func main() { 10 a = hello.GetPrivInt64() 11 b = ciao.GetLinkedInt64() 12} 13 14 15$ go build -o c3 main.go && go tool objdump -s 'main.main' c3 16TEXT main.main(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/main.go 17 main.go:13 0x105e180 488b0501a00600 MOVQ full/hello.privInt64(SB), AX 18 main.go:13 0x105e187 4889057aa30900 MOVQ AX, main.a(SB) 19 main.go:14 0x105e18e 488b05f39f0600 MOVQ full/hello.privInt64(SB), AX 20 main.go:14 0x105e195 48890574a30900 MOVQ AX, main.b(SB) 21 main.go:15 0x105e19c c3 RET
Place the
go:linkname
in the source package (hello
).1// -- hello/hello.go -- 2//go:linkname privInt64 full/ciao.linkedInt64 3var privInt64 int64 = 5 4 5// -- ciao/ciao.go -- 6var linkedInt64 int64 7 8// -- main.go -- 9func main() { 10 a = hello.GetPrivInt64() 11 b = ciao.GetLinkedInt64() 12} 13 14 15$ go build -o c3 main.go && go tool objdump -s 'main.main' c3 16TEXT main.main(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/main.go 17 main.go:13 0x105e180 488b0501a00600 MOVQ full/ciao.linkedInt64(SB), AX 18 main.go:13 0x105e187 4889057aa30900 MOVQ AX, main.a(SB) 19 main.go:14 0x105e18e 488b05f39f0600 MOVQ full/ciao.linkedInt64(SB), AX 20 main.go:14 0x105e195 48890574a30900 MOVQ AX, main.b(SB) 21 main.go:15 0x105e19c c3 RET
Should only place
go:linkname
on either source or destination package, not both. Declarego:linkname
on both two packages still works as intended but it’s easy to get lost.Initialize the variable/function on both 2 places and also link it causes compile fail with
duplicated definition of symbol
.You can actually link to or from the struct’s private methods too, but it’s easy to get SEGFAULT, so do not try it.
hello/hello.go | ciao/ciao.go | stdout | Assembly | |
---|---|---|---|---|
1 | var privInt64 int64 = 5 | var linkedInt64 int64 | hello.privInt64=5 ciao.linkedInt64=0 | go build -o c3 main.go && go tool objdump -s ‘main.main’ c3 TEXT main.main(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/main.go main.go:13 0x105e180 488b0501a00600 MOVQ full/hello.privInt64(SB), AX main.go:13 0x105e187 48890582a30900 MOVQ AX, main.a(SB) main.go:14 0x105e18e 488b056ba30900 MOVQ full/ciao.linkedInt64(SB), AX main.go:14 0x105e195 4889057ca30900 MOVQ AX, main.b(SB) main.go:15 0x105e19c c3 RET |
2 | var privInt64 int64 = 5 | //go:linkname linkedInt64 full/hello.privInt64 var linkedInt64 int64 | hello.privInt64=5 ciao.linkedInt64=5 | go build -o c3 main.go && go tool objdump -s ‘main.main’ c3 TEXT main.main(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/main.go main.go:13 0x105e180 488b0501a00600 MOVQ full/hello.privInt64(SB), AX main.go:13 0x105e187 4889057aa30900 MOVQ AX, main.a(SB) main.go:14 0x105e18e 488b05f39f0600 MOVQ full/hello.privInt64(SB), AX main.go:14 0x105e195 48890574a30900 MOVQ AX, main.b(SB) main.go:15 0x105e19c c3 RET |
3 | //go:linkname privInt64 full/ciao.linkedInt64 var privInt64 int64 = 5 | var linkedInt64 int64 | hello.privInt64=5 ciao.linkedInt64=5 | go build -o c3 main.go && go tool objdump -s ‘main.main’ c3 TEXT main.main(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/main.go main.go:13 0x105e180 488b0501a00600 MOVQ full/ciao.linkedInt64(SB), AX main.go:13 0x105e187 4889057aa30900 MOVQ AX, main.a(SB) main.go:14 0x105e18e 488b05f39f0600 MOVQ full/ciao.linkedInt64(SB), AX main.go:14 0x105e195 48890574a30900 MOVQ AX, main.b(SB) main.go:15 0x105e19c c3 RET |
4 | var privInt64 int64 = 5 | //go:linkname linkedInt64 full/hello.privInt64 var linkedInt64 int64 = 10 | Compile error: duplicated definition of symbol full/hello.privInt64 | |
5 | //go:linkname privInt64 full/ciao.linkedInt64 var privInt64 int64 = 5 | var linkedInt64 int64 = 10 | Compile error: duplicated definition of symbol full/ciao.linkedInt64 | |
6 | //go:linkname privInt64 full/ciao.linkedInt64 var privInt64 int64 = 5 | //go:linkname linkedInt64 full/hello.privInt64 var linkedInt64 int64 | hello.privInt64=5 ciao.linkedInt64=0 | go build -o c3 main.go && go tool objdump -s ‘main.main’ c3 TEXT main.main(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/main.go main.go:14 0x105e180 488b0501a00600 MOVQ full/ciao.linkedInt64(SB), AX main.go:14 0x105e187 48890582a30900 MOVQ AX, main.a(SB) main.go:15 0x105e18e 488b056ba30900 MOVQ full/hello.privInt64(SB), AX main.go:15 0x105e195 4889057ca30900 MOVQ AX, main.b(SB) main.go:18 0x105e19c c3 RET |
7 | //go:linkname privInt64 full/ciao.linkedInt64 var privInt64 int64 | //go:linkname linkedInt64 full/hello.privInt64 var linkedInt64 int64 = 10 | hello.privInt64=0 ciao.linkedInt64=10 | go build -o c3 main.go && go tool objdump -s ‘main.main’ c3 TEXT main.main(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/main.go main.go:14 0x105e180 488b0579a30900 MOVQ full/ciao.linkedInt64(SB), AX main.go:14 0x105e187 48890582a30900 MOVQ AX, main.a(SB) main.go:15 0x105e18e 488b05f39f0600 MOVQ full/hello.privInt64(SB), AX main.go:15 0x105e195 4889057ca30900 MOVQ AX, main.b(SB) main.go:18 0x105e19c c3 RET |
8 | //go:linkname privInt64 full/ciao.linkedInt64 var privInt64 int64 = 5 | //go:linkname linkedInt64 full/hello.privInt64 var linkedInt64 int64 = 10 | hello.privInt64=5 ciao.linkedInt64=10 | go build -o c3 main.go && go tool objdump -s ‘main.main’ c3 TEXT main.main(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/main.go main.go:14 0x105e180 488b0501a00600 MOVQ full/ciao.linkedInt64(SB), AX main.go:14 0x105e187 4889057aa30900 MOVQ AX, main.a(SB) main.go:15 0x105e18e 488b05fb9f0600 MOVQ full/hello.privInt64(SB), AX main.go:15 0x105e195 48890574a30900 MOVQ AX, main.b(SB) main.go:18 0x105e19c c3 RET |
9 | type Public struct {} func (p Public) getPrivInt64() int64 { return privInt64 } | //go:linkname LinkPrivateStructMethodToFunc full/hello.Public.getPrivInt64 func LinkPrivateStructMethodToFunc() int64 | hello.privInt64=5 ciao.linkedInt64=0 ciao.LinkPrivateStructMethodToFunc=5 | go build -o c3 main.go && go tool objdump -s ‘Public.getPrivInt64’ c3 TEXT full/hello.Public.getPrivInt64(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/hello/hello.go hello.go:20 0x105e180 488b0501a00600 MOVQ full/hello.privInt64(SB), AX hello.go:20 0x105e187 4889442408 MOVQ AX, 0x8(SP) hello.go:20 0x105e18c c3 RET |
10 | type Public struct { field string } func (p Public) getPrivInt64() int64 { return privInt64 } | //go:linkname LinkPrivateStructMethodToFunc full/hello.Public.getPrivInt64 func LinkPrivateStructMethodToFunc() int64 | SEGFAULT: hello.privInt64=5 ciao.linkedInt64=0 ciao.LinkPrivateStructMethodToFunc=17733920 | go build -o c3 main.go && go tool objdump -s ‘Public.getPrivInt64’ c3 TEXT full/hello.Public.getPrivInt64(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/hello/hello.go hello.go:21 0x105e180 488b0501a00600 MOVQ full/hello.privInt64(SB), AX hello.go:21 0x105e187 4889442418 MOVQ AX, 0x18(SP) hello.go:21 0x105e18c c3 RET |
11 | var linkedInt64 int64 = 10 type Public struct { field string } func (p Public) GetLinkedInt64() int64 | //go:linkname getLinkedInt64 full/hello.Public.GetLinkedInt64 //go:noinline func getLinkedInt64() int64 { return linkedInt64 } | SEGFAULT: hello.Public{}.GetLinkedInt64()=0 | go build -o c3 main.go && go tool objdump -s ‘Public.GetLinkedInt64’ c3 TEXT full/hello.Public.GetLinkedInt64(SB) /Users/quy.l/ws/projects/go/modules/www/write/content/blog/go-linkname/code/full/ciao/ciao.go ciao.go:21 0x10a4600 488b05416c0a00 MOVQ full/ciao.linkedInt64(SB), AX ciao.go:21 0x10a4607 4889442408 MOVQ AX, 0x8(SP) ciao.go:21 0x10a460c c3 RET |
Clever uses
1. Read the value of unexported struct’s private field
1// -- hello/hello.go --
2package hello
3
4// Objective: From another package, how can we read the value of privStruct.field?
5type privStruct struct {
6 field int64
7}
8
9func getPrivStruct() *privStruct {
10 return &privStruct{ field: 100 }
11}
12
13// -- ciao/ciao.go --
14package ciao
15
16import (
17 _ "unsafe"
18)
19
20// This struct is the copied of hello/hello.privStruct
21type copiedPrivStruct struct {
22 field int64
23}
24
25//go:linkname getHelloPrivStruct full/hello.getPrivStruct
26func getHelloPrivStruct() *copiedPrivStruct
27
28func ResolveHelloPrivStructField() int64 {
29 p := getHelloPrivStruct()
30 return p.field // We can read the 100 value from hello.getPrivStruct here without error
31}
The idea of this usage came from the process of somehow we can read the goroutine’s ID. If we can go:linkname
the runtime.getg()
function which returns the current goroutine g
. Inside the g
struct, there is a goid
field which is the goroutine ID.
Note: This approach is failed because we cannot link runtime.getg()
, because that function itself has been implemented/linked from the Assembly directly.
Take a look at this package when the author have to implement getg
function in Assembly instead.
2. Access to unexported TLS 1.3 cipher suites
The author of this post want to write a tool to scan all the supported TLS ciphers of server but Go’s crypto/tls
package doesn’t expose the list of default cipher suites for TLS 1.3.
So he used go:linkname
to have access to the unexported list.
3. Access to runtime function to trace goroutine’s allocated memory
We can monitor the process memory in Go, but for occasional fine tuning, we may want to track the number of heap allocated bytes, objects or calls per goroutine.
In the way to achive that goal, the author of this post had to use go:linkname
to get access to the unexported mallocgc
function from runtime
package.
Go spec - Exported Identifiers: https://golang.org/ref/spec#Exported_identifiers. ↩︎