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

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

このコミットは、Go言語のテストスイートに、可変長引数(varargs)関数内でrecoverを呼び出すケースのテストを追加するものです。特に、gccgoコンパイラがこのシナリオを正しく処理していなかった問題に対処するために導入されました。これにより、panicが発生した際に可変長引数関数内のdeferrecoverが期待通りに動作するかを確認します。

コミット

commit b14a6643dc47104689facd938a0fb254996ddf85
Author: Ian Lance Taylor <iant@golang.org>
Date:   Thu Mar 1 08:24:03 2012 -0800

    test: add test of calling recover in a varargs function
    
    gccgo did not handle this correctly.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5714050

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

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

元コミット内容

test: add test of calling recover in a varargs function

gccgo did not handle this correctly.

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

変更の背景

この変更の主な背景は、Go言語のコンパイラの一つであるgccgoが、可変長引数関数(variadic function)内でrecover関数が呼び出された際に、その動作を正しく処理できていなかったというバグが存在したことです。

Go言語には、プログラムの異常終了を防ぐためのpanicrecoverというメカニズムがあります。panicは実行時のエラーや予期せぬ状況が発生した際にプログラムの通常のフローを中断させるもので、recoverdefer文の中で呼び出されることで、panicによって中断されたパニックシーケンスを捕捉し、プログラムの制御を回復させるために使用されます。

可変長引数関数は、引数の数が不定である関数を定義できるGoの強力な機能です。gccgoは、Go言語のフロントエンドとしてGCC(GNU Compiler Collection)のバックエンドを利用するコンパイラであり、標準のGoコンパイラ(gc)とは異なる実装を持っています。この実装の違いが、特定の複雑なシナリオ、特に可変長引数関数のスタックフレームや引数処理とpanic/recoverメカニズムの相互作用において、バグを引き起こしていたと考えられます。

このコミットは、gccgoのこのバグを特定し、修正を検証するために、具体的なテストケースをGoの標準テストスイートに追加することを目的としています。テストを追加することで、将来的に同様の回帰バグが発生することを防ぎ、gccgoを含むGoコンパイラの実装がpanic/recoverのセマンティクスを正しく遵守していることを保証します。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念を理解しておく必要があります。

  1. panicrecover:

    • panic: Go言語におけるランタイムエラーや予期せぬ状況が発生した際に、プログラムの通常の実行フローを中断させるメカニズムです。panicが呼び出されると、現在の関数の実行が停止し、その関数にdeferされた関数が実行されます。その後、呼び出し元の関数へとパニックが伝播し、同様にdeferされた関数が実行され、最終的にプログラムがクラッシュします。
    • recover: panicによって中断されたパニックシーケンスを捕捉し、プログラムの制御を回復させるために使用される組み込み関数です。recoverは**deferされた関数の中でのみ**有効に機能します。recoverdeferされた関数内で呼び出されると、パニックの値(panicに渡された引数)が返され、パニックシーケンスは停止し、プログラムはrecoverを呼び出したdefer文を含む関数の次のステートメントから通常の実行を再開します。deferされていない場所でrecoverを呼び出してもnilが返され、効果はありません。
  2. defer:

    • defer文は、そのdefer文を含む関数がリターンする直前(panicが発生した場合も含む)に、指定された関数呼び出しを実行することを保証します。複数のdefer文がある場合、それらはLIFO(Last-In, First-Out)の順序で実行されます。panicが発生した場合、deferされた関数は、スタックをアンワインドする過程で実行されます。
  3. 可変長引数関数(Variadic Functions):

    • Go言語では、引数の数が不定である関数を定義できます。これは、最後のパラメータの型名の前に...を付けることで実現されます(例: func sum(nums ...int))。関数内で可変長引数はスライスとして扱われます。例えば、nums ...intは関数内では[]int型のスライスとしてアクセスできます。
  4. gccgogc:

    • gc: これはGo言語の公式かつ標準のコンパイラであり、Goのソースコードをネイティブバイナリにコンパイルします。ほとんどのGo開発者が日常的に使用しているコンパイラです。
    • gccgo: これはGCC(GNU Compiler Collection)のフロントエンドとして実装されたGoコンパイラです。gcとは異なるコード生成バックエンドを使用しており、GCCがサポートする様々なアーキテクチャや最適化を利用できるという利点があります。しかし、異なる実装であるため、gcとは異なるバグや挙動の違いが発生することがあります。このコミットで修正された問題は、まさにgccgo特有のバグでした。

これらの概念がどのように相互作用するか、特にpanicが可変長引数関数内で発生し、その関数がdeferされ、さらにそのdeferされた関数内でrecoverが呼び出されるという複雑なシナリオが、このテストの焦点となっています。

技術的詳細

このコミットが追加するテストケースは、recoverが可変長引数関数内で正しく機能するかどうかを検証します。具体的には、deferされた可変長引数関数内でrecoverが呼び出された場合の挙動に焦点を当てています。

Go言語のpanic/recoverメカニズムは、スタックのアンワインド(unwinding)と密接に関連しています。panicが発生すると、現在の関数の実行が中断され、deferされた関数が実行されながら、呼び出しスタックを逆順に辿っていきます。この過程でrecoverが呼び出されると、パニックが捕捉され、スタックのアンワインドが停止し、プログラムの制御が回復します。

可変長引数関数は、その引数がスライスとして扱われるため、通常の固定引数関数とは異なる方法でスタックフレームが構築される可能性があります。特に、引数がスタック上にどのように配置され、deferされた関数がそのスタックフレームにどのようにアクセスするかは、コンパイラの実装に依存します。

gccgoにおける問題は、おそらく以下のいずれかのシナリオに関連していたと考えられます。

  1. スタックフレームの不整合: gccgoが可変長引数関数のスタックフレームを構築する際に、panic発生時のスタックアンワインド処理やrecoverが期待するスタック情報との間に不整合があった可能性があります。これにより、recoverがパニック値を正しく取得できなかったり、パニックシーケンスを適切に停止できなかったりしたかもしれません。
  2. 引数スライスのライフタイム: 可変長引数スライスは、関数が呼び出されたときに作成されます。panicが発生し、deferされた関数が実行される際に、このスライスのデータがまだ有効であるか、またはrecoverがそのデータに正しくアクセスできるかどうかに問題があった可能性も考えられます。
  3. レジスタとスタックの不一致: コンパイラはパフォーマンスのために引数をレジスタに配置することがありますが、panic発生時にはスタックに退避された情報が使用されることがあります。gccgoが可変長引数関数において、レジスタとスタックの間で引数の状態を正しく同期できていなかった可能性も考えられます。

このテストは、varargsという可変長引数関数を定義し、その中でrecover()を呼び出しています。そして、test8a関数ではpanic(0)を発生させ、test8b関数では通常のreturnを行います。どちらの関数もdefer varargs(...)を使ってvarargs関数を遅延実行しています。

  • test8aのケースでは、panicが発生するため、deferされたvarargs関数が実行され、その中でrecover()nilではない値を返すことを期待します。これにより、*s += 100が実行され、rの値が100 + (1+2+3) = 106になることを検証します。
  • test8bのケースでは、panicは発生しないため、deferされたvarargs関数内でrecover()nilを返します。これにより、*s += 100は実行されず、rの値が4+5+6 = 15になることを検証します。

これらのテストケースを通じて、gccgopanicrecover、そして可変長引数関数の組み合わせを正しく処理できるようになったことを確認します。

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

変更はtest/recover.goファイルに対して行われています。

--- a/test/recover.go
+++ b/test/recover.go
@@ -244,3 +244,30 @@ func test7() {
 		die()
 	}
 }
+
+func varargs(s *int, a ...int) {
+	*s = 0
+	for _, v := range a {
+		*s += v
+	}
+	if recover() != nil {
+		*s += 100
+	}
+}
+
+func test8a() (r int) {
+	defer varargs(&r, 1, 2, 3)
+	panic(0)
+}
+
+func test8b() (r int) {
+	defer varargs(&r, 4, 5, 6)
+	return
+}
+
+func test8() {
+	if test8a() != 106 || test8b() != 15 {
+		println("wrong value")
+		die()
+	}
+}

具体的には、以下の新しい関数が追加されています。

  • varargs(s *int, a ...int)
  • test8a() (r int)
  • test8b() (r int)
  • test8()

コアとなるコードの解説

追加された各関数の役割は以下の通りです。

  1. func varargs(s *int, a ...int):

    • この関数は可変長引数a ...intを受け取ります。これは関数内で[]int型のスライスとして扱われます。
    • s *intは、結果を格納するためのポインタです。
    • 関数内で、まず*s0に初期化し、次に可変長引数aの要素をすべて合計して*sに加算します。
    • 最も重要なのは、if recover() != nilのブロックです。ここでrecover()が呼び出されます。
      • もしこのvarargs関数がpanicによってdefer経由で呼び出された場合、recover()nilではないパニック値を返します。この場合、*s100が加算されます。
      • もしpanicが発生せずに通常の関数終了によってdefer経由で呼び出された場合、recover()nilを返します。この場合、*s100は加算されません。
    • この関数は、panicの有無によって*sの値が変化することを利用して、recoverの動作をテストします。
  2. func test8a() (r int):

    • この関数はr intという名前付き戻り値を持ちます。
    • defer varargs(&r, 1, 2, 3): この行は、test8a関数が終了する直前にvarargs関数を遅延実行するように設定します。varargsにはrのアドレスと、引数1, 2, 3が渡されます。
    • panic(0): この行で意図的にpanicを発生させます。これにより、test8aの実行は中断され、deferされたvarargs関数が実行されます。
    • panicが発生するため、varargs関数内のrecover()はパニックを捕捉し、rには1 + 2 + 3 + 100 = 106が設定されることを期待します。
  3. func test8b() (r int):

    • この関数もr intという名前付き戻り値を持ちます。
    • defer varargs(&r, 4, 5, 6): test8b関数が終了する直前にvarargs関数を遅延実行するように設定します。varargsにはrのアドレスと、引数4, 5, 6が渡されます。
    • return: この行で関数は正常に終了します。panicは発生しません。
    • panicが発生しないため、varargs関数内のrecover()nilを返し、rには4 + 5 + 6 = 15が設定されることを期待します。
  4. func test8():

    • この関数は、test8a()test8b()を呼び出し、それぞれの戻り値が期待通りであるかを検証します。
    • if test8a() != 106 || test8b() != 15: もしtest8a()106ではないか、またはtest8b()15ではない場合、テストは失敗と判断されます。
    • println("wrong value")die(): テストが失敗した場合にメッセージを出力し、プログラムを終了させます。

これらの関数が連携することで、可変長引数関数内でrecoverが呼び出された際のpanicの捕捉と値の回復が、gccgoを含むGoコンパイラで正しく行われることを厳密にテストしています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: panicrecoverに関するセクション
  • Go言語の公式ドキュメント: deferに関するセクション
  • Go言語の公式ドキュメント: 可変長引数関数に関するセクション
  • GCCGoプロジェクトのドキュメント(一般的な情報源として)
  • Go言語のテストスイートの構造と慣習に関する情報(一般的な情報源として)