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

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

このコミットは、Goランタイムにおけるnildefer関数がスタックコピアによって適切に処理されることを保証するためのテストを追加します。具体的には、deferされた関数がnilである場合に、ランタイムがクラッシュするのではなく、予期されたパニックを発生させることを検証します。これは、以前に修正されたバグ(Issue 8047)に対する回帰テストとして機能します。

コミット

commit aa04caa7594506d805f82b7d7abed35a3a8fbec4
Author: Keith Randall <khr@golang.org>
Date:   Wed Jun 11 20:34:46 2014 -0400

    runtime: add test for issue 8047.
    
    Make sure stack copier doesn't barf on a nil defer.
    Bug was fixed in https://golang.org/cl/101800043
    This change just adds a test.
    
    Fixes #8047
    
    LGTM=dvyukov, rsc
    R=dvyukov, rsc
    CC=golang-codereviews
    https://golang.org/cl/108840043

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

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

元コミット内容

runtime: add test for issue 8047.

Make sure stack copier doesn't barf on a nil defer.
Bug was fixed in https://golang.org/cl/101800043
This change just adds a test.

Fixes #8047

LGTM=dvyukov, rsc
R=dvyukov, rsc
CC=golang-codereviews
https://golang.org/cl/108840043

変更の背景

このコミットは、Goランタイムの以前のバグ(Issue 8047)に対するテストを追加するものです。Issue 8047は、「runtime: how should a nil go or nil defer behave?」(nilgoまたはdeferはどのように振る舞うべきか?)という問題提起から始まりました。これに関連して、Issue 8045「runtime: go of nil func value crashes」(nilの関数値をgoするとクラッシュする)という具体的な問題も存在しました。

これらの問題の核心は、nilの関数ポインタをgoルーチンとして起動したり、deferしたりした場合に、ランタイムがクラッシュ(致命的なエラー)するのか、それともパニック(回復可能なエラー)を発生させるべきかという点でした。Goの設計思想では、予期せぬエラーはパニックとして処理され、recoverによって捕捉されることが期待されます。しかし、特定の条件下では、nilの関数をgoまたはdeferすると、ランタイムがクラッシュし、プログラムが強制終了してしまうという問題がありました。

このバグは、https://golang.org/cl/101800043 で修正されたとコミットメッセージに記載されています。この修正により、nildefer関数がスタックコピアによって処理される際に、クラッシュではなくパニックが発生するようになりました。このコミットは、その修正が将来の変更によって回帰しないことを保証するために、専用のテストケースを追加するものです。

前提知識の解説

Goのdefer

defer文は、関数がリターンする直前(またはパニックが発生して関数が終了する直前)に実行される関数呼び出しをスケジュールするために使用されます。deferされた関数はLIFO(後入れ先出し)の順序で実行されます。これは、リソースの解放(ファイルのクローズ、ロックの解除など)や、パニックの捕捉(recoverと組み合わせて)によく利用されます。

例:

func readFile(filename string) {
    f, err := os.Open(filename)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close() // 関数が終了する際にf.Close()が実行される
    // ファイルの読み込み処理
}

Goのスタックコピアとスタック管理

Goのgoroutineは、可変サイズのスタックを持っています。これは、必要に応じてスタックが自動的に拡大・縮小されることを意味します。スタックが拡大する必要がある場合、Goランタイムは現在のスタックの内容をより大きな新しいスタック領域にコピーします。このプロセスは「スタックコピア」によって行われます。

スタックコピアは、スタック上のすべてのデータ(ローカル変数、関数引数、リターンアドレスなど)を正確にコピーする必要があります。これには、deferされた関数に関する情報も含まれます。もしスタックコピアがdeferされたnil関数を適切に処理できない場合、スタックコピー中に不正なメモリアクセスが発生し、ランタイムがクラッシュする可能性があります。

nil関数とパニック

Goでは、関数型の変数はnilになることがあります。nilの関数を直接呼び出そうとすると、ランタイムパニックが発生します。

var f func()
f() // panic: call of nil func

この挙動は、プログラマが予期せぬnil関数呼び出しを検出し、recoverメカニズムを通じて処理できるようにするために重要です。Issue 8047/8045の背景にあった問題は、godeferのコンテキストでnil関数が使用された場合に、この通常のパニック挙動ではなく、より深刻なクラッシュが発生していた点にありました。

技術的詳細

このコミットが追加するテストは、Goランタイムがnildefer関数を処理する際の堅牢性を検証します。問題の核心は、defer ((func())(nil))() のようなコードが実行されたときに、スタックコピアがこのnildeferエントリをどのように扱うかでした。

Goのスタックコピアは、スタックフレームを走査し、各フレームに関連付けられたdeferレコードを処理します。deferレコードには、deferされた関数のポインタが含まれています。もしこのポインタがnilである場合、スタックコピアはそれを安全にスキップするか、あるいはパニックをトリガーするような方法で処理する必要があります。以前のバグでは、nilの関数ポインタがスタックコピアによって不正にアクセスされ、クラッシュを引き起こしていた可能性があります。

修正されたランタイムでは、defer ((func())(nil))() が実行されると、deferリストにnil関数が追加されます。その後、関数がリターンする際にdeferされた関数が実行されますが、このnil関数が呼び出される直前にランタイムがパニックを発生させるようになります。このパニックは、テストコード内でrecover()によって捕捉され、期待通りの挙動であることを確認します。

テストのstackit(1000)呼び出しは、スタックを深くすることで、スタックコピアが動作する可能性を高めます。これにより、スタックが拡大される際にnildeferエントリがコピーされるシナリオをシミュレートし、その過程で問題が発生しないことを確認します。

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

このコミットは、既存のコードを変更するものではなく、新しいテストファイル test/fixedbugs/issue8047.go を追加するものです。

--- /dev/null
+++ test/fixedbugs/issue8047.go
@@ -0,0 +1,29 @@
+// run
+
+// Copyright 2014 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.
+
+// Issue 8047.  Stack copier shouldn't crash if there
+// is a nil defer.
+
+package main
+
+func stackit(n int) {
+	if n == 0 {
+		return
+	}
+	stackit(n - 1)
+}
+
+func main() {
+	defer func() {
+		// catch & ignore panic from nil defer below
+		err := recover()
+		if err == nil {
+			panic("defer of nil func didn't panic")
+		}
+	}()
+	defer ((func())(nil))()
+	stackit(1000)
+}

コアとなるコードの解説

追加されたテストファイル test/fixedbugs/issue8047.go は、以下の主要な部分で構成されています。

  1. // run ディレクティブ: このファイルがGoのテストスイートによって実行されるべきテストであることを示します。
  2. 著作権表示とライセンス: Goプロジェクトの標準的なヘッダです。
  3. コメント: // Issue 8047. Stack copier shouldn't crash if there // is a nil defer. は、このテストの目的を明確に示しています。
  4. package main: 実行可能なプログラムであることを示します。
  5. func stackit(n int):
    • この関数は再帰的に自身を呼び出し、スタックを深くするために使用されます。
    • n0になるまで再帰を続けます。
    • main関数内でstackit(1000)として呼び出され、深いスタックを生成し、スタックコピアが動作する可能性を高めます。
  6. func main():
    • 外側のdeferrecover:
      defer func() {
          err := recover()
          if err == nil {
              panic("defer of nil func didn't panic")
          }
      }()
      
      このdeferブロックは、その後に続くnildeferがパニックを発生させることを期待して、そのパニックを捕捉するために設置されています。もしnildeferがパニックを発生させなかった場合(つまり、以前のバグのようにクラッシュした場合や、何も起こらなかった場合)、このrecover()nilを返し、その結果、このテスト自体が「defer of nil func didn't panic」というメッセージでパニックし、テスト失敗となります。これにより、期待されるパニック挙動が保証されます。
    • 問題のnil defer:
      defer ((func())(nil))()
      
      これがテストの核心となる行です。func() (nil) は、nil値を持つ関数型の変数を表します。このnil関数をdeferすることで、ランタイムがnildeferエントリをどのように処理するかをテストします。修正が適用されていれば、このdeferは関数終了時にパニックを発生させるはずです。
    • stackit(1000): 前述の通り、スタックを深くし、スタックコピアが動作するシナリオを作り出します。これにより、スタックコピアがnildeferエントリをコピーする際に問題が発生しないことを確認します。

このテストは、nildeferがランタイムをクラッシュさせることなく、適切にパニックを発生させるというGoランタイムの修正された挙動を効果的に検証しています。

関連リンク

参考にした情報源リンク

  • Go Issue Tracker (GitHub)
  • Go Change List (Gerrit)
  • Go言語のdefer文に関する公式ドキュメントやチュートリアル
  • Goランタイムのスタック管理に関する技術記事やGoのソースコード分析