Help us understand the problem. What is going on with this article?

"go build"した時に何が起きているのか?

この記事は How “go build” Works の翻訳記事です。

go buildはどのようにして最も単純なGolangプログラムをコンパイルしているのでしょうか?

この記事ではその疑問を解消することを目標にしています。

最も単純な以下のプログラムについて考えてみましょう。

// main.go
package main

func main() {}

go build main.goを実行すると、1.1Mbの実行可能ファイルmainが出力され、何も実行されません。 この何もしないバイナリを作成するために、go buildは何をしたのでしょうか?

go buildコマンドはいくつかの便利なオプションを提供しています。

  1. -work: go buildは、作業ファイル用の一時フォルダーを作成します。 この引数は、そのフォルダーの場所を出力し、ビルド後に削除しません
  2. -a: Golangは、以前にビルドされたパッケージをキャッシュします。 -ago buildにキャッシュを無視させるので、ビルドはすべてのステップを出力します
  3. -p 1: これにより、処理が単一スレッドで行われるように設定され、出力が線形にログに記録されます
  4. -x: go buildは、compileなどの他のGolangツールのラッパーです。 -xは、これらのツールに送信されるコマンドと引数を出力します

go build -work -a -p 1 -x main.goを実行するとmainだけでなく大量のログが出て、buildmainを作成する際に行われていることを我々に教えてくれます。

ログはまず以下の内容を出力します。

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では

  1. アクションディレクトリを作成します(すべてのアクションがこれを行うため、以降ではこの記述を省略します)
  2. ツールcompileで使用するimportcfgファイルを作成します(空です)
  3. ディレクトリをruntime/internal/sysパッケージのソースフォルダに変更します。 このパッケージには、ランタイムで使用される定数が含まれています
  4. パッケージをコンパイルします
  5. buildidを使用してメタデータをパッケージに書き込み(-w)、パッケージをgo-buildキャッシュにコピーします(すべてのパッケージがキャッシュされるため、以降ではこの記述を省略します)

これをツールcompileに送信された引数に分解してみましょう(go tool compile --helpでも説明されています)。

  1. -o 出力先のファイル
  2. -trimpath ソースファイルのパスからprefix$WORK/b008=>を取り除く
  3. -p importで使われるパッケージ名をセット
  4. -std compiling standard library(現時点ではよくわかりませんでした)
  5. -+ compiling runtime(これもわかりませんでした)
  6. -complete コンパイラはCまたはアセンブリではなく完全なパッケージを出力
  7. -buildid メタデータにbuild idを付与する
  8. -goversion コンパイルされたパッケージに必要なバージョン
  9. -D ローカルなインポートで使う相対パスは""
  10. -importcfg インポート設定ファイルは他のパッケージを参照
  11. -pack パッケージをオブジェクトファイル.oではなくアーカイブ.aとして作る
  12. -c ビルド時にどれだけ並列で処理するか
  13. パッケージ内のファイルのリスト

これらの引数のほとんどはすべての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のログでは、buildIDcompileツールによって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
  1. packagefile runtime/internal/sys=$WORK/b008/_pkg_.aと書かれたimportcfgを作成しています これはb007b008に依存していることを示します
  2. 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ファイルの処理を開始します。

  1. ヘッダファイルgo_asm.hを作成
  2. 低レベルな関数が集まったruntime/internal/atomicパッケージに移動
  3. ツールgo tool asmgo tool asm --helpで説明)を実行して、symabis "Symbol Application Binary Interfaces(ABI)ファイル" を作成し、次にオブジェクトファイルasm_amd64.oを作成
  4. compileを使用して、symabisファイルと -asmhdrを含むヘッダーを含む_pkg_.aファイルを作成
  5. packコマンドでasm_amd64.o_pkg_.aの中に加える

asmツールはここでは以下の引数を伴って呼び出されています。

  1. -I: アクション b007およびlibexec/pkg/includesフォルダーを含めます。 includesには3つのファイルasm_ppc64x.hfuncdata.htextflag.hがあり、すべて低レベルの関数定義があります。 例えば、FIXED_FRAMEは、スタックフレームの固定部分のサイズを定義します
  2. -D: 事前定義されたシンボルを含める
  3. -gensymabis: symabisファイルを作成する
  4. -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もやることはb004b006と同じです。

このパッケージの主な問題は、多くのオブジェクトファイル.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はコアライブラリの中では最も複雑なパッケージかもしれませんが、ビルド自体は今までと同じ工程で行われます。つまりasmcompileして出力されたファイルを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

  1. 最初に、b002runtimeを含むようにしてimportcfgを作成した後、main.goをコンパイルして_pkg_.aを作成します。
  2. 以前に登場した全パッケージに加えてcommand-line-arguments=$WORK/b001/_pkg_.aを含むimportcfg.linkを作成したら、linkコマンドでそれらをリンクして実行ファイルを作成します。
  3. 最後にmainにリネームして出力先に移動します。

linkの引数の補足をしておきましょう。

  1. -buildmode: 実行ファイルをビルドする
  2. -extld: 外部のリンカを参照する

ようやくお目当てのものが手に入りました。

b001から生まれるのがmainバイナリです。

Bazelとの類似点

効率的なキャッシュを実現するためのアクショングラフの作成は、Bazelが高速ビルドに使用するビルドツールと同じアイデアです。

Golangの actionidcontentidは、Bazelがキャッシュで使用するactioncachecontent-addressable store(CAS)に対応しています。

BazelはGoogleの製品であり、Golangも同様です。 彼らがソフトウェアを迅速そして正確にビルドする方法について同様の哲学をもったことは非常に合理的と言えるでしょう。

Bazelの rules_goパッケージでは、builderコードで go buildを再実装する方法を確認できます。

アクショングラフ、フォルダ管理、およびキャッシュはBazelによって外部で処理されるため、これは非常にクリーンな実装です。

次のステップへ

go buildは、今回のような何もしないプログラムでも、それをコンパイルするために多くのことを行っていました。

ツール( compile asm)やその入力ファイルと出力ファイル(.a .o .s)については今回はあまり詳しく説明しませんでした。

また、今回は最も基本的なプログラムをコンパイルしているだけです。

次のようにしてコンパイルをもっと複雑なものにできます。

  1. 他のパッケージをインポートする 例えばHello worldを出力するためfmtをインポートするとさらに23個のアクションがアクショングラフに追加される
  2. 外部パッケージを参照するためにgo.modを使用する
  3. GOOSGOARCHの値を変えて他のアーキテクチャ向けにビルドする 例えば、wasm向けのコンパイルではアクションや引数の内容が全く異なってくる

go buildを実行してログを検査することは、Goコンパイラがどのように機能するかを学ぶためのアプローチとしてはトップダウンなアプローチです。 基礎から学びたい場合は次のようなリソースに飛び込むのに最適な出発点です。

  1. Introduction to the Go compiler
  2. Go: Overview of the Compiler
  3. Go at Google: Language Design in the Service of Software Engineering
  4. build.go
  5. compile/main.go

References

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away