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

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

このコミットは、Go言語のコンパイラの一つであるgccgoが特定の状況下で誤ったコードを生成するバグ(miscompilation)を修正するために追加されたテストケースに関するものです。具体的には、グローバル変数の初期化順序に関する問題が原因で発生していたバグを再現し、その修正を検証するためのテストコードが追加されました。

コミット

commit e38339e3d8e89b7dcffef41a2c373f3c48f032ad
Author: Ian Lance Taylor <iant@golang.org>
Date:   Mon Jan 28 16:17:06 2013 -0800

    test: add test case miscompiled by gccgo

    R=golang-dev, bradfitz, rsc, iant
    CC=golang-dev
    https://golang.org/cl/7240043
---
 test/fixedbugs/bug473.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 69 insertions(+)

diff --git a/test/fixedbugs/bug473.go b/test/fixedbugs/bug473.go
new file mode 100644
index 0000000000..49ce7d7379
--- /dev/null
+++ b/test/fixedbugs/bug473.go
@@ -0,0 +1,69 @@
+// run
+
+// Copyright 2013 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 be miscompiled by gccgo, due to a bug in handling
+// initialization ordering.
+
+package main
+
+func F(a ...interface{}) interface{} {
+	s := 0
+	for _, v := range a {
+		s += v.(int)
+	}
+	return s
+}
+
+var V1 = F(V10, V4, V3, V11)
+
+var V2 = F(V1)
+
+var V3 = F(1)
+
+var V4 = F(2)
+
+var V5 = F(3)
+
+var V6 = F(4)
+
+var V7 = F(5)
+
+var V8 = F(V14, V7, V3, V6, V5)
+
+var V9 = F(V4, F(V12))
+
+var V10 = F(V4, V9)
+
+var V11 = F(6)
+
+var V12 = F(V5, V3, V8)
+
+var V13 = F(7)
+
+var V14 = F(8)
+
+func expect(name string, a interface{}, b int) {
+	if a.(int) != b {
+		panic(name)
+	}
+}
+
+func main() {
+	expect("V1", V1, 38)
+	expect("V2", V2, 38)
+	expect("V3", V3, 1)
+	expect("V4", V4, 2)
+	expect("V5", V5, 3)
+	expect("V6", V6, 4)
+	expect("V7", V7, 5)
+	expect("V8", V8, 21)
+	expect("V9", V9, 27)
+	expect("V10", V10, 29)
+	expect("V11", V11, 6)
+	expect("V12", V12, 25)
+	expect("V13", V13, 7)
+	expect("V14", V14, 8)
+}

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

https://github.com/golang/go/commit/e38339e3d8e89b7dcffef41a2c373f3c48f032ad

元コミット内容

test: add test case miscompiled by gccgo

R=golang-dev, bradfitz, rsc, iant
CC=golang-dev
https://golang.org/cl/7240043

変更の背景

このコミットの背景には、Go言語のコンパイラの一つであるgccgoにおける特定のバグが存在しました。具体的には、Goプログラムにおけるグローバル変数の初期化順序の取り扱いに関する問題が原因で、gccgoが誤った実行ファイルを生成してしまうという「miscompilation」が発生していました。

Go言語では、パッケージレベルの変数は、そのパッケージのinit関数が実行される前に初期化されます。この初期化は、変数の依存関係に基づいて順序付けられます。例えば、ある変数が別の変数の値に依存している場合、依存される変数が先に初期化される必要があります。gccgoは、この複雑な初期化順序の解決において、特定のケースで誤った順序で変数を初期化してしまい、結果としてプログラムが期待通りの動作をしないという問題がありました。

このコミットは、そのバグを明確に再現し、修正が正しく適用されたことを検証するためのテストケースを追加することを目的としています。このようなテストケースは、将来的な回帰を防ぎ、コンパイラの堅牢性を高める上で不可欠です。

前提知識の解説

Go言語のグローバル変数初期化

Go言語では、パッケージレベルで宣言された変数は、プログラムの実行開始前に初期化されます。この初期化は、宣言順序だけでなく、変数の依存関係に基づいて行われます。

  • 宣言順序: 基本的には、ファイル内で宣言された順序で初期化されます。
  • 依存関係: ある変数の初期化式が別のパッケージレベルの変数を参照している場合、参照される変数が先に初期化されます。この依存関係は、複雑なグラフを形成することがあり、Goランタイムはこれを解決して正しい初期化順序を決定します。
  • 初期化ループ: 循環参照がある場合、Goコンパイラはエラーを報告します。
  • init関数: 各パッケージは複数のinit関数を持つことができ、これらはパッケージの初期化が完了した後に自動的に実行されます。グローバル変数の初期化はinit関数よりも前に行われます。

gccgo

gccgoは、GCC (GNU Compiler Collection) のフロントエンドとして実装されたGo言語のコンパイラです。Go言語の公式コンパイラであるgc(Go Compiler)とは異なる実装であり、GCCの最適化バックエンドを利用できるという特徴があります。しかし、異なる実装であるため、gcでは発生しないような特定のバグがgccgoで発生することがあります。今回のケースはまさにその一例で、Go言語の仕様に厳密に従った初期化順序のセマンティクスをgccgoが完全に実装できていなかったために発生したと考えられます。

可変引数関数 (...interface{})

Go言語の関数は、可変引数(variadic arguments)を受け取ることができます。これは、引数の数が不定であることを意味します。func F(a ...interface{}) のように宣言された関数は、任意の数の引数を受け取ることができ、それらの引数は関数内でinterface{}型のスライスaとして扱われます。このテストケースでは、F関数がこの可変引数機能を利用して、複数のint型の値を合計しています。interface{}型から元の型(int)への型アサーション(v.(int))も使用されています。

技術的詳細

このテストケースtest/fixedbugs/bug473.goは、Go言語のグローバル変数の初期化順序が複雑に絡み合う状況を意図的に作り出すことで、gccgoのバグを再現するように設計されています。

コード内で定義されているF関数は、可変引数を受け取り、それらをint型に型アサートして合計を返すシンプルな関数です。この関数自体にバグはありませんが、この関数がグローバル変数の初期化式の中で再帰的かつ相互に依存する形で呼び出される点が重要です。

例えば、以下のような変数の定義があります。

var V1 = F(V10, V4, V3, V11)
var V2 = F(V1)
var V3 = F(1)
var V4 = F(2)
var V9 = F(V4, F(V12))
var V10 = F(V4, V9)
var V12 = F(V5, V3, V8)

これらの定義を見ると、V1V10, V4, V3, V11に依存し、V10V4V9に依存し、V9V4V12に依存し、V12V5, V3, V8に依存するといった具合に、複雑な依存関係の連鎖が形成されています。Go言語の仕様では、これらの変数が正しく初期化されるためには、依存関係の順序が厳密に守られる必要があります。例えば、V1を初期化する前にV10, V4, V3, V11が全て初期化されている必要があります。

gccgoのバグは、このような複雑な依存関係を持つグローバル変数の初期化において、正しい順序を決定できず、誤った順序で初期化を実行してしまったことに起因します。これにより、F関数が呼び出された際に、まだ初期化されていない(ゼロ値のままの)変数が引数として渡され、結果としてF関数の戻り値、ひいては依存するグローバル変数の値が期待と異なるものになっていました。

main関数内のexpect関数は、各グローバル変数の最終的な値が期待される値と一致するかどうかを検証します。もし一致しない場合はpanicを発生させ、テストが失敗したことを示します。このテストケースが追加されたことで、gccgoの初期化順序のバグが修正されたかどうかを自動的に検証できるようになりました。

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

このコミットで追加されたファイルはtest/fixedbugs/bug473.goのみです。既存のコードの変更はありません。

--- /dev/null
+++ b/test/fixedbugs/bug473.go
@@ -0,0 +1,69 @@
+// run
+
+// Copyright 2013 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 be miscompiled by gccgo, due to a bug in handling
+// initialization ordering.
+
+package main
+
+func F(a ...interface{}) interface{} {
+	s := 0
+	for _, v := range a {
+		s += v.(int)
+	}
+	return s
+}
+
+var V1 = F(V10, V4, V3, V11)
+
+var V2 = F(V1)
+
+var V3 = F(1)
+
+var V4 = F(2)
+
+var V5 = F(3)
+
+var V6 = F(4)
+
+var V7 = F(5)
+
+var V8 = F(V14, V7, V3, V6, V5)
+
+var V9 = F(V4, F(V12))
+
+var V10 = F(V4, V9)
+
+var V11 = F(6)
+
+var V12 = F(V5, V3, V8)
+
+var V13 = F(7)
+
+var V14 = F(8)
+
+func expect(name string, a interface{}, b int) {
+	if a.(int) != b {
+		panic(name)
+	}
+}
+
+func main() {
+	expect("V1", V1, 38)
+	expect("V2", V2, 38)
+	expect("V3", V3, 1)
+	expect("V4", V4, 2)
+	expect("V5", V5, 3)
+	expect("V6", V6, 4)
+	expect("V7", V7, 5)
+	expect("V8", V8, 21)
+	expect("V9", V9, 27)
+	expect("V10", V10, 29)
+	expect("V11", V11, 6)
+	expect("V12", V12, 25)
+	expect("V13", V13, 7)
+	expect("V14", V14, 8)
+}

コアとなるコードの解説

追加されたtest/fixedbugs/bug473.goファイルは、以下の主要な要素で構成されています。

  1. // run ディレクティブ: ファイルの先頭にある// runコメントは、Goのテストシステムに対する指示で、このファイルが実行可能なテストであることを示します。

  2. F 関数:

    func F(a ...interface{}) interface{} {
    	s := 0
    	for _, v := range a {
    		s += v.(int)
    	}
    	return s
    }
    

    この関数は、可変引数ainterface{}型のスライス)を受け取り、その中の各要素をint型に型アサートして合計を計算し、interface{}型として返します。これは、グローバル変数の初期化式で値を計算するために使用されます。

  3. グローバル変数 V1 から V14:

    var V1 = F(V10, V4, V3, V11)
    var V2 = F(V1)
    var V3 = F(1)
    // ... (他の変数定義)
    var V14 = F(8)
    

    これらの変数は、このテストケースの核心です。それぞれの変数はF関数を呼び出して初期化されますが、その引数には他のグローバル変数が含まれていることが多く、これにより複雑な初期化依存関係が形成されます。例えば、V1の初期化にはV10, V4, V3, V11の値が必要であり、V10の初期化にはV4V9の値が必要、といった具合です。この相互依存関係が、gccgoの初期化順序バグを露呈させるポイントでした。

  4. expect 関数:

    func expect(name string, a interface{}, b int) {
    	if a.(int) != b {
    		panic(name)
    	}
    }
    

    このヘルパー関数は、テスト対象の変数の実際の値aが期待される値bと一致するかどうかを検証します。一致しない場合は、変数の名前nameを引数にpanicを発生させ、テストの失敗を報告します。

  5. main 関数:

    func main() {
    	expect("V1", V1, 38)
    	expect("V2", V2, 38)
    	// ... (他の expect 呼び出し)
    	expect("V14", V14, 8)
    }
    

    main関数は、プログラムのエントリポイントであり、ここで各グローバル変数の最終的な値が期待される値と一致するかどうかをexpect関数を使って検証します。これらの期待値は、Go言語の正しい初期化順序に従って計算された値です。もしgccgoがバグを抱えていた場合、これらのexpect呼び出しのいずれかがpanicを引き起こし、テストが失敗します。

このテストケースは、Go言語のグローバル変数の初期化セマンティクスを深く理解し、それを複雑な依存関係の形で表現することで、コンパイラの潜在的なバグを効果的に特定できるように設計されています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GCCのドキュメント (gccgoに関する情報)
  • Go言語のソースコードリポジトリ (特にtest/fixedbugsディレクトリ内の他のテストケース)
  • Go言語のIssueトラッカー (関連するバグ報告や議論)
  • Go言語のメーリングリスト (golang-devなど)
  • Stack Overflowや技術ブログ記事 (Goの初期化順序やgccgoに関する議論)
  • https://golang.org/cl/7240043 (このコミットに関連するGo Code Reviewの変更リスト)
  • https://go.dev/issue/473 (このコミットが修正した可能性のあるバグトラッカーのIssue)
    • 注: コミットメッセージのbug473.goというファイル名から、GoのIssue #473に関連する可能性が高いと推測されます。