[インデックス 13311] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における、構造体(struct
)がアンダースコア(_
)で始まるフィールド(ブランク識別子フィールド)を持つ場合に発生するクラッシュを修正するものです。具体的には、そのような構造体に対してハッシュ関数(genhash
)や等価性比較関数(geneq
)を生成する際に問題が発生していました。
コミット
commit 744b23fe4827598a3e76b8fd014fccc824048788
Author: Russ Cox <rsc@golang.org>
Date: Thu Jun 7 02:05:08 2012 -0400
cmd/gc: do not crash on struct with _ field
Fixes #3607.
R=ken2
CC=golang-dev
https://golang.org/cl/6296052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/744b23fe4827598a3e76b8fd014fccc824048788
元コミット内容
このコミットは、Goコンパイラのcmd/gc
において、アンダースコア(_
)で始まるフィールドを持つ構造体(struct
)が原因で発生するクラッシュを修正します。この問題は、Issue 3607として報告されていました。
変更の背景
Go言語では、構造体のフィールド名としてアンダースコア_
を使用することができます。これは「ブランク識別子」と呼ばれ、そのフィールドの値が使用されないことをコンパイラに示します。通常、ブランク識別子フィールドは、構造体のメモリレイアウトを調整するためや、特定のインターフェースを満たすためにダミーのフィールドを置く場合などに利用されます。
しかし、Goコンパイラ(cmd/gc
)が、このようなブランク識別子フィールドを持つ構造体に対して、ハッシュ関数(genhash
)や等価性比較関数(geneq
)を生成する際に、内部的な処理の誤りによりクラッシュするバグが存在していました。特に、map
のキーとしてこのような構造体を使用しようとすると、コンパイラがハッシュ関数を生成する必要があるため、この問題が顕在化しました。
このバグは、GoのIssueトラッカーで「Issue 3607: cmd/gc: crash on struct with _ field」として報告されており、このコミットはその修正を目的としています。
前提知識の解説
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラであり、Goのソースコードを機械語に変換する役割を担っています。gc
はGoコンパイラの歴史的な名称です。 - 構造体 (Struct): 異なる型のフィールドをまとめた複合データ型です。C言語の構造体やC++のクラスに似ています。
- ブランク識別子 (
_
): Go言語における特殊な識別子で、値が使用されないことを明示的に示すために使われます。例えば、関数の戻り値の一部を無視したり、インポートしたパッケージの副作用だけを利用したりする場合に用いられます。構造体のフィールド名として使用されることもあります。 - ハッシュ関数 (
genhash
): データ構造(この場合は構造体)から、そのデータを一意に識別する数値(ハッシュ値)を生成する関数です。map
のキーとして構造体を使用する場合など、データの高速な検索や比較に不可欠です。 - 等価性比較関数 (
geneq
): 2つのデータ構造が等しいかどうかを比較する関数です。これもmap
のキーの比較や、==
演算子による構造体の比較などで使用されます。 map
型: キーと値のペアを格納するGoの組み込みデータ型です。キーはハッシュ可能である必要があり、構造体をキーとして使用する場合、コンパイラはその構造体に対するハッシュ関数と等価性比較関数を自動的に生成します。algtype1
: コンパイラ内部で使用される関数で、型の「アラインメント」や「サイズ」といったメモリレイアウトに関する情報を取得するために使われることがあります。AMEM
は、その型がメモリに格納されることを示す内部的な定数です。isblanksym
: シンボルがブランク識別子(_
)であるかどうかを判定するコンパイラ内部の関数です。
技術的詳細
このバグは、src/cmd/gc/subr.c
ファイル内のgenhash
関数とgeneq
関数に存在していました。これらの関数は、構造体のフィールドを走査し、ハッシュ値の計算や等価性の比較に必要なフィールドを特定します。
問題のコードは、構造体のフィールドをイテレートするループ内で、ハッシュや比較の対象とすべき「最初の」フィールド(first
変数)を特定するロジックにありました。元のコードでは、ブランク識別子フィールド(isblanksym(t1->sym)
が真)またはメモリに格納される型(algtype1(t1->type, nil) == AMEM
が真)のフィールドをスキップしていました。
// 修正前
if(t1 != T && (isblanksym(t1->sym) || algtype1(t1->type, nil) == AMEM)) {
if(first == T)
first = t1; // ここが問題
continue;
}
ここで、first == T
という条件は、まだ有効なフィールドが見つかっていない場合にfirst
を更新するという意図でした。しかし、ブランク識別子フィールドはハッシュや比較の対象から除外されるべきであるにもかかわらず、このロジックではブランク識別子フィールドがfirst
として誤って選択される可能性がありました。特に、構造体の最初のフィールドがブランク識別子である場合に、この問題が発生しやすかったと考えられます。
ブランク識別子フィールドは、その値が使用されないため、ハッシュ値の計算や等価性比較には寄与しません。したがって、これらのフィールドはハッシュや比較の対象から完全に除外されるべきです。しかし、誤ってfirst
として設定されてしまうと、後続の処理で不正なメモリアクセスやロジックの破綻を引き起こし、コンパイラのクラッシュにつながっていました。
修正は、first
を更新する条件に!isblanksym(t1->sym)
を追加することで、ブランク識別子フィールドがfirst
として選択されないようにしました。
// 修正後
if(t1 != T && (isblanksym(t1->sym) || algtype1(t1->type, nil) == AMEM)) {
if(first == T && !isblanksym(t1->sym)) // ここが修正点
first = t1;
continue;
}
この変更により、first
は常にハッシュや比較の対象となる有効なフィールドを指すようになり、ブランク識別子フィールドが誤って処理されることがなくなりました。
また、このコミットにはtest/fixedbugs/bug442.go
という新しいテストファイルが追加されています。このテストは、ブランク識別子フィールドを持つ構造体T
を定義し、それをmap
のキーとして使用するシナリオを再現しています。このテストが正常に実行されることで、修正が正しく適用され、以前のクラッシュが解消されたことが確認できます。
コアとなるコードの変更箇所
変更はsrc/cmd/gc/subr.c
ファイル内の以下の2つの関数に集中しています。
genhash(Sym *sym, Type *t)
関数geneq(Sym *sym, Type *t)
関数
それぞれの関数で、first
変数を初期化または更新するロジックが変更されています。
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2684,7 +2684,7 @@ genhash(Sym *sym, Type *t)
first = T;
for(t1=t->type;; t1=t1->down) {
if(t1 != T && (isblanksym(t1->sym) || algtype1(t1->type, nil) == AMEM)) {
- if(first == T)
+ if(first == T && !isblanksym(t1->sym))
first = t1;
continue;
}
@@ -2901,7 +2901,7 @@ geneq(Sym *sym, Type *t)
first = T;
for(t1=t->type;; t1=t1->down) {
if(t1 != T && (isblanksym(t1->sym) || algtype1(t1->type, nil) == AMEM)) {
- if(first == T)
+ if(first == T && !isblanksym(t1->sym))
first = t1;
continue;
}
また、この修正を検証するための新しいテストケースが追加されています。
test/fixedbugs/bug442.go
// run
// 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.
// Used to crash generating hash and == functions for struct
// with leading _ field. Issue 3607.
package main
type T struct {
_ int
X interface{}
_ string
Y float64
}
func main() {
m := map[T]int{}
m[T{X: 1, Y: 2}] = 1
m[T{X: 2, Y: 3}] = 2
m[T{X: 1, Y: 2}] = 3 // overwrites first entry
if len(m) != 2 {
println("BUG")
}
}
コアとなるコードの解説
genhash
とgeneq
関数は、Goコンパイラのバックエンドの一部であり、特定の型(特に構造体)に対して、実行時に必要となるハッシュ計算ロジックや等価性比較ロジックを生成する役割を担っています。
これらの関数内部では、構造体の各フィールドを順に検査し、ハッシュや比較の対象とすべきフィールドを特定します。first
変数は、ハッシュや比較の対象となる最初の「意味のある」フィールドを追跡するために使用されます。
元のコードでは、以下の条件でフィールドをスキップしていました。
isblanksym(t1->sym)
: フィールドがブランク識別子(_
)である場合。
algtype1(t1->type, nil) == AMEM
: フィールドの型がメモリに格納される型である場合(これは通常、ハッシュや比較の対象となるべきフィールドです)。
問題は、if(first == T)
という条件が、まだfirst
が設定されていない場合にfirst
を更新するという意図であったにもかかわらず、isblanksym(t1->sym)
が真であるフィールド(つまりブランク識別子フィールド)もfirst
として設定してしまう可能性があった点です。ブランク識別子フィールドはハッシュや比較の対象外であるため、これをfirst
として設定してしまうと、後続のハッシュ/比較ロジックが不正なフィールドを参照し、クラッシュを引き起こしていました。
修正は、if(first == T)
の条件に加えて、&& !isblanksym(t1->sym)
という条件を追加しました。これにより、first
がまだ設定されておらず、かつ現在のフィールドがブランク識別子ではない場合にのみfirst
が更新されるようになります。
この変更によって、genhash
とgeneq
は、ハッシュや比較の対象とすべき有効なフィールドのみを正しく識別できるようになり、ブランク識別子フィールドを持つ構造体に対しても安定してハッシュ関数と等価性比較関数を生成できるようになりました。
追加されたbug442.go
テストケースは、この修正の有効性を確認するために重要です。このテストは、_ int
と_ string
というブランク識別子フィールドを持つ構造体T
を定義し、このT
型をmap
のキーとして使用しています。map
はキーのハッシュと等価性比較を内部的に行うため、このテストがクラッシュせずに正常に実行されることは、コンパイラの修正が成功したことを意味します。
関連リンク
- Go Issue 3607: cmd/gc: crash on struct with _ field: https://github.com/golang/go/issues/3607
- Go CL 6296052: cmd/gc: do not crash on struct with _ field: https://golang.org/cl/6296052
参考にした情報源リンク
- Go Issue 3607のGitHubページ
- Go CL 6296052のGerritページ
- Go言語の公式ドキュメント(ブランク識別子、構造体、mapに関する情報)
- Goコンパイラのソースコード(
src/cmd/gc/subr.c
の関連部分) - Go言語の
map
の内部実装に関する一般的な知識