GoSuda

マネージド言語のランタイム構造とGoバイナリのAMD64アセンブリ解析

By Lee Yunjin views ...

マネージド言語とは何か?

マネージド言語とは、アンマネージド言語、すなわちプログラマーが書いたロジックからほとんど逸脱せず実行するだけの言語とは異なり、GC、ランタイム最適化、グリーンスレッド、並行性処理などをランタイムで実行し、ユーザーが危険な低水準の管理を行う必要をなくしてくれる言語である。このような言語の場合、ビジネスロジックだけに集中して開発に没頭できるという利点があるが、一方で、プログラマーの直感と実際のプログラムの動作が異なりうるため、精密なランタイムチューニングが必要な場合もある。まず、マネージド言語の中で最もミニマリスト的な哲学に忠実であり、アセンブリが正直な Go 言語を見ていくことにする。

Go 言語のバイナリ構造

.text.data.gopclntab、.typelink など
実行される機械語コード保存されるデータ言語ランタイムセクション

Go 言語はユーザーが入力したとおりに1:1で機械語へ変換するわけではないため、.text セクションのロジックは言語ランタイムセクションとも密接に関連している。また、ユーザーが別途記述していない runtime.printnl() のような関数が .text セクションのアセンブリに追加される。このような自動的なコード挿入を通じて、Go 言語は開発者を手動管理から解放する手助けをしている。

Go で main 関数の部分だけを見る

まず、簡単な例のソース main.go を作成し、main から AMD64 マシン上で 見てみよう。

 1package main
 2
 3func sayHello(msg string) {
 4    println(msg)
 5}
 6
 7func main() {
 8    sayHello("Hello World")
 9}
10

その後、次のようにビルドする。

1go build main.go
2

Go は簡単な低水準デバッグのために go tool をサポートしている。 go tool でメインパッケージのメイン関数の分だけのアセンブリを見るために、この構文を入力する。

1go tool objdump -s "main\.main" ./main
2

アセンブリ

 1TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
 2  main.go:7             0x468f60                493b6610                CMPQ SP, 0x10(R14)
 3  main.go:7             0x468f64                762f                    JBE 0x468f95
 4  main.go:7             0x468f66                55                      PUSHQ BP
 5  main.go:7             0x468f67                4889e5                  MOVQ SP, BP
 6  main.go:7             0x468f6a                4883ec10                SUBQ $0x10, SP
 7  main.go:8             0x468f6e                90                      NOPL
 8  main.go:4             0x468f6f                e8cca3fcff              CALL runtime.printlock(SB)
 9  main.go:4             0x468f74                488d05da290100          LEAQ 0x129da(IP), AX
10  main.go:4             0x468f7b                bb0b000000              MOVL $0xb, BX
11  main.go:4             0x468f80                e83bacfcff              CALL runtime.printstring(SB)
12  main.go:4             0x468f85                e8f6a5fcff              CALL runtime.printnl(SB)
13  main.go:4             0x468f8a                e811a4fcff              CALL runtime.printunlock(SB)
14  main.go:9             0x468f8f                4883c410                ADDQ $0x10, SP
15  main.go:9             0x468f93                5d                      POPQ BP
16  main.go:9             0x468f94                c3                      RET
17  main.go:7             0x468f95                e8e6afffff              CALL runtime.morestack_noctxt.abi0(SB)
18  main.go:7             0x468f9a                ebc4                    JMP main.main(SB)
19
  • 現在のゴルーチンのスタックフレーム領域が十分かどうかを、ゴルーチン制御ブロックのレジスタ(R14)内部のスタックガード値と現在のスタックポインタ(SP)をCMPQで比較して確認し、不足していればスタック拡張のためのエントリポイントである 0x468f95 アドレスへジャンプ(JBE)する。
  • 以前のベースポインタを保存するために PUSHQ BP でスタックに挿入する。
  • ベースポインタ(BP)レジスタに現在のスタックポインタ(SP)をコピーし、関数開始時のスタック基準点を固定する。
  • その後、16バイトのローカル変数用スタック領域を確保し(SUBQ $0x10, SP)、NOPL を用いて仮想命令で埋めることでCPUキャッシュのアライメントを行う。
  • Go ランタイムで内部文字列の標準出力の同期のために runtime.printlock(SB) を呼び出してロックをかける。
  • LEAQ 命令を用いて、定数として割り当てられた文字列("Hello World")の開始アドレスを、Go ABI 規格に従い第1引数として使われる汎用レジスタ AX に格納する。
  • 次に、文字列の長さを表す値を第2引数レジスタである BX に格納する。(MOVL $0xb, BX、すなわち10進数で11)
  • runtime.printstring(SB) を呼び出し、渡された AX(データアドレス)と BX(長さ)の情報に基づいてコンソールに出力する。
  • 改行処理のために runtime.printnl(SB) を呼び出す。
  • 出力が完了したので、runtime.printunlock(SB) によってロックを解除する。
  • ADDQ $0x10, SP で確保していた16バイトのスタックメモリを復元する。
  • POPQ BP で既存のベースポインタを復元する。
  • RET によって関数を呼び出した箇所へ制御権を返す。
  • 最初のスタック検査で領域が不足していた場合は、0x468f95 アドレスの runtime.morestack_noctxt.abi0(SB) を呼び出し、マネージド言語らしくスタックランタイムを動的に拡張する。
  • スタック拡張が完了すると、再び main.main(SB) のエントリポイントへ復帰(JMP)する。見てのとおり、ビジネスロジックのアセンブリはかなり明確であり、薄いランタイム管理だけが付け加えられた形である。

最適化がない場合

上記の形は、Go コンパイラが分かれている2つの関数を自動的にインライン化して最適化した結果である。しかし、学習のため、この場合は sayHello をインライン化しないようにする。これを行うために、次のフラグでソースをコンパイルする。

1go build -gcflags="-l" main.go
2

シェルで結果を出力してみると、重複するアセンブリが見つかる。

 1yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go
 2
 3go tool objdump -s "main\.sayHello" ./main
 4TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
 5  main.go:3             0x468f60                493b6610               CMPQ SP, 0x10(R14)
 6  main.go:3             0x468f64                7636                   JBE 0x468f9c
 7  main.go:3             0x468f66                55                     PUSHQ BP
 8  main.go:3             0x468f67                4889e5                 MOVQ SP, BP
 9  main.go:3             0x468f6a                4883ec10               SUBQ $0x10, SP
10  main.go:5             0x468f6e                4889442420             MOVQ AX, 0x20(SP)
11  main.go:5             0x468f73                48895c2428             MOVQ BX, 0x28(SP)
12  main.go:4             0x468f78                e8c3a3fcff             CALL runtime.printlock(SB)
13  main.go:4             0x468f7d                488b442420             MOVQ 0x20(SP), AX
14  main.go:4             0x468f82                488b5c2428             MOVQ 0x28(SP), BX
15  main.go:4             0x468f87                e834acfcff             CALL runtime.printstring(SB)
16  main.go:4             0x468f8c                e8efa5fcff             CALL runtime.printnl(SB)
17  main.go:4             0x468f91                e80aa4fcff             CALL runtime.printunlock(SB)
18  main.go:5             0x468f96                4883c410               ADDQ $0x10, SP
19  main.go:5             0x468f9a                5d                     POPQ BP
20  main.go:5             0x468f9b                c3                     RET
21  main.go:3             0x468f9c                4889442408             MOVQ AX, 0x8(SP)
22  main.go:3             0x468fa1                48895c2410             MOVQ BX, 0x10(SP)
23  main.go:3             0x468fa6                e8d5afffff             CALL runtime.morestack_noctxt.abi0(SB)
24  main.go:3             0x468fab                488b442408             MOVQ 0x8(SP), AX
25  main.go:3             0x468fb0                488b5c2410             MOVQ 0x10(SP), BX
26  main.go:3             0x468fb5                eba9                   JMP main.sayHello(SB)
27

インライン化を解除すると、関数呼び出し規格に合わせて引数(AX、BX)を保存するため、スタックポインタのオフセットである 0x20(SP) などに値を再びロードする MOVQ 演算が挿入される。つまり、コンパイラが最適化しているのは、このような不要なメモリ移動演算および呼び出しオーバーヘッドの対象であることが確認された。

次回

次回は Go 言語における if 文、switch 文を扱う予定である。今後時間が取れれば、Go ランタイムセクションも分析する予定である。