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

[インデックス 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つの関数に集中しています。

  1. genhash(Sym *sym, Type *t) 関数
  2. 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")
	}
}

コアとなるコードの解説

genhashgeneq関数は、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が更新されるようになります。

この変更によって、genhashgeneqは、ハッシュや比較の対象とすべき有効なフィールドのみを正しく識別できるようになり、ブランク識別子フィールドを持つ構造体に対しても安定してハッシュ関数と等価性比較関数を生成できるようになりました。

追加されたbug442.goテストケースは、この修正の有効性を確認するために重要です。このテストは、_ int_ stringというブランク識別子フィールドを持つ構造体Tを定義し、このT型をmapのキーとして使用しています。mapはキーのハッシュと等価性比較を内部的に行うため、このテストがクラッシュせずに正常に実行されることは、コンパイラの修正が成功したことを意味します。

関連リンク

参考にした情報源リンク

  • Go Issue 3607のGitHubページ
  • Go CL 6296052のGerritページ
  • Go言語の公式ドキュメント(ブランク識別子、構造体、mapに関する情報)
  • Goコンパイラのソースコード(src/cmd/gc/subr.cの関連部分)
  • Go言語のmapの内部実装に関する一般的な知識