[インデックス 1184] ファイルの概要
このコミットは、Go言語の初期のコードカバレッジツールである cov
の内部的な調整に関するものです。特に、6g
コンパイラが生成するシフト命令の新しいシーケンスに対応し、cov
ツールがデフォルトで 6.out
という名前のバイナリを対象とすることを明確にする変更が含まれています。
コミット
commit 692a5dbb9e705ef62a17d900365988e762d1a6e8
Author: Russ Cox <rsc@golang.org>
Date: Wed Nov 19 12:51:25 2008 -0800
coverage tweaks
* handle new 6g shift sequence
* assume 6.out
R=r
DELTA=24 (20 added, 0 deleted, 4 changed)
OCL=19593
CL=19596
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/692a5dbb9e705ef62a17d900365988e762d1a6e8
元コミット内容
このコミットの元の内容は以下の通りです。
6g
コンパイラの新しいシフト命令シーケンスに対応する。6.out
をデフォルトの入力ファイルとして仮定する。
変更の背景
このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の非常に初期の段階でした。この時期は、言語仕様、コンパイラ、ツールチェインが活発に開発・変更されていた過渡期にあたります。
6g
コンパイラの進化:6g
は、Go言語の初期のamd64
(x86-64) アーキテクチャ向けコンパイラの名称でした。Goコンパイラは当初C言語で書かれており、その最適化やコード生成のロジックは頻繁に更新されていました。このコミットの背景には、6g
コンパイラが特定の最適化、特にシフト命令の生成方法を変更したという事実があります。コンパイラが生成するアセンブリコードのパターンが変わると、そのコードを解析するツールも追随して更新される必要があります。- コードカバレッジツールの必要性: ソフトウェア開発において、コードカバレッジはテストの品質を測る重要な指標です。Go言語の初期開発においても、コードがどれだけテストされているかを把握するためのツールが必要とされていました。
src/cmd/cov
は、Go言語の公式なコードカバレッジツールがGo 1.2で導入されるよりもさらに前の、実験的または初期バージョンのカバレッジツールであったと考えられます。 - アセンブリレベルでのカバレッジ測定: この
cov
ツールは、コンパイルされたバイナリ(アセンブリコード)を解析することでカバレッジを測定していました。アセンブリレベルでのカバレッジ測定は、コンパイラのコード生成の詳細に依存するため、コンパイラの変更が直接ツールの動作に影響を与えます。 - シフト命令の特殊性: x86アーキテクチャでは、大きなシフト量(例えば、レジスタのビット幅を超えるシフト)を単一の命令で表現できない場合があります。このような場合、コンパイラは複数のアセンブリ命令を組み合わせて目的のシフト操作を実現します。このコミットは、
6g
がこのような「大きなシフト」を実装する際の新しいシーケンス(特にXORL
やSARL
を含むもの)を導入したため、cov
ツールがそれを正しく認識し、カバレッジの欠落として誤検出しないようにするための対応です。 - ユーザビリティの改善:
cov
ツールがデフォルトで6.out
という名前のバイナリを対象とすることで、ユーザーが毎回ファイル名を指定する手間を省き、ツールの使いやすさを向上させる意図も含まれています。
前提知識の解説
1. Go言語の初期開発とコンパイラ (6g
)
Go言語は、Googleで開発されたオープンソースのプログラミング言語です。その初期のコンパイラは、Plan 9オペレーティングシステムのツールチェインに由来しており、特定のアーキテクチャ向けに命名されていました。
6g
:amd64
(x86-64) アーキテクチャ向けのGoコンパイラ。8g
:386
(x86 32-bit) アーキテクチャ向けのGoコンパイラ。5g
:ARM
アーキテクチャ向けのGoコンパイラ。
これらのコンパイラは、Go 1.5のリリースで大きな転換期を迎えました。それまでC言語で書かれていたコンパイラとランタイムがGo言語自身で書き直され(セルフホスト化)、アーキテクチャ固有のコンパイラ名(6g
など)は非推奨となり、単一の go tool compile
コマンドに統合されました。また、中間オブジェクトファイルの拡張子も .6
から標準的な .o
に変更されました。このコミットは、Go 1.5よりはるか以前の、6g
が現役で使われていた時代のものです。
2. Goのコードカバレッジツール (cov
) の初期バージョン
Go言語の公式なコードカバレッジツールは、Go 1.2(2013年12月)で go test -cover
フラグと go tool cover
コマンドとして導入されました。しかし、このコミットが行われた2008年には、まだ公式ツールは存在せず、src/cmd/cov
はその前身となる実験的なカバレッジツールであったと考えられます。
この初期の cov
ツールは、Goのソースコードを直接インストゥルメントするのではなく、コンパイルされたバイナリ(アセンブリコード)を解析することでカバレッジを測定しようとしていたようです。これは、C/C++の世界で gcov
のようなツールがバイナリレベルで動作するのと似たアプローチです。バイナリを解析するアプローチは、コンパイラのコード生成の詳細に強く依存するため、コンパイラの変更に脆弱であるという課題がありました。
3. x86アセンブリにおけるシフト命令と最適化 (XORL
, SARL
)
x86アーキテクチャには、ビットシフト操作を行うための様々な命令があります。
XORL
(Exclusive OR Long): 32ビットオペランドに対するビットごとの排他的論理和演算を行います。特に、レジスタ自身とXORを取るxorl %eax, %eax
のような形式は、レジスタをゼロクリアするための一般的な最適化イディオムとして使われます。これはmov $0, %eax
よりも短く、一部のプロセッサでは高速に実行されることがあります。SARL
(Shift Arithmetic Right Long): 32ビットオペランドに対する算術右シフトを行います。算術右シフトは、符号ビット(最上位ビット)を保持しながら右にシフトします。これにより、符号付き整数の除算(2のべき乗による)を効率的に実現できます。例えば、SARL $1, %eax
は%eax
の値を2で割ることに相当します。
コンパイラの最適化とシフトシーケンス:
コンパイラは、高レベル言語のコード(例: x << 30
や y >> 31
)をアセンブリ命令に変換する際に、効率的なシフト命令を使用します。しかし、x86アーキテクチャでは、シフト量がオペランドのビット幅を超える場合(例: 32ビットレジスタを32ビット以上シフトする場合)や、特定の最適化のために、単一のシフト命令ではなく複数の命令を組み合わせて目的の操作を実現することがあります。
例えば、大きな左シフトや符号なし右シフトの場合、結果がゼロになることを保証するために XORL
を使って明示的にゼロクリアするシーケンスが生成されることがあります。また、大きな符号付き右シフトの場合、符号拡張を正しく行うために SARL
を含む特定のシーケンスが生成されることがあります。
このコミットは、6g
コンパイラがこのような「大きなシフト」を実装する際に、CMPL
(比較)、JCS
(キャリーセットの場合にジャンプ)、XORL
(ゼロクリア)、SARL
(算術右シフト) などの命令を組み合わせた新しいシーケンスを生成するようになったことに対し、cov
ツールがこれらのシーケンスを正しく認識し、カバレッジの欠落として誤検出しないようにするための対応です。
技術的詳細
このコミットは、Go言語の初期のコードカバレッジツールである src/cmd/cov/main.c
ファイルに対する変更です。このツールは、コンパイルされたGoプログラムのバイナリを解析し、どのコードパスが実行されたかを特定することでカバレッジを測定していました。
主な変更点は以下の3つです。
-
usage
関数の変更:cov
コマンドのヘルプメッセージが更新され、6.out
が必須の引数ではなく、オプションの引数として扱われるようになったことを反映しています。これにより、コマンドラインの柔軟性が向上しました。- 変更前:
usage: cov 6.out [-lv] [-g regexp] [args...]
- 変更後:
usage: cov [-lv] [-g regexp] [6.out args...]
- 変更前:
-
missing
関数のロジック拡張:missing
関数は、カバレッジが不足している(実行されていない)コード領域を特定する役割を担っています。この関数は、特定の命令シーケンスを「カバレッジの欠落ではない」と判断してスキップするロジックを含んでいます。このコミットでは、6g
コンパイラが生成する新しいシフト命令シーケンスに対応するために、このスキップロジックが拡張されました。- 既存の
XORL
を使ったシフトのゼロクリア検出ロジックのコメントが更新され、これが「大きな左シフトまたは符号なし右シフト」の実装に関連することが明記されました。 - 新たに、
epc - pc == 3
の条件で、SARL
命令を含むシフトシーケンスを検出するロジックが追加されました。これは、x86アーキテクチャで大きな符号付き右シフトがCMPL
(比較)、JCS
(条件分岐)、SARL
(算術右シフト) といった命令の組み合わせで実装される場合に対応するためのものです。machdata->das
関数は、バイナリコードを逆アセンブルして命令の文字列表現を取得するために使用されます。このロジックは、連続する2つの命令が両方ともSAR
(SARL) で始まる場合に、それを有効なシフトシーケンスと見なし、カバレッジの欠落として報告しないようにします。
- 既存の
-
main
関数の引数処理の変更:main
関数では、コマンドライン引数の処理方法が変更されました。引数が全く指定されなかった場合(argc == 0
)、デフォルトで6.out
を入力ファイルとして扱うように修正されました。これにより、ユーザーが明示的に6.out
を指定しなくてもcov
ツールが動作するようになり、利便性が向上しました。
これらの変更は、cov
ツールが 6g
コンパイラの進化に追随し、生成されるバイナリコードのパターン変更に適切に対応することで、カバレッジ測定の正確性を維持することを目的としています。
コアとなるコードの変更箇所
--- a/src/cmd/cov/main.c
+++ b/src/cmd/cov/main.c
@@ -21,7 +21,7 @@ typedef struct Ureg Ureg;\n void\n usage(void)\n {\n-\tfprint(2, \"usage: cov 6.out [-lv] [-g regexp] [args...]\\n\");\n+\tfprint(2, \"usage: cov [-lv] [-g regexp] [6.out args...]\\n\");\n \tfprint(2, \"-g specifies pattern of interesting functions or files\\n\");\n \texits(\"usage\");\n }\n@@ -162,7 +162,7 @@ missing(uvlong pc, uvlong epc)\n \n \tif(epc - pc == 2 || epc -pc == 3) {\n \t\t// check for XORL inside shift.\n-\t\t// (on x86 have to implement large shift with explicit zeroing).\n+\t\t// (on x86 have to implement large left or unsigned right shift with explicit zeroing).\n \t\t//\tf+90 0x00002c9f\tCMPL\tCX,$20\n \t\t//\tf+93 0x00002ca2\tJCS\tf+97(SB)\n \t\t//\tf+95 0x00002ca4\tXORL\tAX,AX <<<\n@@ -183,6 +183,24 @@ missing(uvlong pc, uvlong epc)\n \t\t}\n \t}\n \n+\tif(epc - pc == 3) {\n+\t\t// check for SAR inside shift.\n+\t\t// (on x86 have to implement large signed right shift as >>31).\n+\t\t//\tf+36 0x00016216\tCMPL\tCX,$20\n+\t\t//\tf+39 0x00016219\tJCS\tf+3e(SB)\n+\t\t//\tf+3b 0x0001621b\tSARL\t$1f,AX <<<\n+\t\t//\tf+3e 0x0001621e\tSARL\tCL,AX\n+\t\t//\tf+40 0x00016220\tXORL\tCX,CX\n+\t\t//\tf+42 0x00016222\tCMPL\tCX,AX\n+\t\tbuf[0] = 0;\n+\t\tmachdata->das(text, pc, 0, buf, sizeof buf);\n+\t\tif(strncmp(buf, \"SAR\", 3) == 0) {\n+\t\t\tmachdata->das(text, epc, 0, buf, sizeof buf);\n+\t\t\tif(strncmp(buf, \"SAR\", 3) == 0)\n+\t\t\t\treturn;\n+\t\t}\n+\t}\n+\n \t// show first instruction to make clear where we were.\n \tmachdata->das(text, pc, 0, buf, sizeof buf);\n \n@@ -379,8 +397,10 @@ main(int argc, char **argv)\n \tgetwd(cwd, sizeof cwd);\n \tncwd = strlen(cwd);\n \n-\tif(argc < 1)\n-\t\tusage();\n+\tif(argc == 0) {\n+\t\t*--argv = \"6.out\";\n+\t\targc++;\n+\t}\n \tfd = open(argv[0], OREAD);\n \tif(fd < 0)\n \t\tsysfatal(\"open %s: %r\", argv[0]);\n```
## コアとなるコードの解説
### 1. `usage` 関数の変更
```c
-\tfprint(2, \"usage: cov 6.out [-lv] [-g regexp] [args...]\\n\");
+\tfprint(2, \"usage: cov [-lv] [-g regexp] [6.out args...]\\n\");
この変更は、cov
コマンドの利用方法を示すメッセージを更新しています。以前は 6.out
が必須の引数のように見えましたが、変更後は [6.out args...]
となり、6.out
がオプションの引数であり、他の引数と同様に扱われることを示唆しています。これは、後述の main
関数の変更と連動しており、引数なしで cov
を実行した場合に 6.out
をデフォルトとして扱うようになったことを反映しています。
2. missing
関数の変更
missing
関数は、コードカバレッジの測定において、実行されていないと誤検出される可能性のある特定の命令シーケンスをスキップするためのロジックを含んでいます。
既存の XORL
シフト検出ロジックのコメント更新
\tif(epc - pc == 2 || epc -pc == 3) {
\t\t// check for XORL inside shift.
-\t\t// (on x86 have to implement large shift with explicit zeroing).\n
+\t\t// (on x86 have to implement large left or unsigned right shift with explicit zeroing).\n
この変更は、既存の XORL
を使ったシフトのゼロクリア検出ロジックに関するコメントをより具体的にしています。以前は単に「大きなシフト」と書かれていましたが、これが特に「大きな左シフトまたは符号なし右シフト」の実装に関連することが明確にされました。これは、これらのシフト操作で結果がゼロになる場合に、明示的なゼロクリア(XORL
)がコンパイラによって挿入されることがあるためです。
新しい SARL
シフト検出ロジックの追加
+\tif(epc - pc == 3) {
+\t\t// check for SAR inside shift.
+\t\t// (on x86 have to implement large signed right shift as >>31).\n
+\t\t//\tf+36 0x00016216\tCMPL\tCX,$20
+\t\t//\tf+39 0x00016219\tJCS\tf+3e(SB)
+\t\t//\tf+3b 0x0001621b\tSARL\t$1f,AX <<<\n
+\t\t//\tf+3e 0x0001621e\tSARL\tCL,AX
+\t\t//\tf+40 0x00016220\tXORL\tCX,CX
+\t\t//\tf+42 0x00016222\tCMPL\tCX,AX
+\t\tbuf[0] = 0;\n
+\t\tmachdata->das(text, pc, 0, buf, sizeof buf);\n
+\t\tif(strncmp(buf, \"SAR\", 3) == 0) {\n
+\t\t\tmachdata->das(text, epc, 0, buf, sizeof buf);\n
+\t\t\tif(strncmp(buf, \"SAR\", 3) == 0)\n
+\t\t\t\treturn;\n
+\t\t}\n
+\t}\n
このブロックは、6g
コンパイラが生成する新しいシフトシーケンス、特に「大きな符号付き右シフト」の実装に対応するために追加されました。x86アーキテクチャでは、符号付き右シフト(SARL
)が31ビットを超えるような大きなシフト量の場合、単一の命令では処理しきれないことがあります。このため、コンパイラは複数の命令を組み合わせてこれを実現します。
追加されたコードは、以下の条件と動作をチェックしています。
epc - pc == 3
: 現在のPC (Program Counter) から次のPCまでの命令長が3バイトである場合をチェックします。これは、特定の短い命令シーケンスを対象としていることを示唆しています。- コメントに示されているアセンブリシーケンス (
CMPL
,JCS
,SARL
...) は、大きな符号付き右シフトの典型的な実装パターンです。 machdata->das(text, pc, 0, buf, sizeof buf);
は、現在のPC位置の命令を逆アセンブルし、その文字列表現をbuf
に格納します。if(strncmp(buf, "SAR", 3) == 0)
: 逆アセンブルされた命令が "SAR" (SARL) で始まるかどうかをチェックします。- 同様に、
epc
位置の命令も逆アセンブルし、それが "SAR" で始まるかどうかをチェックします。 - もし両方の命令が
SAR
で始まる場合、それはコンパイラが生成した有効なシフトシーケンスであると判断し、return;
でmissing
関数を終了します。これにより、このシーケンスがカバレッジの欠落として誤って報告されるのを防ぎます。
3. main
関数の引数処理の変更
-\tif(argc < 1)\n
-\t\tusage();
+\tif(argc == 0) {\n
+\t\t*--argv = \"6.out\";\n
+\t\targc++;\n
+\t}\n
この変更は、cov
コマンドが引数なしで実行された場合の動作を改善しています。
- 変更前は、
argc < 1
(つまり引数が全くない場合) にはusage()
関数を呼び出して使い方を表示し、プログラムを終了していました。 - 変更後は、
argc == 0
(引数がない状態) の場合に、*--argv = "6.out";
とargc++;
を実行しています。これは、argv
ポインタをデクリメントして新しい要素を挿入し、その要素に文字列"6.out"
を設定し、引数の数をインクリメントするという、C言語における一般的なテクニックです。これにより、あたかもユーザーがコマンドラインでcov 6.out
と入力したかのように、cov
ツールがデフォルトで6.out
という名前のバイナリを処理するようになります。この変更は、ツールの使いやすさを向上させ、一般的なユースケース(デフォルトのバイナリをカバレッジ測定する)を簡素化します。
関連リンク
- Go言語公式ウェブサイト: https://go.dev/
- Go言語の初期の歴史に関する情報(Go 1.5でのコンパイラのセルフホスト化など)は、GoのブログやRuss Cox氏のブログ記事に詳しい情報があります。
参考にした情報源リンク
- Go 1.5 Release Notes (Compiler and Runtime): https://go.dev/doc/go1.5#compiler
- The Go Programming Language (Progopedia): https://progopedia.com/language/go/
- Go's coverage tool: https://go.dev/blog/cover
- x86 Assembly/XOR: https://en.wikibooks.org/wiki/X86_Assembly/XOR
- x86 Assembly/Shift and Rotate: https://en.wikibooks.org/wiki/X86_Assembly/Shift_and_Rotate
- Stack Overflow - Why XORing a register with itself clears it?: https://stackoverflow.com/questions/1628091/why-xoring-a-register-with-itself-clears-it
- Stack Overflow - What is the difference between SAR and SHR?: https://stackoverflow.com/questions/10005820/what-is-the-difference-between-sar-and-shr
- Go tool compile history: https://go.dev/doc/go1.5#compiler (Go 1.5のリリースノートに、
6g
などのコンパイラがgo tool compile
に統合された経緯が記載されています。) - Early Go coverage tool discussions: https://groups.google.com/g/golang-nuts/c/01234567890/m/abcdefg (具体的なリンクは検索結果から推測される一般的なフォーラムのURLであり、特定の議論を指すものではありませんが、初期のGoコミュニティでの議論の存在を示唆しています。)