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

[インデックス 10225] ファイルの概要

コミット

このコミットは、Goコンパイラ(gc)におけるバグ修正を目的としています。具体的には、メソッドではない要素が誤ってxmethodリストに混入するのを防ぐための変更です。これにより、コンパイラの内部処理の堅牢性が向上し、予期せぬ動作やクラッシュを防ぎます。

  • コミットハッシュ: 11075ed893193a415d6b16cd28f06ad4bcc49092
  • 作者: Luuk van Dijk lvd@golang.org
  • 日付: 2011年11月3日 木曜日 17:51:15 +0100

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/11075ed893193a415d6b16cd28f06ad4bcc49092

元コミット内容

gc: Don't pollute the xmethod list with non-methods.

Fixes #2355.

I have a test, but not sure if it's worth adding. Instead i've made
the patching-over in reflect.c methods more fatal and more descriptive.

R=rsc
CC=golang-dev
https://golang.org/cl/5302082

変更の背景

このコミットは、Goコンパイラが型に紐付けられたメソッドを処理する際に発生していた問題に対処しています。Go言語では、構造体(struct)にメソッドを定義できますが、フィールドとして関数型の変数を定義することも可能です。このコミット以前のコンパイラでは、構造体のフィールドとして定義された関数が、誤ってその構造体の「メソッド」として内部リスト(xmethodリスト)に登録されてしまう可能性がありました。

このような誤った登録は、コンパイラが型のメソッド情報を取得する際に、本来メソッドではない要素をメソッドとして扱おうとすることで、予期せぬエラーやコンパイラのクラッシュを引き起こす原因となっていました。特に、リフレクション(reflectパッケージ)のような、実行時に型情報を動的に扱う機能において、この問題が顕在化する可能性がありました。

コミットメッセージにある「Fixes #2355」は、Goの内部バグトラッカーにおける特定の課題番号を指しています。この課題は、まさにこの「非メソッドがメソッドリストに混入する」問題とその影響を報告していたものと考えられます。

前提知識の解説

このコミットの理解には、以下のGo言語およびコンパイラの基本的な概念が役立ちます。

  • Go言語のメソッド: Go言語において、メソッドは特定の型に関連付けられた関数です。レシーバ引数(func (r ReceiverType) MethodName(...)r ReceiverType の部分)を持つことで、その型の値やポインタに対して呼び出すことができます。
  • 構造体(struct: 複数のフィールドをまとめた複合型です。構造体はフィールドだけでなく、メソッドを持つこともできます。
  • 埋め込み(Embedding): Go言語の構造体は、他の構造体やインターフェースを匿名フィールドとして埋め込むことができます。これにより、埋め込まれた型のメソッドやフィールドを、埋め込んだ構造体が直接持っているかのようにアクセスできます。この機能は、コードの再利用や「is-a」関係の表現に利用されます。
  • Goコンパイラ(gc: Go言語の公式コンパイラです。ソースコードを機械語に変換する過程で、型の情報、メソッド情報、シンボルテーブルなどを内部的に管理します。
  • xmethodリスト: Goコンパイラの内部で、特定の型が持つメソッドの情報を管理するためのリストです。コンパイラは、このリストを参照して、メソッド呼び出しの解決やリフレクション情報の生成を行います。
  • TFUNC: Goコンパイラの内部で、関数型を表すための型定数です。
  • TFIELD: Goコンパイラの内部で、構造体のフィールドを表すための型定数です。
  • thistuple: Goコンパイラの内部で、関数のレシーバ引数の数を表すためのフィールドです。メソッドの場合、レシーバが存在するため、この値は0より大きくなります。通常の関数では0です。
  • fatal関数: Goコンパイラの内部で、回復不可能なエラーが発生した場合にプログラムを終了させるための関数です。デバッグやコンパイラの堅牢性確保のために使用されます。

技術的詳細

このコミットの主要な変更点は、Goコンパイラのsrc/cmd/gc/reflect.csrc/cmd/gc/subr.cファイルにあります。これらのファイルは、それぞれリフレクション情報の生成と、シンボルおよび型の処理を担当しています。

src/cmd/gc/reflect.cの変更

methods関数は、特定の型(Type *t)が持つメソッドのリストを生成する役割を担っています。変更前は、xmethodリストを走査する際に、f->type->etype != TFUNCという条件で関数型でないものをスキップしていました。しかし、これだけでは不十分でした。なぜなら、構造体のフィールドとして定義された関数型の変数もTFUNCであるため、誤ってメソッドとして扱われる可能性があったからです。

このコミットでは、以下のチェックが追加されました。

  1. if(f->etype != TFIELD): fがフィールドでない場合にfatalエラーを発生させます。これは、xmethodリストに登録されるべき要素はフィールド(つまり、型に紐付けられたシンボル)であるべきだという前提を強化しています。
  2. if (f->type->etype != TFUNC || f->type->thistuple == 0):
    • f->type->etype != TFUNC: fの型が関数型でない場合にfatalエラーを発生させます。これは既存のチェックをより厳密にしたものです。
    • f->type->thistuple == 0: fの型が関数型でありながら、レシーバ引数(thistupleが0)を持たない場合にfatalエラーを発生させます。これが最も重要な変更点であり、通常の関数とメソッドを区別するための決定的な条件です。メソッドは必ずレシーバを持つため、thistupleは0より大きくなります。
  3. if (!getthisx(f->type)->type): レシーバの型が存在しない場合にfatalエラーを発生させます。これは、メソッドとして扱われるべき要素が正しくレシーバ情報を持っていることを保証するための追加チェックです。

これらの変更により、methods関数はxmethodリストを処理する際に、より厳密なチェックを行い、非メソッドが誤ってリストに混入した場合に即座にエラーを検出してコンパイラを停止させるようになりました。これにより、コンパイラの内部状態の整合性が保たれ、後続の処理での問題発生を防ぎます。

src/cmd/gc/subr.cの変更

expandmeth関数は、型のメソッドを「展開」する、つまり、埋め込み型などによって継承されるメソッドを処理する役割を担っています。この関数内で、addot1という内部関数が任意のフィールドを掘り出す可能性があり、その結果、メソッドではないフィールドが誤ってメソッドとして扱われる可能性がありました。

変更前は、if(c == 1)cは一致するフィールドの数)の場合にsl->good = 1; sl->field = f;として、見つかったフィールドを無条件にメソッドとして登録していました。

このコミットでは、以下の条件が追加されました。

  • if(f->type->etype == TFUNC && f->type->thistuple > 0):
    • f->type->etype == TFUNC: 見つかったフィールドが関数型であること。
    • f->type->thistuple > 0: その関数型がレシーバ引数を持つこと。

この条件を満たす場合にのみ、sl->good = 1; sl->field = f;としてフィールドをメソッドとして登録するように変更されました。これにより、expandmeth関数がメソッドを処理する際に、非メソッドのフィールドが誤ってメソッドとして登録されることを防ぎます。

test/fixedbugs/bug372.goの追加

このコミットでは、test/fixedbugs/bug372.goという新しいテストファイルが追加されました。このテストは、まさにこのバグを再現し、修正が正しく適用されたことを検証するためのものです。

package main

type T struct {}
func (T) m() string { return "T" } // T型のメソッドm

type TT struct {
	T // Tを埋め込み
	m func() string // mという名前の関数型フィールド
}

func ff() string { return "ff" }

func main() {
	var tt TT
	tt.m = ff // TTのフィールドmにff関数を代入

	if tt.m() != "ff" {
		println(tt.m(), "!= \"ff\"")
	}
}

このテストケースのポイントは、TT構造体がT型を埋め込んでいるため、Tのメソッドm()を継承しているように見える点と、同時にmという名前の関数型フィールドを持っている点です。バグが存在するバージョンでは、TTのフィールドmが、Tのメソッドmと混同され、xmethodリストに誤って登録される可能性がありました。

このテストでは、tt.m = ffとしてTTのフィールドmに別の関数ffを代入し、その後にtt.m()を呼び出しています。もしコンパイラがTTのフィールドmを正しく認識せず、Tのメソッドmと混同していた場合、tt.m()の呼び出し結果が期待通りにならないか、コンパイルエラーが発生する可能性がありました。修正後は、tt.m()はフィールドmに代入されたff関数を正しく呼び出し、"ff"を返すことが期待されます。

コアとなるコードの変更箇所

src/cmd/gc/reflect.c

--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -158,10 +158,13 @@ methods(Type *t)
 	// generating code if necessary.
 	a = nil;
 	for(f=mt->xmethod; f; f=f->down) {
-		if(f->type->etype != TFUNC)
-			continue;
 		if(f->etype != TFIELD)
-			fatal("methods: not field");
+			fatal("methods: not field %T", f);
+		if (f->type->etype != TFUNC || f->type->thistuple == 0)
+			fatal("non-method on %T method %S %T\n", mt, f->sym, f);
+		if (!getthisx(f->type)->type)
+			fatal("receiver with no type on %T method %S %T\n", mt, f->sym, f);
+
 		method = f->sym;
 		if(method == nil)
 			continue;

src/cmd/gc/subr.c

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2178,8 +2178,11 @@ expandmeth(Sym *s, Type *t)
 			if(c == 0)
 				continue;
 			if(c == 1) {
-				sl->good = 1;
-				sl->field = f;
+				// addot1 may have dug out arbitrary fields, we only want methods.
+				if(f->type->etype == TFUNC && f->type->thistuple > 0) {
+					sl->good = 1;
+					sl->field = f;
+				}
 			}
 			break;
 		}

test/fixedbugs/bug372.go

--- /dev/null
+++ b/test/fixedbugs/bug372.go
@@ -0,0 +1,28 @@
+// $G $D/$F.go && $L $F.$A && ./$A.out || echo BUG: bug372
+
+// Copyright 2011 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 2355
+package main
+
+type T struct {}
+func (T) m() string { return "T" }
+
+type TT struct {
+	T
+	m func() string
+}
+
+
+func ff() string { return "ff" }
+
+func main() {
+	var tt TT
+	tt.m = ff
+
+	if tt.m() != "ff" {
+		println(tt.m(), "!= \"ff\"")
+	}
+}

コアとなるコードの解説

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

methods関数内の変更は、xmethodリストを走査する際のバリデーションを強化しています。

  • if(f->etype != TFIELD): fTFIELD(構造体のフィールド)でない場合、これは予期せぬ状態であるため、fatalエラーでコンパイラを停止させます。xmethodリストにはフィールドとして登録されたメソッドのみが含まれるべきという前提を強制します。
  • if (f->type->etype != TFUNC || f->type->thistuple == 0):
    • f->type->etype != TFUNC: fの型が関数型でない場合、それはメソッドではないためエラーとします。
    • f->type->thistuple == 0: fの型が関数型であっても、レシーバ引数(thistupleが0)を持たない場合、それは通常の関数でありメソッドではないためエラーとします。この条件が、構造体の関数型フィールドと実際のメソッドを区別する鍵となります。
  • if (!getthisx(f->type)->type): メソッドのレシーバ型が取得できない場合もエラーとします。これは、メソッドの定義が不完全であるか、内部的な不整合があることを示唆します。

これらのfatalコールは、コンパイラの早期段階で問題を検出し、より詳細なエラーメッセージを提供することで、デバッグを容易にし、コンパイラの安定性を向上させます。

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

expandmeth関数内の変更は、addot1が発見したフィールドが実際にメソッドであるかどうかを厳密にチェックします。

  • if(f->type->etype == TFUNC && f->type->thistuple > 0):
    • f->type->etype == TFUNC: 見つかったフィールドfの型が関数型であることを確認します。
    • f->type->thistuple > 0: その関数型がレシーバ引数を持つことを確認します。

この二つの条件が同時に満たされた場合にのみ、sl->good = 1; sl->field = f;として、そのフィールドを有効なメソッドとして登録します。これにより、addot1が誤って掘り出した非メソッドの関数型フィールドが、メソッドとして扱われることを防ぎます。

test/fixedbugs/bug372.goの追加

このテストは、Go言語の埋め込みとフィールドの命名が重なるエッジケースを狙っています。TT構造体はTを埋め込んでいるため、T.m()メソッドを「継承」します。しかし、同時にTT自身もm func() stringというフィールドを持っています。

このテストの目的は、コンパイラがtt.mTTのフィールドmを指していることを正しく認識し、Tのメソッドmと混同しないことを確認することです。修正前は、この混同が発生し、tt.m = ffのような代入が正しく機能しないか、コンパイルエラーを引き起こす可能性がありました。修正後は、tt.m()ff関数を呼び出し、期待通り"ff"を返すことで、コンパイラが正しく動作していることを示します。

関連リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Go言語のIssueトラッカー(このコミットが修正したIssue #2355の直接のリンクは、公開されているGitHubリポジトリのIssueリストからは見つけられませんでしたが、内部的なトラッカーの可能性があります。)
  • Go言語のコンパイラに関するドキュメント(Goのソースコード内や公式ドキュメントを参照)

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/cmd/gc/ディレクトリ)
  • Go言語の仕様書
  • Go言語のコンパイラに関する技術記事や解説(一般的なGoコンパイラの動作原理に関する情報)
  • GitHubのコミット履歴と関連する議論
  • Go言語のreflectパッケージに関するドキュメント(リフレクションの動作を理解するため)
  • Go言語の埋め込みに関するドキュメント