ちりもつもればミルキーウェイ

好奇心に可処分時間が奪われる

Goのruntimeコードリーディングで役立つかもしれない個人的tipsまとめ

はじめに

これは Kyash Advent Calendar 2021 の1日目の記事です。

Kyashで入出金関連をメインで担当しているチームで働いている convto です。

最近趣味でGoのruntime周りのソースコードを読み始めているんですが、Goのruntime(や一部の標準パッケージ、それ以外にも最適化が必要なライブラリなど)では普段のコードではあまり見ない機能を使っている場合があります。
runtime周りを読み進める上で知っておくとスムーズに読めるような知識をいくつかまとめてみました。

アセンブリで実装されるケースがある

Goはplan9ベースのアセンブリを記述できます。
Goで利用するアセンブリについては https://go.dev/doc/asm に詳しいです。

アセンブリで実装されている場合はGo側のファイルでは関数定義しかされていないので注意が必要です。
たとえば以下のようなmain.goとadd.sが実行できます。

package main

import "fmt"

func add(a,b int) in

func main() {
    fmt.Println(add(100, 50))
}

asmで実装する場合は、Goのファイルには関数のみ定義して実装は記載しません。
以下のようにasmのファイルで宣言された関数を実装します。

// func add(a, b int)
TEXT ·add(SB),$0
    MOVL a+0(FP),AX
    ADDQ b+8(FP),AX
    MOVL AX, ret+16(FP)
    RET

このようにすると、以下のように実行できます。

$ go run ./
150

playground: https://go.dev/play/p/nfuWejN7ZPY

Goではruntimeやmath, crypto関連などの一部パッケージで、アセンブリによる実装をアーキテクチャごとに準備していることがあり、そこではこのような書き方がされています。

ディレクティブが指定されると特別な挙動をする場合がある

Goでは、特殊な形式なコメントの形でコンパイラに要求を伝えることができます。
最近追加された go:embed などは知っている方も多いのではないでしょうか。

runtimeまわりに関連するディレクティブはおおよそ https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives にまとめられています。

基本的には都度上記のドキュメントをみたり、上記にないディレクティブを見かけたら調べたりすればよいですが、 linkname ディレクティブは少し面白いのでここでかるく紹介します。

linknameディレクティブ

このディレクティブは //go:linkname localname [importpath.name] のように記載します。

さきほどの compiler directive についてのドキュメントでは、以下のように説明されています。

This special directive does not apply to the Go code that follows it. Instead, 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. If the “importpath.name” argument is omitted, the directive uses the symbol's default object file symbol name and only has the effect of making the symbol accessible to other packages. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported "unsafe".

要は別パッケージの任意の関数などについて、現在のパッケージ内で別名をつけて参照できるようになるというディレクティブです。
linknameを利用する側のGoファイルでは( importpath.name が省略された場合は)関数などの定義しか行わないので、これも実装が別のファイルに置かれることになります。

これを利用すると、公開されていない非公開関数などを別パッケージから利用できます。

たとえば以下の例はreflectの非公開関数を使って、定義されている型を公開/非公開問わずに列挙しています。

package main

import (
    "fmt"
    "log"
    "reflect"
    "unsafe"
)

//go:linkname typelinks reflect.typelinks
func typelinks() ([]unsafe.Pointer, [][]int32)

//go:linkname rtypeOff reflect.rtypeOff
func rtypeOff(unsafe.Pointer, int32) unsafe.Pointer

func main() {
    sections, offsets := typelinks()
    if len(sections) != 1 {
        log.Fatal("failed to get sections")
    }
    if len(offsets) != 1 {
        log.Fatal("failed to get offsets")
    }
    for i, base := range sections {
        for _, offset := range offsets[i] {
            typeAddr := rtypeOff(base, offset)
            typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&typeAddr)))
            fmt.Println(typ)
        }
    }
}

playground: https://go.dev/play/p/Xjpt2bmfmcV

Goのruntimeなどには、linknameディレクティブを利用したコードが随所に見られるので知っておくとすこしコードが追いやすくなるかもしれません。
(linknameの指定は単に機能としても面白いので、知っていると色々遊んだりできます)

go tool objdump によるビルトイン関数の実装の探し方

ビルトイン関数は型ごとに準備されていることもあり、実装がどこにあるのか分かりづらいです。また、たとえば <- によるチャネルの受送信などもビルトイン関数ですがこのように名称がイメージしづらい処理は実装が探しづらいです。

そのような場合は僕は小さい構成のコードで go tool objdump を実行したりしています。

たとえば new() というビルトイン関数について実装を調べたいとします。まず以下のようなできる限り小さい new() を呼び出すようなGoのコードを書きます。

package main

import "fmt"

func main() {
    fmt.Println(new(string))
}

これをbuildし、 go tool objdump にかけます。

$ go build -o test
$ go tool objdump test

そうすると、runtimeを含むアセンブリが出力されます。
この中から TEXT main.main(SB) となっているものがmain関数なので、その部分の内容を探してみます。
僕の環境では以下のように出力されました。

TEXT main.main(SB) /Users/yuya.okumura/go/src/github.com/convto/playground/main.go
  main.go:5     0x1089f60       493b6610        CMPQ 0x10(R14), SP          
  main.go:5     0x1089f64       7660            JBE 0x1089fc6               
  main.go:5     0x1089f66       4883ec40        SUBQ $0x40, SP              
  main.go:5     0x1089f6a       48896c2438      MOVQ BP, 0x38(SP)           
  main.go:5     0x1089f6f       488d6c2438      LEAQ 0x38(SP), BP           
  main.go:6     0x1089f74       488d0525790000      LEAQ runtime.types+30624(SB), AX    
  main.go:6     0x1089f7b       0f1f440000      NOPL 0(AX)(AX*1)            
  main.go:6     0x1089f80       e89b19f8ff      CALL runtime.newobject(SB)      
  main.go:6     0x1089f85       440f117c2428        MOVUPS X15, 0x28(SP)            
  main.go:6     0x1089f8b       488d0d0e5c0000      LEAQ runtime.types+23200(SB), CX    
  main.go:6     0x1089f92       48894c2428      MOVQ CX, 0x28(SP)           
  main.go:6     0x1089f97       4889442430      MOVQ AX, 0x30(SP)           
  print.go:274      0x1089f9c       488b1db50c0b00      MOVQ os.Stdout(SB), BX          
  print.go:274      0x1089fa3       488d05f6540300      LEAQ go.itab.*os.File,io.Writer(SB), AX 
  print.go:274      0x1089faa       488d4c2428      LEAQ 0x28(SP), CX           
  print.go:274      0x1089faf       bf01000000      MOVL $0x1, DI               
  print.go:274      0x1089fb4       4889fe          MOVQ DI, SI             
  print.go:274      0x1089fb7       e844acffff      CALL fmt.Fprintln(SB)           
  main.go:7     0x1089fbc       488b6c2438      MOVQ 0x38(SP), BP           
  main.go:7     0x1089fc1       4883c440        ADDQ $0x40, SP              
  main.go:7     0x1089fc5       c3          RET                 
  main.go:5     0x1089fc6       e875f1fcff      CALL runtime.morestack_noctxt.abi0(SB)  
  main.go:5     0x1089fcb       eb93            JMP main.main(SB)

それぞれの内容をざっくり確認すると、どうやらコード上で new(string) が呼ばれたと思われる付近で CALL runtime.newobject(SB) というruntimeの処理が呼ばれていることがわかります。

そこで該当のパッケージを探してみると、以下のような処理があることがわかります。

https://github.com/golang/go/blob/0fa53e41f122b1661d0678a6d36d71b7b5ad031d/src/runtime/malloc.go#L1252-L1254

// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
    return mallocgc(typ.size, typ, true)
}

このように、ビルトイン関数の実装を探すことができました。

さいごに

runtime周りの実装は、普段みる標準パッケージやサードパーティパッケージの実装とはまた少し違っていて複雑さを感じる部分もあります。

ですが、アーキテクチャ差分のruntimeでの抽象化や、Goの並行処理の肝であるgoroutineとchannelについてなど、学べることが多くあります。
これを読んだ方も、興味があればぜひruntimeパッケージに目を通してみてください!

Kyash Advent Calendar 2021 はまだまだつづきます、あしたの投稿もおたのしみに〜。