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

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

このコミットは、Go言語のコンパイラの一つであるgccgoが、特定の有効なGoコードをコンパイルする際にクラッシュするというバグ(bug439)を修正するために、そのバグを再現するテストケースを追加したものです。このテストケースは、gccgoが定数に対するメソッド呼び出しとポインタの取得を適切に処理できない問題を示しています。

コミット

commit 6f6bbdf9b7de15b68b52649be35cd8b62a2edb9d
Author: Ian Lance Taylor <iant@golang.org>
Date:   Tue May 15 13:29:46 2012 -0700

    test: add bug439, valid code that caused a gccgo crash
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6198075

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

https://github.com/golang/go/commit/6f6bbdf9b7de15b68b52649be35cd8b62a2edb9d

元コミット内容

test: add bug439, valid code that caused a gccgo crash

変更の背景

このコミットの背景には、Go言語のコンパイラ実装の一つであるgccgoが、Go言語の仕様に準拠した有効なコードをコンパイルする際に予期せぬクラッシュを引き起こすという重大なバグが存在していました。コンパイラのクラッシュは、開発者が正しく記述したプログラムをビルドできないという深刻な問題であり、開発体験を著しく損ないます。

具体的には、bug439として知られるこのバグは、カスタム型に定義されたメソッドを定数に対して呼び出し、そのメソッドがレシーバのアドレスを返すような特定のコードパターンで発生しました。このようなコードはGo言語の仕様上は完全に合法であり、標準のGoコンパイラ(gc)では問題なくコンパイルできます。しかし、gccgoの実装に欠陥があったため、このパターンを検出するとコンパイラ自体が異常終了していました。

このコミットは、このバグを修正するための第一歩として、バグを確実に再現できる最小限のテストケースtest/fixedbugs/bug439.goを追加しました。テストケースを追加することで、バグが修正されたことを検証し、将来的に同様の回帰が発生しないようにするための自動化されたチェックが可能になります。

前提知識の解説

Go言語の基本

  • 型(Type): Goは静的型付け言語であり、すべての変数には型があります。type E intのように、既存の型(int)を基に新しいカスタム型を定義できます。
  • 定数(Constants): Goの定数は、コンパイル時に値が決定される不変のエンティティです。数値、真偽値、文字列などのリテラルや、他の定数から派生して定義されます。
  • メソッド(Methods): Goでは、構造体だけでなく、任意の型(カスタム型を含む)にメソッドを関連付けることができます。メソッドはレシーバ引数(func (e E) P() *Ee Eの部分)を持ち、その型の値に対して操作を実行します。
  • ポインタ(Pointers): Goはポインタをサポートしており、変数のメモリアドレスを指し示します。&演算子(アドレス演算子)は変数のアドレスを取得し、*演算子(間接参照演算子)はポインタが指す値を取得します。
  • レシーバ(Receiver): メソッドの定義において、func (e E) P()のように括弧内に記述される変数をレシーバと呼びます。レシーバは値レシーバ(e E)とポインタレシーバ(e *E)があります。値レシーバの場合、メソッド内ではレシーバのコピーが使用されます。

gccgoとは

gccgoは、Go言語のフロントエンドとしてGCC(GNU Compiler Collection)に統合されたコンパイラです。Go言語のソースコードをGCCの中間表現に変換し、その後GCCのバックエンドが様々なアーキテクチャ向けの機械語コードを生成します。

Go言語には主に二つの主要なコンパイラ実装が存在します。

  1. gc: Goプロジェクトが公式に開発・メンテナンスしている標準のコンパイラです。Goツールチェーンのデフォルトとして使用されます。
  2. gccgo: GCCの一部として開発されており、GCCの最適化やコード生成のインフラストラクチャを利用します。これにより、特定のプラットフォームでのパフォーマンス向上や、既存のC/C++コードベースとの連携が容易になる場合があります。

両者はGo言語の仕様に準拠していますが、内部実装やコード生成の戦略が異なるため、稀に一方のコンパイラでのみ問題が発生することがあります。今回のケースはまさにその一例で、gccgoが特定の有効なGoコードパターンを正しく処理できなかったためにクラッシュしていました。

コンパイラのクラッシュ

コンパイラのクラッシュとは、コンパイラがソースコードを処理している最中に、予期せぬエラーや例外によって異常終了してしまう現象を指します。これは通常、コンパイラ自体のバグを示しており、入力されたソースコードがGo言語の文法や意味論に完全に準拠しているにもかかわらず発生します。コンパイラのクラッシュは、開発者がコードをビルドできないため、開発プロセスを停止させる深刻な問題です。

技術的詳細

bug439.goのコードは、gccgoがクラッシュする特定のパターンを最小限の形で示しています。

package p

type E int

func (e E) P() *E { return &e }

const (
	C1 E = 0
	C2 = C1
)

func F() *E {
	return C2.P()
}

このコードの核心は以下の点にあります。

  1. カスタム型Eの定義: type E intにより、int型を基にした新しい型Eが定義されます。
  2. 値レシーバを持つメソッドP:
    func (e E) P() *E { return &e }
    
    このメソッドは型Eに属し、値レシーバeを取ります。重要なのは、メソッド内でレシーバeのアドレス&eを返している点です。Go言語では、値レシーバはメソッド呼び出し時にレシーバのコピーを受け取ります。したがって、&eはレシーバのコピーのメモリアドレスを指します。
  3. 定数C1C2の定義:
    const (
    	C1 E = 0
    	C2 = C1
    )
    
    C1は型Eの定数で値は0です。C2C1から派生した定数です。Goの定数はアドレスを持たない(メモリ上に存在しない)という性質があります。
  4. 定数に対するメソッド呼び出し:
    func F() *E {
    	return C2.P()
    }
    
    関数F内で、定数C2に対してメソッドP()が呼び出されています。 Go言語の仕様では、定数に対してメソッドを呼び出すことが許可されています。この場合、定数は一時的な変数に変換され、その一時的な変数がメソッドのレシーバとして渡されます。

gccgoのクラッシュは、この「定数から一時変数への変換」と「その一時変数のアドレスを取得する」という一連の操作の組み合わせで発生したと考えられます。

  • C2.P()が呼び出される際、定数C2はまず型Eの一時的な変数に変換されます。
  • メソッドPの内部では、この一時変数のコピーがレシーバeとして渡されます。
  • return &eの部分で、このレシーバのコピー(つまり一時変数のコピー)のアドレスが取得されます。

問題は、gccgoがこの一時変数の生成と、そのアドレスの取得、そしてそのポインタのライフタイム管理を正しく行えなかったことに起因すると推測されます。特に、定数から派生した値に対するアドレス取得は、コンパイラにとって複雑なエッジケースとなることがあります。gccgoは、この特定のシナリオで内部的な不整合や不正なメモリアクセスを引き起こし、結果としてクラッシュに至ったと考えられます。

このテストケースは、このような複雑な相互作用を最小限のコードで再現し、コンパイラのバグを特定し修正するための重要な手がかりを提供しました。

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

このコミットでは、既存のファイルへの変更はなく、新たに以下のテストファイルが追加されました。

  • test/fixedbugs/bug439.go

このファイルの内容は以下の通りです。

// compile

// 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.

// Gccgo used to crash compiling this.

package p

type E int

func (e E) P() *E { return &e }

const (
	C1 E = 0
	C2 = C1
)

func F() *E {
	return C2.P()
}

ファイルの先頭にある// compileコメントは、Goのテストフレームワークにおいて、このファイルがコンパイル可能であるべきことを示す特別な指示です。もしコンパイルに失敗した場合、テストは失敗とみなされます。

コアとなるコードの解説

追加されたbug439.goファイルは、gccgoコンパイラのバグを再現するための最小限のGoプログラムです。

  1. package p: パッケージ宣言。
  2. type E int: int型を基底とする新しい型Eを定義します。これは、組み込み型ではなくカスタム型に対してメソッドが定義されるシナリオをシミュレートします。
  3. func (e E) P() *E { return &e }:
    • EPというメソッドを定義しています。
    • レシーバeは値レシーバです。つまり、Pが呼び出されるとき、eはレシーバのコピーになります。
    • メソッドは*E型(E型へのポインタ)を返します。
    • return &eは、レシーバeのメモリアドレスを返します。このeはメソッド呼び出し時に作成された一時的なコピーです。
  4. const ( C1 E = 0; C2 = C1 ):
    • C1は型Eの定数で、値は0です。
    • C2C1から派生した定数です。定数はコンパイル時に解決され、通常はメモリ上のアドレスを持ちません。
  5. func F() *E { return C2.P() }:
    • 関数F*E型を返します。
    • C2.P()は、定数C2に対してメソッドPを呼び出しています。Goの仕様では、定数に対してメソッドを呼び出す場合、その定数は一時的な変数に「昇格」され、その一時変数がメソッドのレシーバとして渡されます。

gccgoがクラッシュした原因は、このC2.P()の呼び出し、特に定数C2が一時変数に変換され、その一時変数のコピーのアドレスがPメソッド内で取得されるという一連の処理を、gccgoが正しく扱えなかったためです。コンパイラが、定数から生成された一時変数のライフタイムやメモリアドレスの管理を誤った結果、不正なメモリアクセスや内部エラーを引き起こし、クラッシュに至ったと考えられます。

このテストケースは、gccgoがこの特定のコードパターンをコンパイルできるようになったことを確認するために使用されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go言語の型、定数、メソッド、ポインタに関する一般的な知識)
  • GCCgoに関する一般的な情報 (GCCのGoフロントエンドとしての役割)
  • コンパイラの動作原理に関する一般的な知識# [インデックス 13078] ファイルの概要

このコミットは、Go言語のコンパイラの一つであるgccgoが、特定の有効なGoコードをコンパイルする際にクラッシュするというバグ(bug439)を修正するために、そのバグを再現するテストケースを追加したものです。このテストケースは、gccgoが定数に対するメソッド呼び出しとポインタの取得を適切に処理できない問題を示しています。

コミット

commit 6f6bbdf9b7de15b68b52649be35cd8b62a2edb9d
Author: Ian Lance Taylor <iant@golang.org>
Date:   Tue May 15 13:29:46 2012 -0700

    test: add bug439, valid code that caused a gccgo crash
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6198075

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

https://github.com/golang/go/commit/6f6bbdf9b7de15b68b52649be35cd8b62a2edb9d

元コミット内容

test: add bug439, valid code that caused a gccgo crash

変更の背景

このコミットの背景には、Go言語のコンパイラ実装の一つであるgccgoが、Go言語の仕様に準拠した有効なコードをコンパイルする際に予期せぬクラッシュを引き起こすという重大なバグが存在していました。コンパイラのクラッシュは、開発者が正しく記述したプログラムをビルドできないという深刻な問題であり、開発体験を著しく損ないます。

具体的には、bug439として知られるこのバグは、カスタム型に定義されたメソッドを定数に対して呼び出し、そのメソッドがレシーバのアドレスを返すような特定のコードパターンで発生しました。このようなコードはGo言語の仕様上は完全に合法であり、標準のGoコンパイラ(gc)では問題なくコンパイルできます。しかし、gccgoの実装に欠陥があったため、このパターンを検出するとコンパイラ自体が異常終了していました。

このコミットは、このバグを修正するための第一歩として、バグを確実に再現できる最小限のテストケースtest/fixedbugs/bug439.goを追加しました。テストケースを追加することで、バグが修正されたことを検証し、将来的に同様の回帰が発生しないようにするための自動化されたチェックが可能になります。

前提知識の解説

Go言語の基本

  • 型(Type): Goは静的型付け言語であり、すべての変数には型があります。type E intのように、既存の型(int)を基に新しいカスタム型を定義できます。
  • 定数(Constants): Goの定数は、コンパイル時に値が決定される不変のエンティティです。数値、真偽値、文字列などのリテラルや、他の定数から派生して定義されます。
  • メソッド(Methods): Goでは、構造体だけでなく、任意の型(カスタム型を含む)にメソッドを関連付けることができます。メソッドはレシーバ引数(func (e E) P() *Ee Eの部分)を持ち、その型の値に対して操作を実行します。
  • ポインタ(Pointers): Goはポインタをサポートしており、変数のメモリアドレスを指し示します。&演算子(アドレス演算子)は変数のアドレスを取得し、*演算子(間接参照演算子)はポインタが指す値を取得します。
  • レシーバ(Receiver): メソッドの定義において、func (e E) P()のように括弧内に記述される変数をレシーバと呼びます。レシーバは値レシーバ(e E)とポインタレシーバ(e *E)があります。値レシーバの場合、メソッド内ではレシーバのコピーが使用されます。

gccgoとは

gccgoは、Go言語のフロントエンドとしてGCC(GNU Compiler Collection)に統合されたコンパイラです。Go言語のソースコードをGCCの中間表現に変換し、その後GCCのバックエンドが様々なアーキテクチャ向けの機械語コードを生成します。

Go言語には主に二つの主要なコンパイラ実装が存在します。

  1. gc: Goプロジェクトが公式に開発・メンテナンスしている標準のコンパイラです。Goツールチェーンのデフォルトとして使用されます。
  2. gccgo: GCCの一部として開発されており、GCCの最適化やコード生成のインフラストラクチャを利用します。これにより、特定のプラットフォームでのパフォーマンス向上や、既存のC/C++コードベースとの連携が容易になる場合があります。

両者はGo言語の仕様に準拠していますが、内部実装やコード生成の戦略が異なるため、稀に一方のコンパイラでのみ問題が発生することがあります。今回のケースはまさにその一例で、gccgoが特定の有効なGoコードパターンを正しく処理できなかったためにクラッシュしていました。

コンパイラのクラッシュ

コンパイラのクラッシュとは、コンパイラがソースコードを処理している最中に、予期せぬエラーや例外によって異常終了してしまう現象を指します。これは通常、コンパイラ自体のバグを示しており、入力されたソースコードがGo言語の文法や意味論に完全に準拠しているにもかかわらず発生します。コンパイラのクラッシュは、開発者がコードをビルドできないため、開発プロセスを停止させる深刻な問題です。

技術的詳細

bug439.goのコードは、gccgoがクラッシュする特定のパターンを最小限の形で示しています。

package p

type E int

func (e E) P() *E { return &e }

const (
	C1 E = 0
	C2 = C1
)

func F() *E {
	return C2.P()
}

このコードの核心は以下の点にあります。

  1. カスタム型Eの定義: type E intにより、int型を基にした新しい型Eが定義されます。
  2. 値レシーバを持つメソッドP:
    func (e E) P() *E { return &e }
    
    このメソッドは型Eに属し、値レシーバeを取ります。重要なのは、メソッド内でレシーバeのアドレス&eを返している点です。Go言語では、値レシーバはメソッド呼び出し時にレシーバのコピーを受け取ります。したがって、&eはレシーバのコピーのメモリアドレスを指します。
  3. 定数C1C2の定義:
    const (
    	C1 E = 0
    	C2 = C1
    )
    
    C1は型Eの定数で値は0です。C2C1から派生した定数です。Goの定数はアドレスを持たない(メモリ上に存在しない)という性質があります。
  4. 定数に対するメソッド呼び出し:
    func F() *E {
    	return C2.P()
    }
    
    関数F内で、定数C2に対してメソッドP()が呼び出されています。 Go言語の仕様では、定数に対してメソッドを呼び出すことが許可されています。この場合、定数は一時的な変数に変換され、その一時的な変数がメソッドのレシーバとして渡されます。

gccgoのクラッシュは、この「定数から一時変数への変換」と「その一時変数のアドレスを取得する」という一連の操作の組み合わせで発生したと考えられます。

  • C2.P()が呼び出される際、定数C2はまず型Eの一時的な変数に変換されます。
  • メソッドPの内部では、この一時変数のコピーがレシーバeとして渡されます。
  • return &eの部分で、このレシーバのコピー(つまり一時変数のコピー)のアドレスが取得されます。

問題は、gccgoがこの一時変数の生成と、そのアドレスの取得、そしてそのポインタのライフタイム管理を正しく行えなかったことに起因すると推測されます。特に、定数から派生した値に対するアドレス取得は、コンパイラにとって複雑なエッジケースとなることがあります。gccgoは、この特定のシナリオで内部的な不整合や不正なメモリアクセスを引き起こし、結果としてクラッシュに至ったと考えられます。

このテストケースは、このような複雑な相互作用を最小限のコードで再現し、コンパイラのバグを特定し修正するための重要な手がかりを提供しました。

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

このコミットでは、既存のファイルへの変更はなく、新たに以下のテストファイルが追加されました。

  • test/fixedbugs/bug439.go

このファイルの内容は以下の通りです。

// compile

// 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.

// Gccgo used to crash compiling this.

package p

type E int

func (e E) P() *E { return &e }

const (
	C1 E = 0
	C2 = C1
)

func F() *E {
	return C2.P()
}

ファイルの先頭にある// compileコメントは、Goのテストフレームワークにおいて、このファイルがコンパイル可能であるべきことを示す特別な指示です。もしコンパイルに失敗した場合、テストは失敗とみなされます。

コアとなるコードの解説

追加されたbug439.goファイルは、gccgoコンパイラのバグを再現するための最小限のGoプログラムです。

  1. package p: パッケージ宣言。
  2. type E int: int型を基底とする新しい型Eを定義します。これは、組み込み型ではなくカスタム型に対してメソッドが定義されるシナリオをシミュレートします。
  3. func (e E) P() *E { return &e }:
    • EPというメソッドを定義しています。
    • レシーバeは値レシーバです。つまり、Pが呼び出されるとき、eはレシーバのコピーになります。
    • メソッドは*E型(E型へのポインタ)を返します。
    • return &eは、レシーバeのメモリアドレスを返します。このeはメソッド呼び出し時に作成された一時的なコピーです。
  4. const ( C1 E = 0; C2 = C1 ):
    • C1は型Eの定数で、値は0です。
    • C2C1から派生した定数です。定数はコンパイル時に解決され、通常はメモリ上のアドレスを持ちません。
  5. func F() *E { return C2.P() }:
    • 関数F*E型を返します。
    • C2.P()は、定数C2に対してメソッドPを呼び出しています。Goの仕様では、定数に対してメソッドを呼び出す場合、その定数は一時的な変数に「昇格」され、その一時変数がメソッドのレシーバとして渡されます。

gccgoがクラッシュした原因は、このC2.P()の呼び出し、特に定数C2が一時変数に変換され、その一時変数のコピーのアドレスがPメソッド内で取得されるという一連の処理を、gccgoが正しく扱えなかったためです。コンパイラが、定数から生成された一時変数のライフタイムやメモリアドレスの管理を誤った結果、不正なメモリアクセスや内部エラーを引き起こし、クラッシュに至ったと考えられます。

このテストケースは、gccgoがこの特定のコードパターンをコンパイルできるようになったことを確認するために使用されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go言語の型、定数、メソッド、ポインタに関する一般的な知識)
  • GCCgoに関する一般的な情報 (GCCのGoフロントエンドとしての役割)
  • コンパイラの動作原理に関する一般的な知識