この記事は How “go build” Works の翻訳記事です。
go build
はどのようにして最も単純なGolangプログラムをコンパイルしているのでしょうか?
この記事ではその疑問を解消することを目標にしています。
最も単純な以下のプログラムについて考えてみましょう。
// main.go
package main
func main() {}
go build main.go
を実行すると、1.1Mbの実行可能ファイルmain
が出力され、何も実行されません。 この何もしないバイナリを作成するために、go build
は何をしたのでしょうか?
go build
コマンドはいくつかの便利なオプションを提供しています。
-
-work
:go build
は、作業ファイル用の一時フォルダーを作成します。 この引数は、そのフォルダーの場所を出力し、ビルド後に削除しません -
-a
: Golangは、以前にビルドされたパッケージをキャッシュします。-a
はgo build
にキャッシュを無視させるので、ビルドはすべてのステップを出力します -
-p 1
: これにより、処理が単一スレッドで行われるように設定され、出力が線形にログに記録されます -
-x
:go build
は、compile
などの他のGolangツールのラッパーです。-x
は、これらのツールに送信されるコマンドと引数を出力します
go build -work -a -p 1 -x main.go
を実行するとmain
だけでなく大量のログが出て、build
でmain
を作成する際に行われていることを我々に教えてくれます。
ログはまず以下の内容を出力します。
WORK=/var/folders/rw/gtb29xf92fv23f0zqsg42s840000gn/T/go-build940616988
これは、構造が次のような作業ディレクトリです。
├── b001
│ ├── _pkg_.a
│ ├── exe
│ ├── importcfg
│ └── importcfg.link
├── b002
│ └── ...
├── b003
│ └── ...
├── b004
│ └── ...
├── b006
│ └── ...
├── b007
│ └── ...
└── b008
└── ...
go build
は、完了する必要のあるタスクのアクショングラフを定義します。
このグラフの各アクションは、独自のサブディレクトリ(NewObjdir
で定義)を取得します。
グラフの最初のノードb001
は、メインバイナリをコンパイルするためのルートタスクです。
依存する各アクションの数は大きく、最後はb008
です。 (b005
がどこに行ったのかわかりませんが問題ないと思われるので割愛します)
b008
実行される最初のアクションは、グラフの末端のb008
です。
mkdir -p $WORK/b008/
cat >$WORK/b008/importcfg << 'EOF'
# import config
EOF
cd /<..>/src/runtime/internal/sys
/<..>/compile
-o $WORK/b008/_pkg_.a
-trimpath "$WORK/b008=>"
-p runtime/internal/sys
-std
-+
-complete
-buildid gEtYPexVP43wWYWCxFKi/gEtYPexVP43wWYWCxFKi
-goversion go1.14.7
-D ""
-importcfg $WORK/b008/importcfg
-pack
-c=16
./arch.go ./arch_amd64.go ./intrinsics.go ./intrinsics_common.go ./stubs.go ./sys.go ./zgoarch_amd64.go ./zgoos_darwin.go ./zversion.go
/<..>/buildid -w $WORK/b008/_pkg_.a
cp $WORK/b008/_pkg_.a /<..>/Caches/go-build/01/01b...60a-d
b008
では
- アクションディレクトリを作成します(すべてのアクションがこれを行うため、以降ではこの記述を省略します)
- ツール
compile
で使用するimportcfg
ファイルを作成します(空です) - ディレクトリを
runtime/internal/sys
パッケージのソースフォルダに変更します。 このパッケージには、ランタイムで使用される定数が含まれています - パッケージをコンパイルします
-
buildid
を使用してメタデータをパッケージに書き込み(-w
)、パッケージをgo-build
キャッシュにコピーします(すべてのパッケージがキャッシュされるため、以降ではこの記述を省略します)
これをツールcompile
に送信された引数に分解してみましょう(go tool compile --help
でも説明されています)。
-
-o
出力先のファイル -
-trimpath
ソースファイルのパスからprefix$WORK/b008=>
を取り除く -
-p
import
で使われるパッケージ名をセット -
-std
compiling standard library
(現時点ではよくわかりませんでした) -
-+
compiling runtime
(これもわかりませんでした) -
-complete
コンパイラはCまたはアセンブリではなく完全なパッケージを出力 -
-buildid
メタデータにbuild idを付与する -
-goversion
コンパイルされたパッケージに必要なバージョン -
-D
ローカルなインポートで使う相対パスは"" -
-importcfg
インポート設定ファイルは他のパッケージを参照 -
-pack
パッケージをオブジェクトファイル.o
ではなくアーカイブ.a
として作る -
-c
ビルド時にどれだけ並列で処理するか - パッケージ内のファイルのリスト
これらの引数のほとんどはすべてのcompile
コマンドで同じなので、以降ではこの記述を省略します。
b008
の出力はruntime/internal/sys
に対応している$WORK/b008/_pkg_.a
というアーカイブファイルです。
buildid
buildid
とは何かを説明しましょう。
buildid
の形式は<actionid>/<contentid>
です。
これは、パッケージをキャッシュして go build
のパフォーマンスを向上させるためのインデックスとして使用されます。
<actionid>
は、アクション(すべての呼び出し、引数、および入力ファイル)のハッシュです。 <contentid>
は、出力 .a
ファイルのハッシュです。
go build
アクションごとに、同じ<actionid>
を持つ別のアクションによって作成されたコンテンツをキャッシュで検索できます。
これは buildid.go
に実装されています。
buildid
はメタデータとしてファイルに保存されるため、<contentid>
を取得するために毎回ハッシュする必要はありません。 このIDは、 go tool buildid <file>
で確認できます(バイナリでも機能します)。
上記の b008
のログでは、buildID
は compile
ツールによってgEtYPexVP43wWYWCxFKi/gEtYPexVP43wWYWCxFKi
として設定されています。
これは単なるプレースホルダーであり、後でキャッシュされる前に、 go tool buildid -w
で正しいgEtYPexVP43wWYWCxFKi/b-rPboOuD0POrlJWPTEi
に上書きされます。
b007
次はb007
です
cat >$WORK/b007/importcfg << 'EOF'
# import config
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF
cd /<..>/src/runtime/internal/math
/<..>/compile
-o $WORK/b007/_pkg_.a
-p runtime/internal/math
-importcfg $WORK/b007/importcfg
...
./math.go
-
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
と書かれたimportcfg
を作成しています これはb007
がb008
に依存していることを示します -
runtime/internal/math
をコンパイルmath.go
の中身を覗いてみると確かにb008
で作られたruntime/internal/sys
をインポートしています
b007
の出力はruntime/internal/math
に対応している$WORK/b007/_pkg_.a
というアーカイブファイルです。
b006
cat >$WORK/b006/go_asm.h << 'EOF'
EOF
cd /<..>/src/runtime/internal/atomic
/<..>/asm
-I $WORK/b006/
-I /<..>/go/1.14.7/libexec/pkg/include
-D GOOS_darwin
-D GOARCH_amd64
-gensymabis
-o $WORK/b006/symabis
./asm_amd64.s
/<..>/asm
-I $WORK/b006/
-I /<..>/go/1.14.7/libexec/pkg/include
-D GOOS_darwin
-D GOARCH_amd64
-o $WORK/b006/asm_amd64.o
./asm_amd64.s
cat >$WORK/b006/importcfg << 'EOF'
# import config
EOF
/<..>/compile
-o $WORK/b006/_pkg_.a
-p runtime/internal/atomic
-symabis $WORK/b006/symabis
-asmhdr $WORK/b006/go_asm.h
-importcfg $WORK/b006/importcfg
...
./atomic_amd64.go ./stubs.go
/<..>/pack r $WORK/b006/_pkg_.a $WORK/b006/asm_amd64.o
ここで、通常の.go
ファイルから抜け出し、低レベルのGoアセンブリ.s
ファイルの処理を開始します。
- ヘッダファイル
go_asm.h
を作成 - 低レベルな関数が集まった
runtime/internal/atomic
パッケージに移動 - ツール
go tool asm
(go tool asm --help
で説明)を実行して、symabis
"Symbol Application Binary Interfaces(ABI)ファイル" を作成し、次にオブジェクトファイルasm_amd64.o
を作成 -
compile
を使用して、symabis
ファイルと-asmhdr
を含むヘッダーを含む_pkg_.a
ファイルを作成 -
pack
コマンドでasm_amd64.o
を_pkg_.a
の中に加える
asm
ツールはここでは以下の引数を伴って呼び出されています。
-
-I
: アクションb007
およびlibexec/pkg/includes
フォルダーを含めます。includes
には3つのファイルasm_ppc64x.h
、funcdata.h
とtextflag.h
があり、すべて低レベルの関数定義があります。 例えば、FIXED_FRAMEは、スタックフレームの固定部分のサイズを定義します -
-D
: 事前定義されたシンボルを含める -
-gensymabis
:symabis
ファイルを作成する -
-o
: 出力先のファイル
b006
の出力はruntime/internal/atomic
に対応している$WORK/b006/_pkg_.a
というアーカイブファイルです。
b004
cd /<..>/src/internal/cpu
/<..>/asm ... -o $WORK/b004/symabis ./cpu_x86.s
/<..>/asm ... -o $WORK/b004/cpu_x86.o ./cpu_x86.s
/<..>/compile ... -o $WORK/b004/_pkg_.a ./cpu.go ./cpu_amd64.go ./cpu_x86.go
/<..>/pack r $WORK/b004/_pkg_.a $WORK/b004/cpu_x86.o
b004
は対象がinternal/cpu
に変わった以外はb006
と同様です。
最初にsymabis
とオブジェクトファイルをcpu_x86.s
をアセンブルして作成して、goファイルをコンパイルした後、それらを合わせて、アーカイブ_pkg_.a
を作成します。
b004
の出力はinternal/cpu
に対応している$WORK/b004/_pkg_.a
というアーカイブファイルです。
b003
cat >$WORK/b003/go_asm.h << 'EOF'
EOF
cd /<..>/src/internal/bytealg
/<..>/asm ... -o $WORK/b003/symabis ./compare_amd64.s ./count_amd64.s ./equal_amd64.s ./index_amd64.s ./indexbyte_amd64.s
cat >$WORK/b003/importcfg << 'EOF'
# import config
packagefile internal/cpu=$WORK/b004/_pkg_.a
EOF
/<..>/compile ... -o $WORK/b003/_pkg_.a -p internal/bytealg ./bytealg.go ./compare_native.go ./count_native.go ./equal_generic.go ./equal_native.go ./index_amd64.go ./index_native.go ./indexbyte_native.go
/<..>/asm ... -o $WORK/b003/compare_amd64.o ./compare_amd64.s
/<..>/asm ... -o $WORK/b003/count_amd64.o ./count_amd64.s
/<..>/asm ... -o $WORK/b003/equal_amd64.o ./equal_amd64.s
/<..>/asm ... -o $WORK/b003/index_amd64.o ./index_amd64.s
/<..>/asm ... -o $WORK/b003/indexbyte_amd64.o ./indexbyte_amd64.s
/<..>/pack r $WORK/b003/_pkg_.a $WORK/b003/compare_amd64.o $WORK/b003/count_amd64.o $WORK/b003/equal_amd64.o $WORK/b003/index_amd64.o $WORK/b003/indexbyte_amd64.o
b003
もやることはb004
やb006
と同じです。
このパッケージの主な問題は、多くのオブジェクトファイル.o
を作成するために複数の.s
ファイルがあり、それぞれを_pkg_.a
ファイルに追加する必要があることです。
b003
の出力はinternal/bytealg
に対応している$WORK/b003/_pkg_.a
というアーカイブファイルです。
b002
cat >$WORK/b002/go_asm.h << 'EOF'
EOF
cd /<..>/src/runtime
/<..>/asm
...
-o $WORK/b002/symabis
./asm.s ./asm_amd64.s ./duff_amd64.s ./memclr_amd64.s ./memmove_amd64.s ./preempt_amd64.s ./rt0_darwin_amd64.s ./sys_darwin_amd64.s
cat >$WORK/b002/importcfg << 'EOF'
# import config
packagefile internal/bytealg=$WORK/b003/_pkg_.a
packagefile internal/cpu=$WORK/b004/_pkg_.a
packagefile runtime/internal/atomic=$WORK/b006/_pkg_.a
packagefile runtime/internal/math=$WORK/b007/_pkg_.a
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF
/<..>/compile
-o $WORK/b002/_pkg_.a
...
-p runtime
./alg.go ./atomic_pointer.go ./cgo.go ./cgocall.go ./cgocallback.go ./cgocheck.go ./chan.go ./checkptr.go ./compiler.go ./complex.go ./cpuflags.go ./cpuflags_amd64.go ./cpuprof.go ./cputicks.go ./debug.go ./debugcall.go ./debuglog.go ./debuglog_off.go ./defs_darwin_amd64.go ./env_posix.go ./error.go ./extern.go ./fastlog2.go ./fastlog2table.go ./float.go ./hash64.go ./heapdump.go ./iface.go ./lfstack.go ./lfstack_64bit.go ./lock_sema.go ./malloc.go ./map.go ./map_fast32.go ./map_fast64.go ./map_faststr.go ./mbarrier.go ./mbitmap.go ./mcache.go ./mcentral.go ./mem_darwin.go ./mfinal.go ./mfixalloc.go ./mgc.go ./mgcmark.go ./mgcscavenge.go ./mgcstack.go ./mgcsweep.go ./mgcsweepbuf.go ./mgcwork.go ./mheap.go ./mpagealloc.go ./mpagealloc_64bit.go ./mpagecache.go ./mpallocbits.go ./mprof.go ./mranges.go ./msan0.go ./msize.go ./mstats.go ./mwbbuf.go ./nbpipe_pipe.go ./netpoll.go ./netpoll_kqueue.go ./os_darwin.go ./os_nonopenbsd.go ./panic.go ./plugin.go ./preempt.go ./preempt_nonwindows.go ./print.go ./proc.go ./profbuf.go ./proflabel.go ./race0.go ./rdebug.go ./relax_stub.go ./runtime.go ./runtime1.go ./runtime2.go ./rwmutex.go ./select.go ./sema.go ./signal_amd64.go ./signal_darwin.go ./signal_darwin_amd64.go ./signal_unix.go ./sigqueue.go ./sizeclasses.go ./slice.go ./softfloat64.go ./stack.go ./string.go ./stubs.go ./stubs_amd64.go ./stubs_nonlinux.go ./symtab.go ./sys_darwin.go ./sys_darwin_64.go ./sys_nonppc64x.go ./sys_x86.go ./time.go ./time_nofake.go ./timestub.go ./trace.go ./traceback.go ./type.go ./typekind.go ./utf8.go ./vdso_in_none.go ./write_err.go
/<..>/asm ... -o $WORK/b002/asm.o ./asm.s
/<..>/asm ... -o $WORK/b002/asm_amd64.o ./asm_amd64.s
/<..>/asm ... -o $WORK/b002/duff_amd64.o ./duff_amd64.s
/<..>/asm ... -o $WORK/b002/memclr_amd64.o ./memclr_amd64.s
/<..>/asm ... -o $WORK/b002/memmove_amd64.o ./memmove_amd64.s
/<..>/asm ... -o $WORK/b002/preempt_amd64.o ./preempt_amd64.s
/<..>/asm ... -o $WORK/b002/rt0_darwin_amd64.o ./rt0_darwin_amd64.s
/<..>/asm ... -o $WORK/b002/sys_darwin_amd64.o ./sys_darwin_amd64.s
/<..>/pack r $WORK/b002/_pkg_.a $WORK/b002/asm.o $WORK/b002/asm_amd64.o $WORK/b002/duff_amd64.o $WORK/b002/memclr_amd64.o $WORK/b002/memmove_amd64.o $WORK/b002/preempt_amd64.o $WORK/b002/rt0_darwin_amd64.o $WORK/b002/sys_darwin_amd64.o
b002
を見ればこれまでのアクションがなぜ必要だったのかわかります。
b002
はGoのバイナリの実行に必要なruntime
パッケージ全てを含んでいます。例えば、b002
には mgc.go
というGoのGCの実装も含まれています。これはb004
(internal/cpu
)とb006
(runtime/internal/atomic
)をインポートしています。
b002
はコアライブラリの中では最も複雑なパッケージかもしれませんが、ビルド自体は今までと同じ工程で行われます。つまりasm
、compile
して出力されたファイルをpack
して_pkg_.a
にしています。
b002
の出力はruntime
に対応している$WORK/b002/_pkg_.a
というアーカイブファイルです。
b001
cat >$WORK/b001/importcfg << 'EOF'
# import config
packagefile runtime=$WORK/b002/_pkg_.a
EOF
cd /<..>/main
/<..>/compile ... -o $WORK/b001/_pkg_.a -p main ./main.go
cat >$WORK/b001/importcfg.link << 'EOF'
packagefile command-line-arguments=$WORK/b001/_pkg_.a
packagefile runtime=$WORK/b002/_pkg_.a
packagefile internal/bytealg=$WORK/b003/_pkg_.a
packagefile internal/cpu=$WORK/b004/_pkg_.a
packagefile runtime/internal/atomic=$WORK/b006/_pkg_.a
packagefile runtime/internal/math=$WORK/b007/_pkg_.a
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF
/<..>/link
-o $WORK/b001/exe/a.out
-importcfg $WORK/b001/importcfg.link
-buildmode=exe
-buildid=yC-qrh2sY_qI0zh2-NE7/owNzOBTqPO00FkqK0_lF/HPXqvMz_4PvKsQzqGWgD/yC-qrh2sY_qI0zh2-NE7
-extld=clang
$WORK/b001/_pkg_.a
mv $WORK/b001/exe/a.out main
First it builds an importcfg that includes runtime built in b002 to then compile main.go to pkg.a
- 最初に、
b002
のruntime
を含むようにしてimportcfg
を作成した後、main.go
をコンパイルして_pkg_.a
を作成します。 - 以前に登場した全パッケージに加えて
command-line-arguments=$WORK/b001/_pkg_.a
を含むimportcfg.link
を作成したら、link
コマンドでそれらをリンクして実行ファイルを作成します。 - 最後に
main
にリネームして出力先に移動します。
link
の引数の補足をしておきましょう。
-
-buildmode
: 実行ファイルをビルドする -
-extld
: 外部のリンカを参照する
ようやくお目当てのものが手に入りました。
b001
から生まれるのがmain
バイナリです。
Bazelとの類似点
効率的なキャッシュを実現するためのアクショングラフの作成は、Bazelが高速ビルドに使用するビルドツールと同じアイデアです。
Golangの actionid
とcontentid
は、Bazelがキャッシュで使用するactioncache
と content-addressable store(CAS)
に対応しています。
BazelはGoogleの製品であり、Golangも同様です。 彼らがソフトウェアを迅速そして正確にビルドする方法について同様の哲学をもったことは非常に合理的と言えるでしょう。
Bazelの rules_go
パッケージでは、builder
コードで go build
を再実装する方法を確認できます。
アクショングラフ、フォルダ管理、およびキャッシュはBazelによって外部で処理されるため、これは非常にクリーンな実装です。
次のステップへ
go build
は、今回のような何もしないプログラムでも、それをコンパイルするために多くのことを行っていました。
ツール( compile
asm
)やその入力ファイルと出力ファイル(.a
.o
.s
)については今回はあまり詳しく説明しませんでした。
また、今回は最も基本的なプログラムをコンパイルしているだけです。
次のようにしてコンパイルをもっと複雑なものにできます。
- 他のパッケージをインポートする 例えば
Hello world
を出力するためfmt
をインポートするとさらに23個のアクションがアクショングラフに追加される - 外部パッケージを参照するために
go.mod
を使用する -
GOOS
とGOARCH
の値を変えて他のアーキテクチャ向けにビルドする 例えば、wasm
向けのコンパイルではアクションや引数の内容が全く異なってくる
go build
を実行してログを検査することは、Goコンパイラがどのように機能するかを学ぶためのアプローチとしてはトップダウンなアプローチです。 基礎から学びたい場合は次のようなリソースに飛び込むのに最適な出発点です。
- Introduction to the Go compiler
- Go: Overview of the Compiler
- Go at Google: Language Design in the Service of Software Engineering
- build.go
- compile/main.go