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

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

このコミットは、Goランタイムにおけるdeferreturn関数の引数定義から可変引数(...)を削除する変更です。これにより、コンパイラがdeferreturnの引数フレームサイズを正確に認識できるようになり、スタックスキャナの動作が改善されます。

コミット

commit c792bde9eff10869a503910f9c14ea24047ecafa
Author: Keith Randall <khr@golang.org>
Date:   Mon Dec 2 13:07:15 2013 -0800

    runtime: don't use ... formal argument to deferreturn.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/28860043

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

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

元コミット内容

runtime: don't use ... formal argument to deferreturn.

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

変更の背景

Go言語のdeferステートメントは、関数がリターンする直前(またはパニックが発生した場合)に実行される関数呼び出しをスケジュールするために使用されます。このメカニズムは、リソースの解放(ファイルのクローズ、ロックの解除など)を確実に行うために非常に重要です。

deferされた関数が実際に実行される際には、ランタイム内の特別な関数であるdeferreturnが関与します。以前のdeferreturnの定義では、可変引数(...)が使用されていました。これは、コンパイラがこの関数の引数フレームサイズを計算しないようにするためのハックでした。コミットメッセージにあるように、「deferreturnは非常に特殊な関数であり、ランタイムがそのフレームサイズを要求した場合、それはトレースバックルーチンが壊れている可能性が高い」という考えがあったためです。

しかし、この可変引数の使用は、スタックスキャナ(ガベージコレクタがスタック上のポインタを識別するために使用するメカニズム)にとって問題を引き起こす可能性がありました。スタックスキャナは、関数のフレームレイアウトに関する正確な情報に依存して、どのメモリ領域がポインタを含んでいるかを判断します。可変引数を使用すると、コンパイラが正確なフレームサイズを決定できないため、スタックスキャナが誤動作するリスクがありました。

このコミットの目的は、deferreturnの引数定義を修正し、コンパイラがそのフレームサイズを正しく計算できるようにすることで、スタックスキャナの健全性を確保することにあります。これにより、Goランタイムの安定性と信頼性が向上します。

前提知識の解説

Goのdeferステートメント

deferステートメントは、Go言語のユニークな機能の一つで、関数が終了する直前に実行される関数呼び出しをスケジュールします。deferされた関数はLIFO(後入れ先出し)の順序で実行されます。

例:

func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close() // 関数終了時にf.Close()が呼ばれることを保証

    data, err := ioutil.ReadAll(f)
    if err != nil {
        return nil, err
    }
    return data, nil
}

Goランタイムとガベージコレクション

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、スケジューラ、メモリ管理(ガベージコレクタを含む)、プリミティブな同期メカニズムなどが含まれます。

Goのガベージコレクタは、到達可能なオブジェクトを特定し、到達不能なオブジェクトが占めるメモリを再利用します。このプロセスの一部として、ガベージコレクタは実行中のゴルーチンのスタックをスキャンし、スタック上に存在するポインタを識別する必要があります。これにより、ヒープ上のオブジェクトへの参照が正しく追跡され、誤って回収されることがなくなります。

スタックスキャナとフレームポインタ

スタックスキャナがスタックを正確にスキャンするためには、各関数のスタックフレームのレイアウト(引数、ローカル変数、リターンアドレスなどがどこに配置されているか)に関する情報が必要です。この情報は、コンパイラによって生成され、ランタイムが利用できるようにメタデータとして埋め込まれます。

特に、関数が引数を受け取る場合、それらの引数はスタックフレームの一部として配置されます。コンパイラが関数の引数リストを完全に把握している場合、その引数フレームのサイズを正確に計算できます。しかし、可変引数(...)を使用すると、コンパイラはコンパイル時に引数の数を特定できないため、フレームサイズを固定的に決定することが困難になります。

uintptr

uintptrは、ポインタを保持するのに十分な大きさの符号なし整数型です。これは、ポインタ演算や、ポインタを整数として扱う必要がある低レベルのランタイムコードでよく使用されます。

#pragma textflag NOSPLIT

これはGoコンパイラに対するディレクティブで、特定の関数がスタックを分割しないように指示します。Goの関数は通常、必要に応じてスタックを自動的に拡張できますが、NOSPLITが指定された関数は、スタックの拡張を試みません。これは、ランタイムの非常に低レベルな部分や、スタックの拡張が不適切な状況(例えば、スタックの拡張自体がスタックを必要とする場合など)で使用されます。deferreturnのような関数は、スタックの整合性を維持するためにNOSPLITが適用されることがあります。

技術的詳細

このコミットの核心は、deferreturn関数のシグネチャから可変引数(...)を削除し、代わりに明示的なuintptr arg0という単一の引数を指定することです。

変更前: runtime·deferreturn(uintptr arg0, ...)

変更後: runtime·deferreturn(uintptr arg0)

この変更がもたらす技術的な影響は以下の通りです。

  1. コンパイラによるフレームサイズ計算の正確化: 可変引数を使用すると、コンパイラは関数の引数フレームの正確なサイズをコンパイル時に決定できませんでした。これは、スタックスキャナがスタック上のポインタを正確に識別するために必要な情報が不足することを意味しました。...を削除し、単一のuintptr引数を明示的に指定することで、コンパイラはdeferreturnの引数フレームサイズを正確に計算できるようになります。

  2. スタックスキャナの健全性: ガベージコレクタのスタックスキャナは、スタックフレームのレイアウト情報に基づいて、スタック上のどの領域がポインタを含んでいるかを判断します。フレームサイズが不明確だと、スキャナが誤ったメモリ領域をスキャンしたり、ポインタを見落としたりする可能性がありました。この変更により、deferreturnのスタックフレームに関する正確な情報がコンパイラから提供されるため、スタックスキャナはより確実に動作し、ガベージコレクションの正確性が向上します。

  3. src/cmd/gc/pgen.cにおけるmaxargの調整: src/cmd/gc/pgen.cはGoコンパイラのコード生成部分です。compile関数内で、hasdefer(deferが存在するかどうか)が真の場合、ginscall(deferreturn, 0)が呼び出されます。この変更に伴い、deferreturnuintptr引数を1つ持つと「見せかける」ようになったため、コンパイラはdeferreturn呼び出しのために必要な最大引数サイズ(maxarg)を調整する必要があります。具体的には、maxargwidthptr(ポインタの幅、通常は4バイトまたは8バイト)よりも小さい場合、maxargwidthptrに設定することで、スタックスキャナがdeferreturnの引数領域を正しく認識できるようにします。これは、deferreturnが実際に引数を使用しない場合でも、スタック上のその領域がポインタを含む可能性があるとスタックスキャナに「伝える」ための措置です。

  4. deferreturnの引数の役割の明確化: コミットメッセージのコメントで、「単一の引数は実際には使用されない - それは保留中のdeferと一致させるためにそのアドレスが取られるだけである」と説明されています。これは、arg0deferのコンテキストを識別するためのマーカーとして機能し、実際のデータ転送には使われないことを示唆しています。この変更により、deferreturnの引数の意図がより明確になります。

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

src/cmd/gc/pgen.c

--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -168,8 +168,13 @@ compile(Node *fn)
 		if(retpc)
 			patch(retpc, pc);
 		ginit();
-		if(hasdefer)
+		if(hasdefer) {
 			ginscall(deferreturn, 0);
+			// deferreturn pretends to have one uintptr argument.
+			// Reserve space for it so stack scanner is happy.
+			if(maxarg < widthptr)
+				maxarg = widthptr;
+		}
 		if(curfn->exit)
 			genlist(curfn->exit);
 		gclean();

src/pkg/runtime/panic.c

--- a/src/pkg/runtime/panic.c
+++ b/src/pkg/runtime/panic.c
@@ -157,14 +157,12 @@ runtime·deferproc(int32 siz, FuncVal *fn, ...)
 // is called again and again until there are no more deferred functions.
 // Cannot split the stack because we reuse the caller's frame to
 // call the deferred function.
-//
-// The ... in the prototype keeps the compiler from declaring
-// an argument frame size. deferreturn is a very special function,
-// and if the runtime ever asks for its frame size, that means
-// the traceback routines are probably broken.
+\
+// The single argument isn't actually used - it just has its address
+// taken so it can be matched against pending defers.
 #pragma textflag NOSPLIT
 void
-runtime·deferreturn(uintptr arg0, ...)
+runtime·deferreturn(uintptr arg0)
 {
 	Defer *d;
 	byte *argp;

コアとなるコードの解説

src/cmd/gc/pgen.cの変更

このファイルはGoコンパイラのコード生成フェーズの一部です。compile関数は、Goの関数をコンパイルする際の主要なエントリポイントの一つです。

  • 変更前: if(hasdefer) ginscall(deferreturn, 0); deferが存在する場合、deferreturn関数を呼び出します。この際、コンパイラはdeferreturnのシグネチャが可変引数であるため、その引数フレームサイズを正確に把握していませんでした。

  • 変更後:

    if(hasdefer) {
        ginscall(deferreturn, 0);
        // deferreturn pretends to have one uintptr argument.
        // Reserve space for it so stack scanner is happy.
        if(maxarg < widthptr)
            maxarg = widthptr;
    }
    

    deferreturnが単一のuintptr引数を持つと「見せかける」ようになったため、コンパイラはdeferreturn呼び出しのために必要な最大引数サイズ(maxarg)を調整します。widthptrはポインタのサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)を表します。このコードは、deferreturnが少なくともwidthptr分の引数スペースを必要とすることを保証し、スタックスキャナがその領域を正しくスキャンできるようにします。これは、deferreturnが実際にその引数を使用しない場合でも、スタック上のその領域がポインタを含む可能性があるとスタックスキャナに「伝える」ための重要なステップです。

src/pkg/runtime/panic.cの変更

このファイルはGoランタイムのパニックおよびdefer関連の処理を扱います。

  • 変更前:

    void
    runtime·deferreturn(uintptr arg0, ...)
    {
        // ...
    }
    

    deferreturn関数は、uintptr arg0と可変引数...を受け取るように定義されていました。コメントには、「プロトタイプ内の...は、コンパイラが引数フレームサイズを宣言するのを防ぐ」と明記されており、これが意図的なハックであったことがわかります。

  • 変更後:

    void
    runtime·deferreturn(uintptr arg0)
    {
        // ...
    }
    

    deferreturn関数のシグネチャから可変引数...が削除され、単一のuintptr arg0のみを受け取るようになりました。これにより、コンパイラはdeferreturnの引数フレームサイズを正確に計算できるようになります。新しいコメントは、「単一の引数は実際には使用されない - それは保留中のdeferと一致させるためにそのアドレスが取られるだけである」と説明しており、arg0deferのコンテキストを識別するためのマーカーとして機能することを示唆しています。#pragma textflag NOSPLITは変更されておらず、この関数がスタックを分割しないという特性は維持されています。

関連リンク

  • Go言語のdeferステートメントに関する公式ドキュメントやチュートリアル
  • Goのガベージコレクションの仕組みに関する詳細な解説
  • Goコンパイラの内部構造、特にコード生成フェーズに関する資料

参考にした情報源リンク

  • Go言語の公式ドキュメント: https://golang.org/doc/
  • Goのソースコード(特にsrc/cmd/gcsrc/pkg/runtimeディレクトリ)
  • Goのガベージコレクションに関するブログ記事や論文(例: "Go's new GC: less latency, more throughput" by Rick Hudson and Austin Clements)
  • Goのコンパイラとランタイムに関する技術ブログやカンファレンストーク
  • このコミットの変更リスト: https://golang.org/cl/28860043 (注: このリンクは古いGo Code ReviewのURLであり、現在はGitHubのコミットページにリダイレクトされるか、アクセスできない可能性があります。しかし、コミットメッセージに記載されているため、参考情報として含めました。)
  • Goのuintptr型に関する解説
  • Goの#pragmaディレクティブに関する情報