はじめに

Goは、Go1.5以降C言語による実装がなくなり、ほぼ全てがGoによって書かれています。そのためGoエンジニアは最悪Goのソースコードを読んで問題を解決することができるため、とても生産的な状態にあると言えます。
しかしGoがセルフホスティングだとしてもOSの機能群を呼び出すときには、C言語で実装されているシステムコールを実行しているはずです。では、Goではどのようにシステムコールを実行しているのでしょうか?また、どのようにそれらをビルドしているのでしょうか?

この記事では、上記の疑問をGoのソースとドキュメントから読み解いていきます。

Goとシステムコール

一般のGoプログラマがGoでシステムコールを実行するには、syscallパッケージ、golang.org/x/sys/パッケージを利用します。1

関数一覧には見覚えるのあるシステムコールと同様の名前の関数が並んでいます。これらはシステムコールを実行しやすいようにGoでラッパーした関数群です。

syscallパッケージはほとんどドキュメントは書かれていません。システムコールはOSによって詳細な挙動が異なるため統一したドキュメントを書くことはできないのでしょう。

システムコールの実行方法

システムコールの実行方法についてなど、詳細な仕組みはGoならわかるシステムプログラミング ― 第5回 Goから見たシステムコールに大変わかりやすい解説が載っているので説明は割愛します。

またシステムコールというのはOSに依存した処理であり、POSIXなどで規定されていないものが多くあります。そのためすべてのシステムコールのラッパーをGoで実装するということは現実的ではありません。
そこでGoはラッパー関数が用意されていない関数を実行するためにsyscall.Syscall()という関数も用意しています。
しかし移植性を考慮してWindows, Linux, Macなどのそれぞれに合わせた実装をしなければなりませんので、普段は使うことはまずないでしょう。

Goとアセンブラのビルド

先ほど紹介した記事に以下のように書いてありました。

Syscall()の中身は、macOSの場合、asm_darwin_amd64.sというGoの低レベルアセンブリ言語で書かれたコードで定義されています。

上記のようにGoとアセンブラで実装されていますが、これらをどのようにビルドしているのかを見ていきたいと思います。

Syscall()はアセンブラのソースに定義してあり、宣言はsyscall_unix.goにしてあります。

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

普段見かけない記法ですが、異なる場所で定義された関数を実行するためには必要です。詳細は、こちらをご確認ください。Go Binary Hacks – go buildせずにビルドする #golang

また他のシステムコール関数は//sysという特殊な記法で記述されているところがあります。本来は//はコメントのはずですが//sysには特別の意味が与えられています。

以下はMacのシステムコール群です。

//sys    Access(path string, mode uint32) (err error)
//sys    Adjtime(delta *Timeval, olddelta *Timeval) (err error)
//sys    Chdir(path string) (err error)
//sys    Chflags(path string, flags int) (err error)
//sys    Chmod(path string, mode uint32) (err error)
//sys    Chown(path string, uid int, gid int) (err error)
//sys    Chroot(path string) (err error)
//sys    Close(fd int) (err error)

//sysという行は、mksyscall.plを使って//sysと書かれた行のラッパーを自動生成しています。
恐らく単純にシステムコールを呼び出している純粋なラッパーをその都度実装するのが手間なため、専用の記法とツールを用意したのだと思います。

基本的な使い方は以下の通りです。

cd github.com/golang/go/src/syscall
./mksyscall.pl syscall_darwin.go

変換後のソースが標準出力されます。以下、出力例

// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT

func Access(path string, mode uint32) (err error) {
        var _p0 *byte
        _p0, err = BytePtrFromString(path)
        if err != nil {
                return
        }
        _, _, e1 := Syscall(SYS_ACCESS, uintptr(unsafe.Pointer(_p0)), uintptr(mode), 0)
        use(unsafe.Pointer(_p0))
        if e1 != 0 {
                err = errnoErr(e1)
        }
        return
}

この生成されたものは、リポジトリにコミットしてあります。
https://golang.org/src/syscall/zsyscall_darwin_amd64.go#L298

実際には、mkall.shから実行されます。
Mac 64bit環境では以下のようになります。

./mksyscall.pl syscall_bsd.go syscall_darwin.go syscall_darwin_amd64.go |gofmt >zsyscall_darwin_amd64.go'

Go本体のビルド

最後にGoをソースコードからビルドする際にどのようにsyscallパッケージをビルドしているかを確認します。

まずGo 1.5以降のビルドの手順を理解しましょう。

    1. Go 1.4を使ってcmd/distをビルドする。

distを使って、Go 1.4でGo 1.xのコンパイラツールチェインをビルドする。

distを使って、Go 1.xでGo 1.xのコンパイラツールチェインをビルドする。

distを使って、Go 1.xのコンパイラツールチェインでGo 1.xのcmd/goをgo_bootstrapとしてビルドする。

go_bootstrapを使って、残りのGo 1.xの標準ライブラリやコマンドをビルドする。

この中で最終的にGoのsyscallをビルドするところは、最後の「go_bootstrapを使って、残りのGo 1.xの標準ライブラリやコマンドをビルドする。」になります。

実行している箇所は以下になります。

echo "##### Building packages and commands for $GOOS/$GOARCH."
CC=$CC_FOR_TARGET "$GOTOOLDIR"/go_bootstrap install $GO_FLAGS -gcflags "$GO_GCFLAGS" -ldflags "$GO_LDFLAGS" -v std cmd
echo

実行した時のログ。

$ ./make.bash
##### Building packages and commands for darwin/amd64.
+ CC=clang
+ /Users/sonatard/src/github.com/golang/go/pkg/tool/darwin_amd64/go_bootstrap install -gcflags '' -ldflags '' -v std cmd
runtime/internal/sys
runtime/internal/atomic
runtime
errors
internal/race
.
.
.
syscall

これだけでは詳細がわからないのでログを仕込みます。installに-xオプションを追加します。

-/Users/sonatard/src/github.com/golang/go/pkg/tool/darwin_amd64/go_bootstrap install -gcflags '' -ldflags '' -v std cmd
+/Users/sonatard/src/github.com/golang/go/pkg/tool/darwin_amd64/go_bootstrap install -x -gcflags '' -ldflags '' -v std cmd

以下、ログを見やすいように整形しています。

syscall
cd /Users/sonatard/src/github.com/golang/go/src/syscall

# syscall.aを作成
mkdir -p $WORK/syscall/_obj/
cd src/github.com/golang/go/src/syscall
go tool compile -o $WORK/syscall.a -trimpath $WORK -p syscall -buildid 23f18ef6130112e16d17ad131dc023da83cc5f0f -D _/Users/sonatard/src/github.com/golang/go/src/syscall -I $WORK -pack -asmhdr $WORK/syscall/_obj/go_asm.h ./bpf_bsd.go ./env_unix.go ./exec_bsd.go ./exec_unix.go ./flock.go ./msan0.go ./route_bsd.go ./route_darwin.go ./sockcmsg_unix.go ./str.go ./syscall.go ./syscall_bsd.go ./syscall_darwin.go ./syscall_darwin_amd64.go ./syscall_unix.go ./zerrors_darwin_amd64.go ./zsyscall_darwin_amd64.go ./zsysnum_darwin_amd64.go ./ztypes_darwin_amd64.go

# asm.sからasm.oを作成c
go tool asm -o $WORK/syscall/_obj/asm.o -trimpath $WORK -I $WORK/syscall/_obj/ -I go/pkg/include -D GOOS_darwin -D GO
ARCH_amd64 ./asm.s

# asm_darwin_amd64.sからasm_darwin_amd64.oを作成
go tool asm -o $WORK/syscall/_obj/asm_darwin_amd64.o -trimpath $WORK -I $WORK/syscall/_obj/ -I go/pkg/include -D GOOS_darwin -D GOARCH_amd64 ./asm_darwin_amd64.s

# syscall.aに asm.oとasm_darwin_amd64.oを追加
go tool pack r $WORK/syscall.a $WORK/syscall/_obj/asm.o $WORK/syscall/_obj/asm_darwin_amd64.o

# 作成したライブラリを移動
mv $WORK/syscall.a /Users/sonatard/src/github.com/golang/go/pkg/darwin_amd64/syscall.a

asm.oのmain.use()は何なのか不明です。(ただreturnしているだけ?)

$ $WORK/syscall/_obj/
$ go tool nm asm.o
6e T %22%22.use
#include "textflag.h"

TEXT ·use(SB),NOSPLIT,$0
    RET

asm_darwin_amd64.sにはSyscallなどが実装されています。

$ go tool nm asm_darwin_amd64.o
367 T %22%22.RawSyscall
3e6 T %22%22.RawSyscall6
163 T %22%22.Syscall
1f6 T %22%22.Syscall6
28f T %22%22.Syscall9
    U runtime.entersyscall
    U runtime.exitsyscall

まとめ

    • システムコールに関連するOSに依存した処理は、実行環境ごとに実装が用意されている。

 

    • システムコールの呼び出しは、C言語ではなくアセンブラで実装されている。

 

    Go本体では、実行環境に合わせて適切なファイルを選択してビルドする。

以上で一通りのシステムコールの流れがわかったので、もし不足しているシステムコールがあればPRを出せるようになったのではないかと思います。

次回は、自分でgoとassemblaのコードを書いてビルドする方法について書きたいと思います。

関連

Goの仕様について興味がある方はこちらもどうぞ

    • Better C – Goと整数 #golang

 

    Go Binary Hacks – go buildせずにビルドする #golang

Go 1.4以降syscallパッケージの利用は推奨されていません。詳細はThe syscall package ↩