[インデックス 14449] ファイルの概要
このコミットは、Goコンパイラ(特にx86アーキテクチャ向けの8g
)におけるバグ修正に関するものです。具体的には、nil
ポインタのフィールドアクセスに対して誤ってLEAL
(Load Effective Address)命令が生成され、コンパイル時に「gins LEAQ nil *A」のようなエラーが発生する問題を解決します。この修正により、コンパイラがnil
ポインタのデリファレンスを正しく検出し、適切なエラーメッセージを出力するようになります。
コミット
- コミットハッシュ:
1bd4a7dbcbf833a5e37cf8d0a6e7fc55c557543b
- 作者: Rémy Oudompheng oudomphe@phare.normalesup.org
- コミット日時: 2012年11月21日 水曜日 08:39:45 +0100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1bd4a7dbcbf833a5e37cf8d0a6e7fc55c557543b
元コミット内容
cmd/8g: fix erroneous LEAL nil.
Fixes #4399.
R=golang-dev, nigeltao
CC=golang-dev
https://golang.org/cl/6845053
変更の背景
このコミットは、Goコンパイラ8g
(x86アーキテクチャ向け)が、nil
ポインタのフィールドにアクセスしようとした際に、誤ったコードを生成してしまうバグ(Issue 4399)を修正するために行われました。
具体的には、((*A)(nil)).a
のようなコード(型A
のnil
ポインタをデリファレンスしてフィールドa
にアクセスする)をコンパイルしようとすると、8g
コンパイラが「gins LEAQ nil *A」というエラーメッセージを出力して停止していました。これは、コンパイラがnil
ポインタに対するLEAL
(Load Effective Address)命令を誤って生成しようとしたためです。
通常、nil
ポインタのデリファレンスは実行時パニック(runtime panic)を引き起こすべき動作であり、コンパイル時にこのような不正な命令生成エラーが発生するのはコンパイラのバグです。このコミットは、コンパイラがこのようなケースを正しく検出し、適切なエラー処理を行うように修正することを目的としています。
前提知識の解説
- Goコンパイラ (
8g
): Go言語のコンパイラは、ターゲットアーキテクチャごとに異なる名前を持っています。8g
はx86(32ビット)アーキテクチャ向けのGoコンパイラを指します。現代のGoでは、go build
コマンドが自動的に適切なコンパイラを選択するため、ユーザーが直接8g
を意識することは少なくなりましたが、Goの初期のコンパイラ実装ではこのようにアーキテクチャ固有のコンパイラ名が使われていました。 LEAL
(Load Effective Address): x86アセンブリ命令の一つで、「実効アドレスをロードする」という意味です。メモリ上のデータそのものではなく、そのデータが格納されているアドレスをレジスタに計算して格納するために使用されます。例えば、LEAL (%EAX, %EBX, 4), %ECX
は、EAX + EBX*4
のアドレスを計算し、その結果をECX
レジスタに格納します。この命令は、ポインタ演算や構造体フィールドへのアクセスなど、アドレス計算が必要な場面でコンパイラによって生成されます。nil
ポインタ: Go言語におけるnil
は、ポインタ、スライス、マップ、チャネル、インターフェース、関数などのゼロ値です。ポインタの場合、nil
は「何も指していない」状態を表します。nil
ポインタをデリファレンス(つまり、nil
が指すメモリ上の値にアクセスしようとすること)は、Goのランタイムにおいてパニックを引き起こす不正な操作です。ODOTPTR
: Goコンパイラの内部表現(AST: Abstract Syntax Tree)におけるノードの種類の一つです。これは「ポインタのフィールドアクセス」を表します。例えば、p.field
というコードでp
がポインタ型の場合、この操作はODOTPTR
ノードとして表現されます。igen
関数とcgen
関数: Goコンパイラのバックエンドにおけるコード生成フェーズの関数です。igen
(intermediate code generation): 中間コードを生成する関数。cgen
(code generation): 最終的な機械語コードを生成する関数。 これらの関数は、ASTをトラバースしながら、Goのソースコードに対応するアセンブリ命令を生成する役割を担っています。
gins
関数: Goコンパイラのバックエンドで、特定のアセンブリ命令を生成するためのユーティリティ関数です。gins(ALEAL, f, t)
のように呼び出され、ALEAL
(LEAL
命令)を生成します。
技術的詳細
このバグは、src/cmd/8g/cgen.c
内の igen
関数が ODOTPTR
ノードを処理する際のロジックに起因していました。
igen
関数は、ASTノードを評価し、その結果をレジスタまたはメモリに配置するためのコードを生成します。ODOTPTR
(ポインタのフィールドアクセス)の場合、通常はポインタ自体を評価し、そのアドレスにオフセットを加えることでフィールドのアドレスを計算します。
問題のコードは以下の部分でした(修正前):
case ODOTPTR:
if(n->left->addable
|| n->left->op == OCALLFUNC
|| n->left->op == OCALLMETH
|| n->left->op == OCALLINTER) {
// igen-able nodes.
igen(n->left, &n1, res);
regalloc(a, types[tptr], &n1);
gmove(&n1, a);
regfree(&n1);
} else {
regalloc(a, types[tptr], res);
cgen(n->left, a);
}
ここで、n->left
はポインタのベースとなる式(例: ((*A)(nil))
の ((*A)(nil))
部分)を表します。
n->left->addable
は、その式が直接アドレス指定可能かどうかを示します。nil
ポリインタはaddable
ではありません。
OCALLFUNC
, OCALLMETH
, OCALLINTER
は関数呼び出しを表します。
問題は、n->left
が nil
ポインタ定数である場合、上記の if
文の条件に合致せず、else
ブロックに入ってしまうことでした。else
ブロックでは、cgen(n->left, a)
が呼び出されます。cgen
は式を評価してその値をレジスタ a
に格納しようとしますが、nil
ポインタ定数に対してLEAL
命令を生成しようとすると、gins
関数内で「gins LEAQ nil %T」というfatal
エラーが発生していました。
これは、gins
関数(src/cmd/8g/gsubr.c
)のALEAL
命令を処理する部分に、nil
定数に対するLEAL
命令の生成を禁止するチェックがあったためです。
case ALEAL:
if(f != N && isconst(f, CTNIL))
fatal("gins LEAQ nil %T", f->type);
break;
コンパイラは、nil
ポインタのデリファレンスは実行時パニックを引き起こすべきであり、コンパイル時にLEAL nil
のような不正な命令を生成すべきではない、という設計思想を持っています。しかし、ODOTPTR
の処理ロジックがnil
ポインタ定数を適切に扱えていなかったため、このバグが発生していました。
修正は、ODOTPTR
の処理において、n->left
がODOT
やODOTPTR
(つまり、連鎖的なフィールドアクセス)である場合も、OCALLFUNC
などと同様にigen
で処理するように変更することで、nil
ポインタ定数がelse
ブロックに到達するのを防ぎました。これにより、nil
ポインタのデリファレンスは、コンパイル時に不正なLEAL
命令を生成するのではなく、実行時にパニックを引き起こす正しい動作をするようになります。
コアとなるコードの変更箇所
src/cmd/8g/cgen.c
--- a/src/cmd/8g/cgen.c
+++ b/src/cmd/8g/cgen.c
@@ -827,16 +827,19 @@ igen(Node *n, Node *a, Node *res)
return;
case ODOTPTR:
- if(n->left->addable
- || n->left->op == OCALLFUNC
- || n->left->op == OCALLMETH
- || n->left->op == OCALLINTER) {
+ switch(n->left->op) {
+ case ODOT:
+ case ODOTPTR:
+ case OCALLFUNC:
+ case OCALLMETH:
+ case OCALLINTER:
// igen-able nodes.
igen(n->left, &n1, res);
regalloc(a, types[tptr], &n1);
gmove(&n1, a);
regfree(&n1);
- } else {
+ break;
+ default:
regalloc(a, types[tptr], res);
cgen(n->left, a);
}
src/cmd/8g/gsubr.c
--- a/src/cmd/8g/gsubr.c
+++ b/src/cmd/8g/gsubr.c
@@ -1747,7 +1747,7 @@ gins(int as, Node *f, Node *t)
case ALEAL:
if(f != N && isconst(f, CTNIL))
- fatal("gins LEAQ nil %T", f->type);
+ fatal("gins LEAL nil %T", f->type);
break;
}
test/fixedbugs/issue4399.go
(新規ファイル)
// compile
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Issue 4399: 8g would print "gins LEAQ nil *A".
package main
type A struct{ a int }
func main() {
println(((*A)(nil)).a)
}
コアとなるコードの解説
src/cmd/8g/cgen.c
の変更
ODOTPTR
(ポインタのフィールドアクセス)を処理するigen
関数内のロジックが変更されました。
- 変更前:
n->left->addable
または特定の関数呼び出しノード(OCALLFUNC
,OCALLMETH
,OCALLINTER
)の場合にigen
を再帰的に呼び出していました。 - 変更後:
switch
文が導入され、n->left->op
がODOT
(構造体のフィールドアクセス)やODOTPTR
(ポインタのフィールドアクセス)である場合も、OCALLFUNC
などと同様にigen
を再帰的に呼び出すようになりました。それ以外のケースはdefault
で処理されます。
この変更の意図は、((*A)(nil)).a
のような式において、n->left
が ((*A)(nil))
であり、そのオペレーションがODOTPTR
(nil
ポインタのデリファレンス)である場合に、igen
が再帰的に呼び出されるようにすることです。これにより、nil
ポインタ定数に対するLEAL
命令の生成を試みるcgen
の呼び出しを回避し、コンパイラが不正なコードを生成するのを防ぎます。
src/cmd/8g/gsubr.c
の変更
gins
関数内のfatal
メッセージがLEAQ
からLEAL
に修正されました。これはタイプミスであり、機能的な変更ではありませんが、より正確なエラーメッセージになります。LEAQ
はLEAL
の別名のようなもので、文脈によっては同じ意味で使われることもありますが、ここではアセンブリ命令の正確な表記に合わせるための修正です。
test/fixedbugs/issue4399.go
の追加
このテストケースは、修正されたバグを再現し、修正が正しく機能することを確認するために追加されました。println(((*A)(nil)).a)
というコードは、nil
ポインタをデリファレンスしてフィールドにアクセスしようとするもので、このコミット以前はコンパイルエラーを引き起こしていました。修正後は、このコードはコンパイルが成功し、実行時にパニック(runtime error: invalid memory address or nil pointer dereference
)を引き起こすことが期待されます。これにより、コンパイラが不正な命令を生成するのではなく、Goのランタイムがnil
ポインタのデリファレンスを正しく検出するようになります。
関連リンク
- Go Issue 4399: https://code.google.com/p/go/issues/detail?id=4399 (古いGoogle Codeのリンクですが、GoのIssueトラッカーでこの番号を検索すると現在のGitHubのIssueにリダイレクトされるはずです)
- Go Code Review (CL): https://golang.org/cl/6845053
参考にした情報源リンク
- Go Issue 4399の議論
- Goコンパイラのソースコード(
src/cmd/8g/cgen.c
,src/cmd/8g/gsubr.c
) - x86アセンブリ命令
LEAL
に関する一般的な情報 - Go言語における
nil
ポインタの動作に関する情報