[インデックス 14823] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるuintptr(nil)
に関連する問題を修正するものです。具体的には、[]int(nil)[1:]
のようなnilスライスに対するスライス操作や、uintptr(unsafe.Pointer(nil))
のようなunsafe.Pointer(nil)
をuintptr
に変換する際に発生するコンパイラの誤った挙動を修正しています。この修正により、これらの操作が正しく処理され、コンパイラがクラッシュしたり、不正なコードを生成したりするのを防ぎます。
コミット
commit fba96e915dd16974745b199d09cfa6a4839cd03e
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Tue Jan 8 00:23:02 2013 +0100
cmd/gc: fix uintptr(nil) issues.
A constant node of type uintptr with a nil literal could
happen in two cases: []int(nil)[1:] and
uintptr(unsafe.Pointer(nil)).
Fixes #4614.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7059043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fba96e915dd16974745b199d09cfa6a4839cd03e
元コミット内容
cmd/gc: fix uintptr(nil) issues.
このコミットは、uintptr(nil)
に関連する問題を修正します。
uintptr
型の定数ノードがnilリテラルを持つケースは、[]int(nil)[1:]
とuintptr(unsafe.Pointer(nil))
の2つの場合に発生する可能性がありました。
Issue #4614を修正します。
変更の背景
この変更の背景には、Goコンパイラが特定の状況下でnil
値とuintptr
型の変換を誤って処理するというバグが存在していました。具体的には、以下の2つのシナリオで問題が発生していました。
- nilスライスに対するスライス操作:
[]int(nil)[1:]
のように、nil
であるスライスに対してスライス操作を行う場合。Go言語では、nil
スライスは長さ0、容量0として扱われますが、その内部表現やコンパイラが中間表現を生成する際に、nil
がuintptr
型の定数として誤って解釈されることがありました。 unsafe.Pointer(nil)
からuintptr
への変換:uintptr(unsafe.Pointer(nil))
のように、unsafe.Pointer(nil)
をuintptr
に明示的に型変換する場合。unsafe.Pointer
は任意の型のポインタを保持できる特殊な型であり、nil
は有効なunsafe.Pointer
値です。これをuintptr
(符号なし整数型でポインタ値を表現する型)に変換する際、コンパイラがnil
リテラルを正しくuintptr(0)
として扱えず、内部的なノード表現に問題が生じていました。
これらの問題は、コンパイラの内部処理においてnil
がuintptr
型の定数ノードとして扱われる際に、その値が正しく0
として初期化されない、あるいは型チェックや値の伝播が適切に行われないことに起因していました。結果として、コンパイルエラーや、場合によっては不正なバイナリが生成される可能性がありました。このバグはGo Issue #4614として報告され、このコミットで修正されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの概念に関する知識が必要です。
-
Goコンパイラ (
cmd/gc
):- Go言語の公式コンパイラは、主に
cmd/gc
というディレクトリに実装されています。これは、Goソースコードを機械語に変換する役割を担います。 - コンパイラは、ソースコードを抽象構文木(AST)にパースし、型チェック、最適化、コード生成といった複数のフェーズを経て実行可能ファイルを生成します。
const.c
やgen.c
といったファイルは、コンパイラのバックエンド部分、特に定数評価やコード生成に関連する処理を担っています。
- Go言語の公式コンパイラは、主に
-
uintptr
型:uintptr
はGo言語の組み込み型の一つで、ポインタ値を保持できる符号なし整数型です。- この型は、ポインタを整数として扱う必要がある低レベルな操作(例:
unsafe
パッケージの使用)で主に使用されます。 uintptr
はガベージコレクタの管理対象外であり、uintptr
に変換されたポインタはガベージコレクタによって追跡されません。そのため、unsafe
パッケージと組み合わせて慎重に使用する必要があります。
-
unsafe.Pointer
型:unsafe.Pointer
は、Go言語の型システムをバイパスして任意の型のポインタを保持できる特殊なポインタ型です。- これは、
*T
(任意の型T
へのポインタ)とuintptr
の間で相互に変換できる唯一のポインタ型です。 unsafe.Pointer(nil)
は、nil
値を持つunsafe.Pointer
であり、これは有効な値です。
-
nil
とスライス:- Go言語において、
nil
はポインタ、スライス、マップ、チャネル、インターフェースなどのゼロ値を表します。 nil
スライスは、基底配列を持たず、長さも容量も0のスライスです。var s []int
のように宣言されたスライスは初期値としてnil
になります。nil
スライスに対してスライス操作(例:s[low:high]
)を行うことは合法であり、結果は常にnil
スライスになります。しかし、この操作がコンパイラの内部でuintptr(nil)
のような中間表現を生成する際に問題が発生していました。
- Go言語において、
-
コンパイラのノード表現:
- コンパイラは、ソースコードの各要素(変数、定数、式など)を内部的に「ノード」として表現します。
- これらのノードは、型情報、値、その他の属性を持ち、コンパイルの各フェーズで処理されます。
nil
リテラルも、コンパイラ内部では特定のノードとして表現されます。このコミットは、nil
リテラルがuintptr
型に変換される際のノードの処理に焦点を当てています。
-
mal
関数とmpmovecfix
関数:mal
は、Goコンパイラの内部でメモリを割り当てるための関数です。mpmovecfix
は、多倍長整数(mpint
)を扱うための関数で、ここでは定数0
をn->val.u.xval
に設定するために使用されています。xval
は定数ノードの値を保持するフィールドです。
技術的詳細
このコミットは、Goコンパイラのsrc/cmd/gc/const.c
とsrc/cmd/gc/gen.c
の2つのファイルに修正を加えています。
src/cmd/gc/const.c
の変更点
const.c
は、コンパイラが定数を処理する部分です。特に、convlit1
関数はリテラル(定数)を特定の型に変換する際のロジックを含んでいます。
変更前は、nil
リテラルがuintptr
型に変換される際に、その値が正しく0
として初期化されないケースがありました。このコミットでは、TUINTPTR
(uintptr
型)への変換処理に以下のロジックが追加されました。
case TUINTPTR:
// A nil literal may be converted to uintptr
// if it is an unsafe.Pointer
if(n->type->etype == TUNSAFEPTR) {
n->val.u.xval = mal(sizeof(*n->val.u.xval));
mpmovecfix(n->val.u.xval, 0);
n->val.ctype = CTINT;
} else
goto bad;
}
break;
このコードは、nil
リテラルがuintptr
型に変換される際に、元の型がTUNSAFEPTR
(unsafe.Pointer
型)であるかどうかをチェックします。もしそうであれば、新しく割り当てられたメモリ(mal
)に0
をセットし(mpmovecfix
)、定数型をCTINT
(整数定数)に設定します。これにより、uintptr(unsafe.Pointer(nil))
のようなケースで、uintptr
型の定数ノードが確実に0
という値を持つようにします。unsafe.Pointer(nil)
は概念的に0
アドレスを指すポインタと等価であるため、uintptr(0)
に変換されるのが正しい挙動です。
src/cmd/gc/gen.c
の変更点
gen.c
は、コンパイラがコードを生成する部分です。cgen_slice
関数はスライス操作のコード生成を担当しています。
変更前は、[]int(nil)[1:]
のようなnilスライスに対するスライス操作において、スライスの基底ポインタを表すノード(n->left
)がnil
である場合に、そのnil
が正しく処理されず、コンパイラがクラッシュする可能性がありました。
if(isnil(n->left)) {
tempname(&src, n->left->type);
cgen(n->left, &src);
} else
src = *n->left;
この修正では、スライス操作の対象(n->left
)がnil
であるかどうかをisnil
関数でチェックします。もしnil
であれば、一時的な名前(tempname
)を割り当て、そのnil
ノードからコードを生成します(cgen
)。これにより、nil
スライスに対するスライス操作が、コンパイラの内部でnil
ポインタを正しく0
として扱い、クラッシュを回避できるようになります。具体的には、nil
スライスの基底ポインタは0
であるべきであり、その0
をuintptr
として扱う際に問題が生じていたため、この修正でその0
が正しく伝播されるようにしています。
test/fixedbugs/issue4614.go
の追加
このコミットでは、上記の修正が正しく機能することを確認するための新しいテストファイルtest/fixedbugs/issue4614.go
が追加されています。このテストファイルには、問題となっていた以下の2つのケースが含まれています。
var _ = []int(nil)[1:]
var _ = []int(nil)[n:]
var _ = uintptr(unsafe.Pointer(nil))
var _ = unsafe.Pointer(uintptr(0))
これらの行は、コンパイル時にエラーが発生しないことを確認するためのものです。テストの存在は、バグが修正され、将来のリグレッションを防ぐための重要な要素です。
コアとなるコードの変更箇所
src/cmd/gc/const.c
--- a/src/cmd/gc/const.c
+++ b/src/cmd/gc/const.c
@@ -162,6 +162,16 @@ convlit1(Node **np, Type *t, int explicit)\n \tcase TFUNC:\n \t\tcase TUNSAFEPTR:\n \t\t\tbreak;\n+\n+\t\tcase TUINTPTR:\n+\t\t\t// A nil literal may be converted to uintptr\n+\t\t\t// if it is an unsafe.Pointer\n+\t\t\tif(n->type->etype == TUNSAFEPTR) {\n+\t\t\t\tn->val.u.xval = mal(sizeof(*n->val.u.xval));\n+\t\t\t\tmpmovecfix(n->val.u.xval, 0);\n+\t\t\t\tn->val.ctype = CTINT;\n+\t\t\t} else\n+\t\t\t\tgoto bad;\n \t}\n \tbreak;\
src/cmd/gc/gen.c
--- a/src/cmd/gc/gen.c
+++ b/src/cmd/gc/gen.c
@@ -810,7 +810,11 @@ cgen_slice(Node *n, Node *res)\n \tcheckref(n->left);\n }\n \n-\tsrc = *n->left;\n+\tif(isnil(n->left)) {\n+\t\ttempname(&src, n->left->type);\n+\t\tcgen(n->left, &src);\n+\t} else\n+\t\tsrc = *n->left;\n \tsrc.xoffset += Array_array;\
コアとなるコードの解説
src/cmd/gc/const.c
の変更
この変更は、convlit1
関数内でnil
リテラルがuintptr
型に変換される際の挙動を修正しています。
以前は、uintptr(unsafe.Pointer(nil))
のような式がコンパイラによって正しくuintptr(0)
として扱われない可能性がありました。これは、nil
リテラルがuintptr
型のノードとして表現される際に、その値が0
に初期化されないためです。
追加されたコードブロックは、TUINTPTR
(uintptr
型)への変換パスに特化しています。
if(n->type->etype == TUNSAFEPTR)
の条件は、変換元のリテラルがunsafe.Pointer
型であるnil
であることを確認しています。
この条件が真の場合、以下の処理が行われます。
n->val.u.xval = mal(sizeof(*n->val.u.xval));
:n->val.u.xval
は、定数ノードの値を保持するためのメモリ領域を指します。mal
関数を使って、この値のためのメモリを新しく割り当てます。mpmovecfix(n->val.u.xval, 0);
: 割り当てられたメモリに、多倍長整数として0
を書き込みます。これにより、uintptr(nil)
が確実にuintptr(0)
として表現されるようになります。n->val.ctype = CTINT;
: ノードの定数型をCTINT
(整数定数)に設定します。
この修正により、uintptr(unsafe.Pointer(nil))
のような式がコンパイル時に正しく0
という値を持つuintptr
定数として扱われるようになり、コンパイラの内部的な整合性が保たれます。
src/cmd/gc/gen.c
の変更
この変更は、cgen_slice
関数内でスライス操作の対象がnil
である場合のコード生成を修正しています。
以前は、[]int(nil)[1:]
のようなnilスライスに対するスライス操作が、コンパイラの内部でnil
ポインタを正しく扱えず、クラッシュを引き起こす可能性がありました。
変更前のコードでは、スライスの基底ポインタを表すノード(n->left
)を直接src
にコピーしていました。しかし、n->left
がnil
ノードである場合、そのノードが持つべき情報(例えば、0
という値)が適切に伝播されないことがありました。
追加されたif(isnil(n->left))
ブロックは、n->left
がnil
ノードであるかどうかをチェックします。
- もし
n->left
がnil
であれば、tempname(&src, n->left->type);
で一時的な変数名をsrc
に割り当て、cgen(n->left, &src);
でnil
ノードからコードを生成します。このcgen
呼び出しにより、nil
ノードが持つべき0
という値がsrc
に正しく設定されるようになります。 n->left
がnil
でなければ、以前と同様にsrc = *n->left;
でノードをコピーします。
この修正により、nil
スライスに対するスライス操作が、コンパイラの内部でnil
ポインタを正しく0
として扱い、その0
がuintptr
として後続の処理に伝播されるようになります。これにより、コンパイラのクラッシュが回避され、正しいコードが生成されるようになります。
関連リンク
- Go Issue #4614: https://github.com/golang/go/issues/4614 (Web検索結果から推測されるリンク)
- Go CL 7059043: https://golang.org/cl/7059043 (コミットメッセージに記載)
参考にした情報源リンク
- コミットメッセージと差分 (
./commit_data/14823.txt
) - Go Issue #4614に関するWeb検索結果
- Go言語の公式ドキュメント(
uintptr
,unsafe.Pointer
, スライスに関する一般的な知識) - Goコンパイラのソースコード(
src/cmd/gc/const.c
,src/cmd/gc/gen.c
の周辺コード) - Go言語のコンパイラに関する一般的な知識(AST、型チェック、コード生成など)