[インデックス 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)
この変更がもたらす技術的な影響は以下の通りです。
-
コンパイラによるフレームサイズ計算の正確化: 可変引数を使用すると、コンパイラは関数の引数フレームの正確なサイズをコンパイル時に決定できませんでした。これは、スタックスキャナがスタック上のポインタを正確に識別するために必要な情報が不足することを意味しました。
...
を削除し、単一のuintptr
引数を明示的に指定することで、コンパイラはdeferreturn
の引数フレームサイズを正確に計算できるようになります。 -
スタックスキャナの健全性: ガベージコレクタのスタックスキャナは、スタックフレームのレイアウト情報に基づいて、スタック上のどの領域がポインタを含んでいるかを判断します。フレームサイズが不明確だと、スキャナが誤ったメモリ領域をスキャンしたり、ポインタを見落としたりする可能性がありました。この変更により、
deferreturn
のスタックフレームに関する正確な情報がコンパイラから提供されるため、スタックスキャナはより確実に動作し、ガベージコレクションの正確性が向上します。 -
src/cmd/gc/pgen.c
におけるmaxarg
の調整:src/cmd/gc/pgen.c
はGoコンパイラのコード生成部分です。compile
関数内で、hasdefer
(deferが存在するかどうか)が真の場合、ginscall(deferreturn, 0)
が呼び出されます。この変更に伴い、deferreturn
がuintptr
引数を1つ持つと「見せかける」ようになったため、コンパイラはdeferreturn
呼び出しのために必要な最大引数サイズ(maxarg
)を調整する必要があります。具体的には、maxarg
がwidthptr
(ポインタの幅、通常は4バイトまたは8バイト)よりも小さい場合、maxarg
をwidthptr
に設定することで、スタックスキャナがdeferreturn
の引数領域を正しく認識できるようにします。これは、deferreturn
が実際に引数を使用しない場合でも、スタック上のその領域がポインタを含む可能性があるとスタックスキャナに「伝える」ための措置です。 -
deferreturn
の引数の役割の明確化: コミットメッセージのコメントで、「単一の引数は実際には使用されない - それは保留中のdefer
と一致させるためにそのアドレスが取られるだけである」と説明されています。これは、arg0
がdefer
のコンテキストを識別するためのマーカーとして機能し、実際のデータ転送には使われないことを示唆しています。この変更により、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
と一致させるためにそのアドレスが取られるだけである」と説明しており、arg0
がdefer
のコンテキストを識別するためのマーカーとして機能することを示唆しています。#pragma textflag NOSPLIT
は変更されておらず、この関数がスタックを分割しないという特性は維持されています。
関連リンク
- Go言語の
defer
ステートメントに関する公式ドキュメントやチュートリアル - Goのガベージコレクションの仕組みに関する詳細な解説
- Goコンパイラの内部構造、特にコード生成フェーズに関する資料
参考にした情報源リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goのソースコード(特に
src/cmd/gc
とsrc/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
ディレクティブに関する情報