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

## はじめに

Goではルールに従いソースコードを配置して`go build`を実行するだけで実行バイナリを作ることができます。
とても便利なのですが、一体裏で何が行われているのでしょうか？

この記事ではgo buildの仕組み、簡単なシンボルテーブルの説明、Goに用意されているバイナリ操作コマンドを学ぶことができます。


## 実行バイナリの作成

実はGoでもC言語と同様に以下のフローで実行バイナリを生成しています。
1. ソースコードをコンパイルをしてオブジェクトファイルを作成
2. オブジェクトファイルをまとめてライブラリを作成
3. オブジェクトやライブラリをリンクして実行バイナリを作成

`go build`ではこれらの処理を複数コマンドにより実現しています。
コマンドの詳細は`go build`の`-x`オプションで確認することができます。

## 同一パッケージに1つのファイルしかない場合

```go:main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
```

以下、コメントは私が追記したものです。

```
$ go build -x
# 作業用のディレクトリを作成
WORK=/var/folders/4z/yzprxc9n5sx7gly7ybrynkl80000gn/T/go-build482602531
mkdir -p $WORK/github.com/sonatard/buildtest/_obj/
mkdir -p $WORK/github.com/sonatard/buildtest/_obj/exe/
cd /Users/ken/src/github.com/sonatard/buildtest
# コンパイル
/usr/local/Cellar/go/1.7.3/libexec/pkg/tool/darwin_amd64/compile -o $WORK/github.com/sonatard/buildtest.a -trimpath $WORK -p main -complete -buildid 77c8cc4f3c5bf3be375a16c76ac243e317fad15b -D _/Users/ken/src/github.com/sonatard/buildtest -I $WORK -pack main.go
cd .
# リンク
/usr/local/Cellar/go/1.7.3/libexec/pkg/tool/darwin_amd64/link -o $WORK/github.com/sonatard/buildtest/_obj/exe/a.out -L $WORK -extld=clang -buildmode=exe -buildid=77c8cc4f3c5bf3be375a16c76ac243e317fad15b $WORK/github.com/sonatard/buildtest.a
#　作業ディレクトリから実行バイナリをカレントディレクトリにコピー
mv $WORK/github.com/sonatard/buildtest/_obj/exe/a.out buildtest
```

上記では表現されていませんが`go build`終了後にWORKディレクトリ以下は削除されます。
削除しないためには`-work`オプションを指定します。

最低限必要なコマンドとオプションに絞ると以下のようになります。以降はこちらの表記で説明していきます。


```
SRC_PATH="$GOPATH/src"
PKG_PATH="$GOPATH/pkg/darwin_amd64"

go tool compile -o buildtest.a -p main -I $PKG_PATH -pack main.go
go tool link -o buildtest -L $SRC_PATH -L $PKG_PATH buildtest.a
$ ls                                                                                                                                                
buildtest  buildtest.a  main.go
$ ./buildtest
Hello, World!
```

### go tool compile

```
go tool compile -o buildtest.a -p main -I $PKG_PATH -pack ./main.go
```

C言語と似ていますが、少し異なる点としてコンパイルする際に`-pack`オプションという静的ライブラリを生成するオプションが用意されているため`.o`ファイルを経由せずに`.a`ファイルを作成することができます。
あとは`-p`でパッケージを指定。`-I`でimportファイルを検索するディレクトリを指定します。

### go tool link

```
go tool link -o buildtest -L $SRC_PATH -L $PKG_PATH buildtest.a
```

`-L`オプションでライブラリを検索するディレクトリを指定します。


## 同一パッケージに2つのファイルがある場合

```go:main.go
package main

func main() {
    test()
}
```

```go:test.go
package main

import "fmt"

func test() {
    fmt.Printf("Hello, Test!\n")
}
```



同ディレクトリに複数ファイルがある場合は、`go build`は複数ファイルをまとめてコンパイルします。

```
go tool compile -o buildtest.a -p main -I $PKG_PATH -pack ./main.go ./test.go
go tool link -o buildtest -L $SRC_PATH -L $PKG_PATH buildtest.a
```

しかしC言語のように複数ファイルがある場合にはファイルごとにコンパイルすることで、**「変更があったファイルだけを再度コンパイルする」**ということがしたくなります。

`go tool compile`には`-pack`オプションをつけないことでオブジェクトファイルを作成することができ、また`go tool pack`コマンドで複数のオブジェクトファイルをまとめたライブラリを作成することもできるので、「変更があったファイルだけを再度コンパイルする」ということができそうです。しかし以下のコマンドを実際に実行してみるといくつかの問題が発生します。

```
SRC_PATH="$GOPATH/src"
PKG_PATH="$GOPATH/pkg/darwin_amd64"

go tool compile -o main.o -p main -I $PKG_PATH　main.go
go tool compile -o test.o -p main -I $PKG_PATH　test.go
go tool pack vc buildtest.a main.o test.o
go tool link -v -n -o buildtest -L $SRC_PATH -L $PKG_PATH buildtest.a
```

### 問題1 他のファイルの関数のシンボルが見つからない

```
$ go tool compile -o main.o -p main -I $PKG_PATH main.go
main.go:6: undefined: test
```

`main.go`で呼び出している`test()`のシンボルが見つからないためエラーになります。

これは`main.go`に宣言を追加することで解決することができます。


```go:main.go
package main

func test()

func main() {
    test()
}
```

```
$ go tool compile -o main.o -p main -I $PKG_PATH main.go
$ ls main.o
main.o
```

無事コンパイルできました。

上記のようにGoでもC言語のように関数定義とは別に関数宣言をすることが可能です。[^1]

[^1]: 通常、宣言を使うことはないように思えますが、Go本体でもシステムコールを呼ぶために利用しています。


作成したシンボルテーブルを確認してみましょう。`go tool nm`コマンドを使います。

`go tool nm`コマンドの表示結果のシンボルクラスは`nm`コマンドと同様です。
`U`は未定義シンボル、`T`はテキスト(コード)セクション`B`は未初期化データ領域(BSS)、`R`は読み込み専用データセクションです。

以下のように宣言だけしており定義がされていない`test()`は`U %22%22.test`のように正しく未定義シンボルとなっていることが確認できます。(%22%22はmainパッケージを意味する模様、何故こうなっているのかは未調査。%22は" ダブルクォーテーションを表す)

```
$ go tool nm main.o
         U
     1df T %22%22.init
     22e B %22%22.initdone·
     23e R %22%22.init·f
     1af T %22%22.main
     236 R %22%22.main·f
         U %22%22.test
     21e R %22%22.test.args_stackmap
     22e R %22%22.test·f
     226 R gclocals·33cdeccccebe80329f1fdbee7f5874cb
         U runtime.morestack_noctxt
         U runtime.throwinit
```

### 問題2 init関数のシンボルが衝突する

続いて`test.go`をコンパイル、ライブラリ作成、リンクをします。

```
$ go tool compile -o test.o -p main -I $PKG_PATH test.go
$ go tool pack vc buildtest.a main.o test.o
$ go tool link -v -n -o buildtest -L $SRC_PATH -L $PKG_PATH buildtest.a
2017/01/22 14:56:24 duplicate symbol main.init (types 1 and 1) in main and buildtest.a(test.o)
```

リンクエラーになりました。

mainパッケージの`main.init`シンボルの衝突エラーです。
皆さんご存知の通りGoでは.goファイルごとに`init()`が最初に実行されます。今回のように`go tool compile`でコンパイルした`main.go`と`test.go`にそれぞれ`init()`関数がmain.initシンボルとして作成されてしまうため、リンクした`main.o`と`test.o`でシンボルが衝突してしまいます。

`go tool nm`で確認してみます。

```
$ go tool nm main.o test.o
main.o:          U
main.o:      1df T %22%22.init
main.o:      22e B %22%22.initdone·
main.o:      23e R %22%22.init·f
main.o:      1af T %22%22.main
main.o:      236 R %22%22.main·f
main.o:          U %22%22.test
main.o:      21e R %22%22.test.args_stackmap
main.o:      22e R %22%22.test·f
main.o:      226 R gclocals·33cdeccccebe80329f1fdbee7f5874cb
main.o:          U runtime.morestack_noctxt
main.o:          U runtime.throwinit
test.o:          U
test.o:      26e T %22%22.init
test.o:      2dd B %22%22.initdone·
test.o:      2e5 R %22%22.init·f
test.o:      1f2 T %22%22.test
test.o:      2dd R %22%22.test·f
test.o:          U fmt.Printf
test.o:          U fmt.init
test.o:      2d5 R gclocals·33cdeccccebe80329f1fdbee7f5874cb
test.o:      2c9 R go.string."call test()\n"
test.o:      2b9 R go.string.hdr."call test()\n"
test.o:          U runtime.morestack_noctxt
test.o:          U runtime.throwinit
test.o:      2ed R type..importpath.fmt.
```

今回重要なのは`main.o:      1df T %22%22.init`と`test.o:      26e T %22%22.init`の行です。同じシンボルが2つ定義されています。

つまりリンクするためにはシンボルを衝突を回避しなければなりません。

以下のように`test.o`のinitシンボルを強引に変更してからリンクしてみます。


```
$perl -pi -e 's|init|tnit|g' test.o
$ go tool nm main.o test.o
main.o:          U
main.o:      1df T %22%22.init
main.o:      22e B %22%22.initdone·
main.o:      23e R %22%22.init·f
main.o:      1af T %22%22.main
main.o:      236 R %22%22.main·f
main.o:          U %22%22.test
main.o:      21e R %22%22.test.args_stackmap
main.o:      22e R %22%22.test·f
main.o:      226 R gclocals·33cdeccccebe80329f1fdbee7f5874cb
main.o:          U runtime.morestack_noctxt
main.o:          U runtime.throwinit
test.o:          U
test.o:      1f2 T %22%22.test
test.o:      2dd R %22%22.test·f
test.o:      26e T %22%22.tnit
test.o:      2dd B %22%22.tnitdone·
test.o:      2e5 R %22%22.tnit·f
test.o:          U fmt.Printf
test.o:          U fmt.tnit
test.o:      2d5 R gclocals·33cdeccccebe80329f1fdbee7f5874cb
test.o:      2c9 R go.string."call test()\n"
test.o:      2b9 R go.string.hdr."call test()\n"
test.o:          U runtime.morestack_noctxt
test.o:          U runtime.throwtnit
test.o:      2ed R type..importpath.fmt.
$ go tool pack vc buildtest.a main.o test.o
$ go tool link -o buildtest -L $SRC_PATH -L $PKG_PATH buildtest.a
```

`test.o:      26e T %22%22.tnit`のように変更されていることが確認できます。

無理やりコンパイルを通しているだけなので正しく実行はできません。(実行できるように修正したかったのですがオブジェクトファイルフォーマットの理解が足りないためできませんでした)

```go
./buildtest
```

では普段使っている`go build`では、この問題をどのように解決しているのでしょうか？
`go build`は最初に紹介した通り`go compile`時に必ず同一パッケージ内の".go"ファイルを同時にコンパイルします。その時に複数ファイルがある場合には、initのシンボル名を衝突しないように変更します。もしくはinit関数が不要な場合はシンボルを削除するようです。[^2]

[^2]: 詳細はこちらを参照。[packageに複数のinitがあるときの挙動](http://qiita.com/astronoka/items/aa2f271d280863cedf5e)


`go build`にはC言語のようにファイルごとにオブジェクトを作成しておいて、変更があったファイルのみ差分コンパイルする仕組みがありません。
つまり**Goは必ず1つのパッケージ内では複数ファイルを同時にコンパイルして、ライブラリを作成するまでが仕様になっています。**Goらしい割り切った仕様ですが、それでいいよねって話です。(ここらへんについてはドキュメントやソースを追ったわけではないので確信がありません余裕があったらソースを読んでみようと思います。)

そもそも差分コンパイルのために多くのC言語プログラマーがMakefileの作成で苦しんできた歴史を考えると、Goの決断は素晴らしいと思います。また差分コンパイルができなくても困らないのは、そもそも**Goのコンパイルがとても速い**ためです。C言語で毎回フルコンパイルさせられたら堪りません。

本当にGoは素晴らしいですね。



コマンドとしては以下のようになります。

```
$ go tool compile -o buildtest.a -p main -I $SRC_PATH -I $PKG_PATH -pack main.go test.go
```

今回は、`5dd T %22%22.init`が1つかないことが確認できます。

```
$ go tool nm buildtest.a
         U
     5dd T %22%22.init
     664 B %22%22.initdone·
     674 R %22%22.init·f
     4de T %22%22.main
     664 R %22%22.main·f
     50e T %22%22.test
     66c R %22%22.test·f
         U fmt.Println
         U fmt.init
     628 R gclocals·33cdeccccebe80329f1fdbee7f5874cb
     65c R gclocals·69c1753bd5f81501d95132d08af04464
     64c R gclocals·e29b39dba2f7b47ee8f21f123fdd2633
     640 R go.string."Hello, Test!"
     630 R go.string.hdr."Hello, Test!"
     7c3 R go.typelink.*[1]interface {}
     787 R go.typelink.[1]interface {}
     728 R go.typelink.[]interface {}
         U runtime.algarray
         U runtime.convT2E
     6dd R runtime.gcbits.01
     67c R runtime.gcbits.03
         U runtime.morestack_noctxt
         U runtime.throwinit
     78b R type.*[1]interface {}
     7c7 R type..importpath.fmt.
     72c R type..namedata.*[1]interface {}.
     6de R type..namedata.*[]interface {}.
     67d R type..namedata.*interface {}.
     73f R type.[1]interface {}
     6f0 R type.[]interface {}
     68d R type.interface {}
         U type.string
```


## 複数パッケージが存在する場合

最後に`go build`時に複数パッケージが存在する場合を簡単に紹介します。


```
$ ls ./ ./hello
./:
hello/ main.go  test.go

./hello:
hello.go
```

このような構成の時は、以下のようにビルドされます。

```
SRC_PATH="$GOPATH/src"
PKG_PATH="$GOPATH/pkg/darwin_amd64"

cd hello
go tool compile -o ../hello.a -p github.com/sonatard/buildtest/hello -pack ./hello.go

cd ..
go tool compile -o buildtest.a -p main -I $SRC_PATH -I $PKG_PATH -pack ./main.go ./test.go
go tool link -o buildtest -L $SRC_PATH -L $PKG_PATH buildtest.a
```

以下に、実際にコンパイルできるソース一式を置いておきましたのでお試しください。

[Github - sonatard/buildtest](https://github.com/sonatard/buildtest)


## まとめ

安定しているGoの環境では中々バイナリレベルのトラブルになることはないと思いますが
何かの参考になればと思います。
今後はもう少しソースを読んでいきたいです。


## 関連

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

- [Better C - Go言語と整数 #golang](http://qiita.com/sonatard/items/464a9d45c689386edfe1)