[インデックス 13906] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるnil
インターフェースおよびnil
スライスの誤った使用に関するバグ修正です。具体的には、nil
定数として扱われるインターフェースやスライスが、コンパイラによって不正なコード生成を引き起こす問題に対処しています。この修正により、nil
インターフェースやnil
スライスが使用された際に、適切にパニックを発生させるようなコードが生成されるようになります。
コミット
commit 05ac300830ab89c46a259b3bb9d57a4a5a080a15
Author: Russ Cox <rsc@golang.org>
Date: Sat Sep 22 20:42:11 2012 -0400
cmd/gc: fix use of nil interface, slice
Fixes #3670.
R=ken2
CC=golang-dev
https://golang.org/cl/6542058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/05ac300830ab89c46a259b3bb9d57a4a5a080a15
元コミット内容
このコミットは、Goコンパイラがnil
インターフェースやnil
スライスを誤って処理し、結果として不正なコードを生成するバグを修正します。この問題は、特にreflect.TypeOf(T(nil))
のような操作や、[]byte(nil)[0]
のようにnil
スライスにアクセスしようとした場合に顕在化していました。コンパイラはこれらのnil
値をアドレス可能(addressable)なものとして扱おうとし、load-effective-address
命令(LEAQ
やALEAL
)を不適切に生成していました。
変更の背景
Go言語では、nil
は特定の型のゼロ値を表します。インターフェース型やスライス型もnil
になり得ます。しかし、コンパイラがこれらのnil
値を処理する際に、その内部表現やメモリレイアウトに関する誤解釈が生じることがありました。特に、nil
インターフェースやnil
スライスに対してアドレスを取得しようとするような状況で、コンパイラがクラッシュしたり、予期せぬ動作を引き起こすコードを生成したりするバグが存在していました。
この問題は、Goの内部バグトラッカーで「Issue 3670」として報告されていました。このコミットは、そのIssue 3670を修正することを目的としています。nil
インターフェースやnil
スライスへのアクセスは、通常、実行時パニック(runtime panic)を引き起こすべき動作です。しかし、バグのあるコンパイラは、パニックを発生させる代わりに、不正なメモリ参照を行うコードを生成してしまう可能性がありました。
test/fixedbugs/bug444.go
というテストファイルがこのバグを再現するために追加されており、reflect.TypeOf(T(nil))
が以前はコンパイルエラーまたは誤った動作を引き起こしていたこと、そして[]byte(nil)[0]
のようなnil
スライスへのアクセスが誤ったコンパイルを招いていたことが示されています。
前提知識の解説
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。5g
,6g
,8g
はそれぞれ、Goがサポートしていた異なるアーキテクチャ(5g
はARM、6g
はx86-64、8g
はx86-32)向けのコンパイラバックエンドを指します。 nil
: Goにおけるゼロ値の一つで、ポインタ、インターフェース、スライス、マップ、チャネル、関数などの参照型が何も指していない状態を表します。- インターフェース (Interface): Goのインターフェースは、メソッドのシグネチャの集合を定義します。インターフェース型の変数は、そのインターフェースが定義するすべてのメソッドを実装する任意の型の値を保持できます。
nil
インターフェースは、基底となる値も型も持たない状態です。 - スライス (Slice): Goのスライスは、配列の一部を参照する動的なデータ構造です。内部的には、ポインタ、長さ、容量の3つの要素で構成されます。
nil
スライスは、これらの要素がすべてゼロ値である状態です。 isconst(n, CTNIL)
: コンパイラ内部の関数で、与えられたノードn
がnil
定数であるかどうかを判定します。CTNIL
はnil
定数を表すフラグです。widthptr
: ポインタのサイズ(バイト単位)を表す定数。通常、32ビットシステムでは4バイト、64ビットシステムでは8バイトです。agen
関数: コンパイラのコード生成フェーズで使用される関数の一つで、ノードn
のアドレスを計算し、結果をres
に格納します。gins
関数: アセンブリ命令を生成する関数。ALEAQ
(Load Effective Address Quadword) やALEAL
(Load Effective Address Longword) は、メモリのアドレスをレジスタにロードする命令です。これらは通常、ポインタ演算や構造体フィールドへのアクセスなどで使用されます。tempname
: 一時変数を生成するコンパイラ内部の関数。clearfat
: 構造体やスライスなどの「fat」なデータ構造(ポインタだけでなく、長さや容量などの追加情報を持つもの)をゼロクリアする関数。regalloc
/regfree
: レジスタの割り当てと解放を行う関数。- パニック (Panic): Goのランタイムエラーメカニズム。通常、プログラムの回復不可能なエラーが発生した場合に、現在のゴルーチンを停止させ、スタックをアンワインドします。
nil
スライスへのインデックスアクセスなど、不正な操作はパニックを引き起こします。
技術的詳細
このコミットの核心は、Goコンパイラのコード生成部分、特にagen
関数におけるnil
インターフェースおよびnil
スライスの特殊なハンドリングにあります。
以前のコンパイラでは、nil
インターフェースやnil
スライスがagen
関数に渡された際、それらがアドレス可能であるかのように扱われ、ALEAQ
やALEAL
といったアドレスをロードする命令が生成されていました。しかし、nil
インターフェースやnil
スライスは、有効なメモリ領域を指していないため、そのアドレスを取得しようとすると問題が発生します。特に、これらの型はwidthptr
(ポインタのサイズ)よりも大きいサイズを持つことがあり、その場合、単なるポインタとして扱われると情報が失われたり、不正なメモリ参照を引き起こしたりする可能性がありました。
この修正では、agen
関数に以下のロジックが追加されました。
-
isconst(n, CTNIL) && n->type->width > widthptr
のチェック:isconst(n, CTNIL)
: 現在処理しているノードn
がnil
定数であるかを確認します。n->type->width > widthptr
: そのnil
定数の型が、ポインタのサイズ(widthptr
)よりも大きいサイズを持つかを確認します。これは、インターフェース(型情報と値ポインタを持つ)やスライス(ポインタ、長さ、容量を持つ)のような「fat」な型がnil
である場合に該当します。- この条件が真の場合、それは
nil
インターフェースまたはnil
スライスの使用を意味します。
-
一時変数の生成とアドレスの取得:
tempname(&n1, n->type)
:nil
インターフェース/スライスと同じ型を持つ一時変数n1
を生成します。clearfat(&n1)
: その一時変数をゼロクリアします。これにより、nil
の状態が表現されます。regalloc(&n2, types[tptr], res)
: ポインタ型の一時レジスタn2
を割り当てます。gins(ALEAQ, &n1, &n2)
(またはALEAL
for 8g): 生成した一時変数n1
のアドレスをレジスタn2
にロードするアセンブリ命令を生成します。gmove(&n2, res)
:n2
の内容を結果ノードres
に移動します。regfree(&n2)
: 一時レジスタn2
を解放します。goto ret;
(またはreturn;
for 8g): 処理を終了します。
この変更の意図は、「nil
インターフェースやnil
スライスが使用された場合、生成されるコードは単にパニックを引き起こすだけでよい」というものです。コンパイラは、これらの不正な使用に対して効率的なコードを生成する必要はなく、むしろ実行時に確実にパニックを発生させるようにすべきです。一時変数を作成し、そのアドレスを取得するこの方法は、実際にnil
値がメモリ上に存在するかのように見せかけ、その後の不正なアクセスがランタイムパニックを引き起こすことを保証します。
また、gsubr.c
内のgins
関数にも変更が加えられています。ALEAQ
(またはALEAL
)命令を生成する際に、ソースオペランドf
がnil
定数である場合、fatal
エラーを発生させるチェックが追加されました。これは、コンパイラがnil
定数に対して直接LEAQ
命令を生成しようとするような、より根本的な誤りを防ぐための防御的なチェックです。
コアとなるコードの変更箇所
主に以下のファイルが変更されています。
src/cmd/5g/cgen.c
src/cmd/6g/cgen.c
src/cmd/8g/cgen.c
- これらのファイルは、それぞれARM、x86-64、x86-32アーキテクチャ向けのGoコンパイラのコード生成部分(
agen
関数)です。agen
関数内に、nil
インターフェース/スライスの特殊なハンドリングロジックが追加されています。
- これらのファイルは、それぞれARM、x86-64、x86-32アーキテクチャ向けのGoコンパイラのコード生成部分(
src/cmd/6g/gsubr.c
src/cmd/8g/gsubr.c
- これらのファイルは、それぞれx86-64、x86-32アーキテクチャ向けのGoコンパイラの共通サブルーチンです。
gins
関数内に、ALEAQ
/ALEAL
命令がnil
定数に対して生成されようとした場合のfatal
エラーチェックが追加されています。
- これらのファイルは、それぞれx86-64、x86-32アーキテクチャ向けのGoコンパイラの共通サブルーチンです。
test/fixedbugs/bug444.go
- このファイルは、修正されたバグを再現し、修正が正しく機能することを確認するためのテストケースです。
コアとなるコードの解説
src/cmd/{5,6,8}g/cgen.c
の変更点
// ... (既存のコード) ...
if(isconst(n, CTNIL) && n->type->width > widthptr) {
// Use of a nil interface or nil slice.
// Create a temporary we can take the address of and read.
// The generated code is just going to panic, so it need not
// be terribly efficient. See issue 3670.
tempname(&n1, n->type);
clearfat(&n1);
regalloc(&n2, types[tptr], res);
gins(ALEAQ, &n1, &n2); // 8gではALEAL
gmove(&n2, res);
regfree(&n2);
goto ret; // 8gではreturn;
}
// ... (既存のコード) ...
このコードブロックは、agen
関数内で、ノードn
がnil
定数であり、かつその型がポインタサイズ(widthptr
)よりも大きい場合に実行されます。これは、nil
インターフェースやnil
スライスを処理していることを意味します。
tempname(&n1, n->type);
:n
と同じ型を持つ一時変数n1
を宣言します。clearfat(&n1);
:n1
のメモリ領域をゼロで埋めます。これにより、nil
インターフェースやnil
スライスの内部表現(例えば、インターフェースの型情報や値ポインタ、スライスのポインタ、長さ、容量など)がすべてゼロに設定されます。regalloc(&n2, types[tptr], res);
: ポインタを保持するための一時レジスタn2
を割り当てます。gins(ALEAQ, &n1, &n2);
(またはALEAL
for 8g):n1
のアドレスをn2
にロードするアセンブリ命令を生成します。これにより、nil
インターフェース/スライスがメモリ上のどこかに存在するかのように見せかけます。gmove(&n2, res);
:n2
の内容(n1
のアドレス)を結果ノードres
に移動します。regfree(&n2);
:n2
を解放します。goto ret;
(またはreturn;
for 8g):agen
関数の処理を終了します。
このロジックにより、nil
インターフェースやnil
スライスへのアクセスは、有効なアドレスを指さない一時変数へのアクセスとしてコンパイルされ、結果として実行時にパニックを引き起こすことが保証されます。
src/cmd/{6,8}g/gsubr.c
の変更点
// ... (既存のコード) ...
case ALEAQ: // 8gではALEAL
if(f != N && isconst(f, CTNIL)) {
fatal("gins LEAQ nil %T", f->type);
}
break;
// ... (既存のコード) ...
この変更は、gins
関数内でALEAQ
(またはALEAL
)命令を生成する際の防御的なチェックです。
if(f != N && isconst(f, CTNIL))
: 命令のソースオペランドf
がnil
定数であるかどうかをチェックします。fatal("gins LEAQ nil %T", f->type);
: もしf
がnil
定数であれば、コンパイラは致命的なエラーを発生させます。これは、nil
定数に対して直接アドレスをロードしようとするのは、コンパイラの内部ロジックに誤りがあることを示唆するためです。このチェックは、agen
関数での修正とは異なる、より低レベルでの誤ったコード生成を防ぐためのものです。
test/fixedbugs/bug444.go
の変更点
// ... (既存のコード) ...
// The no-op conversion here used to confuse the compiler
// into doing a load-effective-address of nil.
// See issue 3670.
package main
import "reflect"
type T interface {}
var x bool
func main() {
reflect.TypeOf(nil)
reflect.TypeOf(T(nil)) // used to miscompile
shouldPanic()
}
func f() byte {
return []byte(nil)[0] // used to miscompile
}
func shouldPanic() {
defer func() {
if recover() == nil {
panic("not panicking")
}
}()
f()
}
このテストファイルは、以下の2つの主要なケースをテストしています。
reflect.TypeOf(T(nil))
:nil
インターフェースの型情報を取得しようとするケース。以前はこれが誤ってコンパイルされていました。[]byte(nil)[0]
:nil
バイトスライスにインデックス0でアクセスしようとするケース。これは実行時にパニックを引き起こすべきですが、以前は誤ってコンパイルされていました。
shouldPanic
関数は、f()
がパニックを引き起こすことを検証するためのヘルパー関数です。f()
がパニックしない場合、shouldPanic
自体がパニックします。これにより、[]byte(nil)[0]
が正しくパニックを引き起こすようになったことを確認できます。
関連リンク
- Go言語のインターフェース: https://go.dev/tour/methods/10
- Go言語のスライス: https://go.dev/tour/moretypes/7
- Go言語のパニックとリカバリー: https://go.dev/blog/defer-panic-and-recover
参考にした情報源リンク
- コミットハッシュ:
05ac300830ab89c46a259b3bb9d57a4a5a080a15
- Goのコードレビューシステム (Gerrit) の変更リスト:
https://golang.org/cl/6542058
(現在はGitHubに移行しているため、直接アクセスできない可能性がありますが、コミットメッセージに記載されています) - Go言語のソースコード (GitHub): https://github.com/golang/go
- Go言語のIssue Tracker (GitHub): https://github.com/golang/go/issues (Issue 3670は古いIssueであり、直接リンクを見つけるのは難しい場合がありますが、このコミットが修正した問題の性質を理解する上で重要です。)