Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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つのシナリオで問題が発生していました。

  1. nilスライスに対するスライス操作: []int(nil)[1:]のように、nilであるスライスに対してスライス操作を行う場合。Go言語では、nilスライスは長さ0、容量0として扱われますが、その内部表現やコンパイラが中間表現を生成する際に、niluintptr型の定数として誤って解釈されることがありました。
  2. unsafe.Pointer(nil)からuintptrへの変換: uintptr(unsafe.Pointer(nil))のように、unsafe.Pointer(nil)uintptrに明示的に型変換する場合。unsafe.Pointerは任意の型のポインタを保持できる特殊な型であり、nilは有効なunsafe.Pointer値です。これをuintptr(符号なし整数型でポインタ値を表現する型)に変換する際、コンパイラがnilリテラルを正しくuintptr(0)として扱えず、内部的なノード表現に問題が生じていました。

これらの問題は、コンパイラの内部処理においてniluintptr型の定数ノードとして扱われる際に、その値が正しく0として初期化されない、あるいは型チェックや値の伝播が適切に行われないことに起因していました。結果として、コンパイルエラーや、場合によっては不正なバイナリが生成される可能性がありました。このバグはGo Issue #4614として報告され、このコミットで修正されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラの概念に関する知識が必要です。

  1. Goコンパイラ (cmd/gc):

    • Go言語の公式コンパイラは、主にcmd/gcというディレクトリに実装されています。これは、Goソースコードを機械語に変換する役割を担います。
    • コンパイラは、ソースコードを抽象構文木(AST)にパースし、型チェック、最適化、コード生成といった複数のフェーズを経て実行可能ファイルを生成します。
    • const.cgen.cといったファイルは、コンパイラのバックエンド部分、特に定数評価やコード生成に関連する処理を担っています。
  2. uintptr:

    • uintptrはGo言語の組み込み型の一つで、ポインタ値を保持できる符号なし整数型です。
    • この型は、ポインタを整数として扱う必要がある低レベルな操作(例: unsafeパッケージの使用)で主に使用されます。
    • uintptrはガベージコレクタの管理対象外であり、uintptrに変換されたポインタはガベージコレクタによって追跡されません。そのため、unsafeパッケージと組み合わせて慎重に使用する必要があります。
  3. unsafe.Pointer:

    • unsafe.Pointerは、Go言語の型システムをバイパスして任意の型のポインタを保持できる特殊なポインタ型です。
    • これは、*T(任意の型Tへのポインタ)とuintptrの間で相互に変換できる唯一のポインタ型です。
    • unsafe.Pointer(nil)は、nil値を持つunsafe.Pointerであり、これは有効な値です。
  4. nilとスライス:

    • Go言語において、nilはポインタ、スライス、マップ、チャネル、インターフェースなどのゼロ値を表します。
    • nilスライスは、基底配列を持たず、長さも容量も0のスライスです。var s []intのように宣言されたスライスは初期値としてnilになります。
    • nilスライスに対してスライス操作(例: s[low:high])を行うことは合法であり、結果は常にnilスライスになります。しかし、この操作がコンパイラの内部でuintptr(nil)のような中間表現を生成する際に問題が発生していました。
  5. コンパイラのノード表現:

    • コンパイラは、ソースコードの各要素(変数、定数、式など)を内部的に「ノード」として表現します。
    • これらのノードは、型情報、値、その他の属性を持ち、コンパイルの各フェーズで処理されます。
    • nilリテラルも、コンパイラ内部では特定のノードとして表現されます。このコミットは、nilリテラルがuintptr型に変換される際のノードの処理に焦点を当てています。
  6. mal関数とmpmovecfix関数:

    • malは、Goコンパイラの内部でメモリを割り当てるための関数です。
    • mpmovecfixは、多倍長整数(mpint)を扱うための関数で、ここでは定数0n->val.u.xvalに設定するために使用されています。xvalは定数ノードの値を保持するフィールドです。

技術的詳細

このコミットは、Goコンパイラのsrc/cmd/gc/const.csrc/cmd/gc/gen.cの2つのファイルに修正を加えています。

src/cmd/gc/const.cの変更点

const.cは、コンパイラが定数を処理する部分です。特に、convlit1関数はリテラル(定数)を特定の型に変換する際のロジックを含んでいます。

変更前は、nilリテラルがuintptr型に変換される際に、その値が正しく0として初期化されないケースがありました。このコミットでは、TUINTPTRuintptr型)への変換処理に以下のロジックが追加されました。

		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型に変換される際に、元の型がTUNSAFEPTRunsafe.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であるべきであり、その0uintptrとして扱う際に問題が生じていたため、この修正でその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に初期化されないためです。

追加されたコードブロックは、TUINTPTRuintptr型)への変換パスに特化しています。 if(n->type->etype == TUNSAFEPTR)の条件は、変換元のリテラルがunsafe.Pointer型であるnilであることを確認しています。 この条件が真の場合、以下の処理が行われます。

  1. n->val.u.xval = mal(sizeof(*n->val.u.xval));: n->val.u.xvalは、定数ノードの値を保持するためのメモリ領域を指します。mal関数を使って、この値のためのメモリを新しく割り当てます。
  2. mpmovecfix(n->val.u.xval, 0);: 割り当てられたメモリに、多倍長整数として0を書き込みます。これにより、uintptr(nil)が確実にuintptr(0)として表現されるようになります。
  3. 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->leftnilノードである場合、そのノードが持つべき情報(例えば、0という値)が適切に伝播されないことがありました。

追加されたif(isnil(n->left))ブロックは、n->leftnilノードであるかどうかをチェックします。

  • もしn->leftnilであれば、tempname(&src, n->left->type);で一時的な変数名をsrcに割り当て、cgen(n->left, &src);nilノードからコードを生成します。このcgen呼び出しにより、nilノードが持つべき0という値がsrcに正しく設定されるようになります。
  • n->leftnilでなければ、以前と同様にsrc = *n->left;でノードをコピーします。

この修正により、nilスライスに対するスライス操作が、コンパイラの内部でnilポインタを正しく0として扱い、その0uintptrとして後続の処理に伝播されるようになります。これにより、コンパイラのクラッシュが回避され、正しいコードが生成されるようになります。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分 (./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、型チェック、コード生成など)