[インデックス 14218] ファイルの概要
このコミットは、Goコンパイラの一部であるcmd/6gにおけるcgen_bmul関数でのクラッシュを修正するものです。具体的には、バイト型(byte)の乗算が原因で発生する内部コンパイラエラー「bad width: 0463」を解決します。この問題は、特に小さな整数型が関わる演算において、コンパイラがレジスタ幅を誤って解釈することで引き起こされていました。
コミット
commit 335eef85c33d80c068db766b0841a12d7c1e6d79
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Fri Oct 26 00:29:44 2012 +0200
cmd/6g: fix crash in cgen_bmul.
Used to print:
../test/torture.go:116: internal compiler error: bad width: 0463 (../test/torture.go:116) MOVB ,BX (0, 8)
R=nigeltao, rsc
CC=golang-dev
https://golang.org/cl/6736068
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/335eef85c33d80c068db766b0841a12d7c1e6d79
元コミット内容
このコミットは、Goコンパイラのcmd/6g(AMD64アーキテクチャ向けコンパイラ)において、バイト型(byte)の乗算処理を行うcgen_bmul関数で発生していたクラッシュを修正します。クラッシュは「internal compiler error: bad width: 0463」というメッセージを伴い、MOVB命令(バイト移動命令)が不正な幅で生成されることが原因でした。この問題は、test/torture.goに追加された特定のテストケース(determinantByte関数)によって再現されました。
変更の背景
Go言語では、byte型はuint8のエイリアスであり、8ビットの符号なし整数を表します。Goの仕様では、数値演算においてオペランドの型が異なる場合や、特定の演算子(例えば乗算)が使用される場合に、より広い型への「型昇格(type promotion)」が行われることがあります。しかし、コンパイラのコード生成段階でこの型昇格が適切に処理されないと、予期せぬエラーやクラッシュが発生する可能性があります。
このコミットで修正された問題は、cgen_bmul関数がバイト型の乗算を処理する際に、中間結果やレジスタの割り当てにおいて、期待される64ビット幅ではなく、誤った幅(0463は8進数で307、おそらくバイト幅を誤って解釈した結果)で命令を生成してしまったことに起因します。これにより、コンパイラが不正な機械語命令を生成しようとして内部エラーを引き起こし、コンパイルが中断していました。
test/torture.goに追加されたdeterminantByte関数は、byte型の4x4行列の行列式を計算するもので、多数のバイト型乗算を含んでいます。このような複雑なバイト型演算が、コンパイラの既存のバグを顕在化させるトリガーとなりました。
前提知識の解説
Goコンパイラ (cmd/6g)
Go言語のコンパイラは、初期にはgc(Go Compiler)と呼ばれ、各アーキテクチャ(例: 6gはAMD64、8gはARM、5gはx86)ごとに独立したコンパイラが存在しました。cmd/6gは、AMD64(x86-64)アーキテクチャ向けのGoプログラムをコンパイルする役割を担っていました。これらのコンパイラは、Goのソースコードを中間表現に変換し、最終的にターゲットアーキテクチャの機械語に変換するバックエンド部分を含んでいます。
cgen_bmul関数
cgen_bmulは「code generation for binary multiplication」の略で、Goコンパイラのコード生成フェーズにおいて、二項乗算(*演算子)を処理するための関数です。この関数は、Goのソースコード中の乗算演算を、ターゲットアーキテクチャ(この場合はAMD64)の適切な機械語命令に変換する責任を負っています。特に、異なるサイズの整数型(例: byte, int, int64など)の乗算を正確に処理し、必要に応じて型昇格やレジスタの幅調整を行う必要があります。
整数型と型昇格 (Type Promotion)
Go言語では、byte(uint8)、int8、uint16、int16などの小さな整数型が存在します。これらの型は、メモリ効率のために使用されますが、算術演算を行う際には注意が必要です。多くのプログラミング言語と同様に、Goでも特定の演算(特に乗算や除算)では、オペランドがより広い型(例: intやint64)に自動的に昇格されることがあります。これは、中間結果が元の型の範囲を超えてオーバーフローするのを防ぐためです。
例えば、byte * byteの乗算では、結果がbyteの最大値255を超える可能性があるため、通常はint型に昇格して計算が行われます。コンパイラは、この型昇格を正しく認識し、それに応じたレジスタ幅で演算を行う機械語を生成する必要があります。
レジスタと幅 (Register Width)
CPUには、データを一時的に保持するためのレジスタがあります。AMD64アーキテクチャでは、64ビットのレジスタ(例: RAX, RBXなど)が一般的ですが、これらのレジスタは部分的にアクセスすることもできます(例: AXはRAXの下位16ビット、ALはRAXの下位8ビット)。コンパイラは、変数の型に応じて適切なレジスタの幅を選択し、それに対応する機械語命令(例: MOVBはバイト移動、MOVQはクワッドワード(64ビット)移動)を生成する必要があります。
「bad width: 0463」というエラーは、コンパイラがMOVB命令を生成しようとした際に、そのオペランドの幅が期待される8ビット(バイト)ではなく、不正な値(0463)として認識されたことを示唆しています。これは、型昇格の処理やレジスタ割り当てのロジックに問題があったことを意味します。
技術的詳細
このバグは、cgen_bmul関数がバイト型の乗算を処理する際に、オペランドを一時的に保持するレジスタの型を誤って扱っていたことに起因します。
修正前のコードでは、cgen_bmulはまず、乗算のオペランド(nlとnr)を64ビット幅のレジスタ(n1, n2)にコピーしようとしていました。
// copy from byte to full registers
t = types[TUINT64];
if(issigned[nl->type->etype])
t = types[TINT64];
regalloc(&n1, t, res);
cgen(nl, &n1);
regalloc(&n2, t, N);
cgen(nr, &n2);
このアプローチの問題点は、cgen(nl, &n1)がnl(例えばbyte型)の値をn1(uint64またはint64型)にコピーする際に、nlの元の型情報が失われ、n1が常に64ビット幅として扱われてしまう可能性があったことです。特に、byte型のような小さな整数型の場合、コンパイラはまず8ビットレジスタに値をロードし、その後必要に応じて64ビットレジスタに符号拡張またはゼロ拡張してコピーする必要があります。しかし、この処理が正しく行われず、MOVBのようなバイト操作命令が、実際には64ビットレジスタの一部を操作しようとした際に、コンパイラがその幅を誤って認識してクラッシュしていました。
修正後のコードでは、この問題を解決するために、まずオペランドをそれぞれの元の型(例えばbyte型)に対応する「8ビット」レジスタに生成し、その後で64ビット幅の乗算を行うように変更されました。
// generate operands in "8-bit" registers.
regalloc(&n1b, nl->type, res);
cgen(nl, &n1b);
regalloc(&n2b, nr->type, N);
cgen(nr, &n2b);
// perform full-width multiplication.
t = types[TUINT64];
if(issigned[nl->type->etype])
t = types[TINT64];
nodreg(&n1, t, n1b.val.u.reg);
nodreg(&n2, t, n2b.val.u.reg);
a = optoas(op, t);
gins(a, &n2, &n1);
// truncate.
gmove(&n1, res);
regfree(&n1b);
regfree(&n2b);
この変更のポイントは以下の通りです。
- 一時的なバイト幅レジスタの導入:
n1bとn2bという新しいNode変数が導入され、これらはnl->typeとnr->type(元のオペランドの型、例えばbyte)に基づいてレジスタを割り当てられます。これにより、cgen(nl, &n1b)はnlの値をその本来の型(バイト幅)でレジスタにロードします。 - 64ビット幅への明示的な変換:
nodreg(&n1, t, n1b.val.u.reg)とnodreg(&n2, t, n2b.val.u.reg)の呼び出しにより、n1bとn2bに格納された値が、明示的に64ビット幅のレジスタとして扱われるように設定されます。ここでtはTUINT64またはTINT64です。これにより、乗算演算(gins(a, &n2, &n1))は常に64ビット幅で行われることが保証されます。 - レジスタの解放順序の変更:
regfree(&n1b)とregfree(&n2b)が追加され、一時的に使用したバイト幅レジスタが適切に解放されます。
この修正により、コンパイラはバイト型の乗算を行う際に、まず値を適切なバイト幅レジスタにロードし、その後、乗算のために64ビット幅に拡張して演算を行うという、より堅牢なコード生成パスをたどるようになります。これにより、「bad width」エラーが解消され、コンパイラのクラッシュが防止されました。
コアとなるコードの変更箇所
変更はsrc/cmd/6g/ggen.cファイル内のcgen_bmul関数に集中しています。
--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -984,15 +984,10 @@ ret:
void
cgen_bmul(int op, Node *nl, Node *nr, Node *res)
{
- Node n1, n2, *tmp;
+ Node n1, n2, n1b, n2b, *tmp;
Type *t;
int a;
- // copy from byte to full registers
- t = types[TUINT64];
- if(issigned[nl->type->etype])
- t = types[TINT64];
-
// largest ullman on left.
if(nl->ullman < nr->ullman) {
tmp = nl;
@@ -1000,15 +995,25 @@ cgen_bmul(int op, Node *nl, Node *nr, Node *res)
nr = tmp;
}
- regalloc(&n1, t, res);
- cgen(nl, &n1);
- regalloc(&n2, t, N);
- cgen(nr, &n2);
+ // generate operands in "8-bit" registers.
+ regalloc(&n1b, nl->type, res);
+ cgen(nl, &n1b);
+ regalloc(&n2b, nr->type, N);
+ cgen(nr, &n2b);
+
+ // perform full-width multiplication.
+ t = types[TUINT64];
+ if(issigned[nl->type->etype])
+ t = types[TINT64];
+ nodreg(&n1, t, n1b.val.u.reg);
+ nodreg(&n2, t, n2b.val.u.reg);
a = optoas(op, t);
gins(a, &n2, &n1);
- regfree(&n2);
+
+ // truncate.
gmove(&n1, res);
- regfree(&n1);
+ regfree(&n1b);
+ regfree(&n2b);
}
void
また、バグを再現し、修正を検証するためのテストケースがtest/torture.goに追加されました。
--- a/test/torture.go
+++ b/test/torture.go
@@ -58,6 +58,64 @@ func determinant(m [4][4]float64) float64 {
\tm[0][3]*m[1][2]*m[2][1]*m[3][0]\n}\n \n+// Compute the determinant of a 4x4-matrix by the sum\n+// over all index permutations.\n+func determinantInt(m [4][4]int) int {\n+\treturn m[0][0]*m[1][1]*m[2][2]*m[3][3] -\n+\t\tm[0][0]*m[1][1]*m[2][3]*m[3][2] -\n+\t\tm[0][0]*m[1][2]*m[2][1]*m[3][3] +\n+\t\tm[0][0]*m[1][2]*m[2][3]*m[3][1] +\n+\t\tm[0][0]*m[1][3]*m[2][1]*m[3][2] -\n+\t\tm[0][0]*m[1][3]*m[2][2]*m[3][1] -\n+\t\tm[0][1]*m[1][0]*m[2][2]*m[3][3] +\n+\t\tm[0][1]*m[1][0]*m[2][3]*m[3][2] +\n+\t\tm[0][1]*m[1][2]*m[2][0]*m[3][3] -\n+\t\tm[0][1]*m[1][2]*m[2][3]*m[3][0] -\n+\t\tm[0][1]*m[1][3]*m[2][0]*m[3][2] +\n+\t\tm[0][1]*m[1][3]*m[2][2]*m[3][0] +\n+\t\tm[0][2]*m[1][0]*m[2][1]*m[3][3] -\n+\t\tm[0][2]*m[1][0]*m[2][3]*m[3][1] -\n+\t\tm[0][2]*m[1][1]*m[2][0]*m[3][3] +\n+\t\tm[0][2]*m[1][1]*m[2][3]*m[3][0] +\n+\t\tm[0][2]*m[1][3]*m[2][0]*m[3][1] -\n+\t\tm[0][2]*m[1][3]*m[2][1]*m[3][0] -\n+\t\tm[0][3]*m[1][0]*m[2][1]*m[3][2] +\n+\t\tm[0][3]*m[1][0]*m[2][2]*m[3][1] +\n+\t\tm[0][3]*m[1][1]*m[2][0]*m[3][2] -\n+\t\tm[0][3]*m[1][1]*m[2][2]*m[3][0] -\n+\t\tm[0][3]*m[1][2]*m[2][0]*m[3][1] +\n+\t\tm[0][3]*m[1][2]*m[2][1]*m[3][0]\n+}\n+\n+// Compute the determinant of a 4x4-matrix by the sum\n+// over all index permutations.\n+func determinantByte(m [4][4]byte) byte {\n+\treturn m[0][0]*m[1][1]*m[2][2]*m[3][3] -\n+\t\tm[0][0]*m[1][1]*m[2][3]*m[3][2] -\n+\t\tm[0][0]*m[1][2]*m[2][1]*m[3][3] +\n+\t\tm[0][0]*m[1][2]*m[2][3]*m[3][1] +\n+\t\tm[0][0]*m[1][3]*m[2][1]*m[3][2] -\n+\t\tm[0][0]*m[1][3]*m[2][2]*m[3][1] -\n+\t\tm[0][1]*m[1][0]*m[2][2]*m[3][3] +\n+\t\tm[0][1]*m[1][0]*m[2][3]*m[3][2] +\n+\t\tm[0][1]*m[1][2]*m[2][0]*m[3][3] -\n+\t\tm[0][1]*m[1][2]*m[2][3]*m[3][0] -\n+\t\tm[0][1]*m[1][3]*m[2][0]*m[3][2] +\n+\t\tm[0][1]*m[1][3]*m[2][2]*m[3][0] +\n+\t\tm[0][2]*m[1][0]*m[2][1]*m[3][3] -\n+\t\tm[0][2]*m[1][0]*m[2][3]*m[3][1] -\n+\t\tm[0][2]*m[1][1]*m[2][0]*m[3][3] +\n+\t\tm[0][2]*m[1][1]*m[2][3]*m[3][0] +\n+\t\tm[0][2]*m[1][3]*m[2][0]*m[3][1] -\n+\t\tm[0][2]*m[1][3]*m[2][1]*m[3][0] -\n+\t\tm[0][3]*m[1][0]*m[2][1]*m[3][2] +\n+\t\tm[0][3]*m[1][0]*m[2][2]*m[3][1] +\n+\t\tm[0][3]*m[1][1]*m[2][0]*m[3][2] -\n+\t\tm[0][3]*m[1][1]*m[2][2]*m[3][0] -\n+\t\tm[0][3]*m[1][2]*m[2][0]*m[3][1] +\n+\t\tm[0][3]*m[1][2]*m[2][1]*m[3][0]\n+}\n+\n // A right-leaning tree of byte multiplications.\n func righttree(a, b, c, d uint8) uint8 {\n \treturn a * (b * (c * (d *\n```
## コアとなるコードの解説
`cgen_bmul`関数の修正は、Goコンパイラがバイト型のような小さな整数型の乗算をどのように扱うかという、コード生成の低レベルな詳細に関わっています。
**修正前**:
以前のコードでは、`cgen_bmul`は乗算のオペランドを直接64ビット幅のレジスタにコピーしようとしていました。これは、Goの型昇格のセマンティクス(例えば`byte * byte`の結果が`int`になる)を考慮したものでしたが、実装に問題がありました。`cgen(nl, &n1)`のような呼び出しでは、`nl`が`byte`型であっても、`n1`が64ビットレジスタとして割り当てられているため、コンパイラが`nl`の値を64ビットレジスタにロードする際に、その元のバイト幅の情報を適切に保持せず、結果として不正な命令(`MOVB`が不正な幅で生成される)を生成してしまう可能性がありました。これは、コンパイラがレジスタの「幅」を追跡する内部状態と、実際の命令生成の間の不整合を示しています。
**修正後**:
新しいコードは、この不整合を解消するために、より段階的なアプローチを採用しています。
1. **`n1b`, `n2b`の導入**: まず、`nl`と`nr`の値を、それぞれの元の型(例えば`byte`)に対応するレジスタに生成します。これは`regalloc(&n1b, nl->type, res); cgen(nl, &n1b);`と`regalloc(&n2b, nr->type, N); cgen(nr, &n2b);`で行われます。これにより、`n1b`と`n2b`は、それぞれ`nl`と`nr`のバイト幅の値を正しく保持するレジスタとして扱われます。
2. **`nodreg`による型変換**: 次に、`nodreg(&n1, t, n1b.val.u.reg);`と`nodreg(&n2, t, n2b.val.u.reg);`の行が重要です。`nodreg`関数は、既存のレジスタ(`n1b.val.u.reg`など)の内容を、指定された新しい型`t`(この場合は`TUINT64`または`TINT64`)を持つ新しい`Node`(`n1`, `n2`)に関連付けます。これにより、コンパイラは`n1b`と`n2b`に格納されたバイト幅の値を、乗算のために64ビット幅に「拡張」して扱うべきであることを明示的に認識します。このステップで、必要に応じて符号拡張やゼロ拡張が行われ、値が64ビットレジスタに正しく配置されます。
3. **64ビット乗算**: その後、`gins(a, &n2, &n1);`によって、`n1`と`n2`に格納された64ビット幅の値に対して乗算命令が生成されます。これにより、オーバーフローの心配なく、正確な乗算が行われます。
4. **結果の切り詰めとレジスタ解放**: 最後に、`gmove(&n1, res);`で結果が最終的な`res`ノードに移動され、`regfree(&n1b); regfree(&n2b);`で一時的に使用したレジスタが解放されます。
この修正は、Goコンパイラが異なる整数型の間の型変換とレジスタ割り当てを、より厳密かつ正確に処理するように改善されたことを示しています。特に、小さな整数型が関わる演算において、コンパイラが内部的に正しいレジスタ幅を維持し、適切な機械語命令を生成するための重要な変更です。
`test/torture.go`に追加された`determinantInt`と`determinantByte`関数は、それぞれ`int`型と`byte`型の行列式を計算するもので、特に`determinantByte`は多数のバイト型乗算を含むため、このバグを効果的に再現するテストケースとして機能しました。
## 関連リンク
* **Go Issue Tracker (関連する可能性のあるIssue)**:
* Goの古いバグトラッカーはGoogle Code上にありましたが、現在はGitHub Issuesに移行しています。このコミットのCL(Change List)番号`6736068`を検索すると、関連するIssueが見つかる可能性があります。
* [Go Project Issues on GitHub](https://github.com/golang/go/issues)
* **Go Compiler Internals**:
* Goコンパイラの内部構造に関する公式ドキュメントは少ないですが、ソースコード自体が最も正確な情報源です。
* [Go Source Code on GitHub](https://github.com/golang/go)
## 参考にした情報源リンク
* **Go Language Specification**: Go言語の型システムと演算子の振る舞いに関する公式な定義。
* [The Go Programming Language Specification](https://go.dev/ref/spec)
* **AMD64 Architecture Manuals**: x86-64アーキテクチャのレジスタと命令セットに関する詳細情報。
* Intel 64 and IA-32 Architectures Software Developer's Manuals (Intelのウェブサイトで入手可能)
* AMD64 Architecture Programmer's Manuals (AMDのウェブサイトで入手可能)
* **Go Compiler Design Documents (非公式/コミュニティ)**:
* Goコンパイラの進化に関するブログ記事やプレゼンテーションが、Goコミュニティによって作成されている場合があります。
* "Go's New Backend" (Go 1.7以降のSSAバックエンドに関する記事など)
* "A brief tour of the Go compiler" (古いコンパイラに関する情報も含む可能性あり)
* **Go Code Review Comments**: Goのコードレビューシステム(GerritベースのCL)のコメントは、しばしば設計上の決定やバグの詳細に関する貴重な情報を含んでいます。
* [https://golang.org/cl/6736068](https://golang.org/cl/6736068) (このコミットのChange Listページ)