[インデックス 18739] ファイルの概要
このコミットは、Go言語のランタイムおよびリンカ (cmd/ld
) に関連する変更を導入しています。具体的には、非クロージャ関数におけるコンテキストレジスタの扱いを改善し、ガベージコレクション(GC)によるメモリリークの可能性を低減することを目的としています。
コミット
commit c2dd33a46f66b2b56987ff9849f64513a4323385
Author: Russ Cox <rsc@golang.org>
Date: Tue Mar 4 13:53:08 2014 -0500
cmd/ld: clear unused ctxt before morestack
For non-closure functions, the context register is uninitialized
on entry and will not be used, but morestack saves it and then the
garbage collector treats it as live. This can be a source of memory
leaks if the context register points at otherwise dead memory.
Avoid this by introducing a parallel set of morestack functions
that clear the context register, and use those for the non-closure functions.
I hope this will help with some of the finalizer flakiness, but it probably won't.
Fixes #7244.
LGTM=dvyukov
R=khr, dvyukov
CC=golang-codereviews
https://golang.org/cl/71030044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c2dd33a46f66b2b56987ff9849f64513a4323385
元コミット内容
cmd/ld: clear unused ctxt before morestack
非クロージャ関数において、morestack
呼び出し前に未使用のコンテキストレジスタをクリアする。
非クロージャ関数では、エントリ時にコンテキストレジスタが未初期化であり、使用されない。しかし、morestack
はこれを保存し、ガベージコレクタがこれを「生きている」ものとして扱う。これにより、コンテキストレジスタが本来は到達不能なメモリを指している場合、メモリリークの原因となる可能性がある。この問題を回避するため、コンテキストレジスタをクリアする並列のmorestack
関数群を導入し、非クロージャ関数にはこれらを使用する。
この変更がファイナライザの不安定性(flakiness)の改善に役立つことを期待するが、おそらくそうはならないだろう。
Fixes #7244.
変更の背景
Goランタイムにおけるスタック管理とガベージコレクションの相互作用に起因する潜在的なメモリリークが問題となっていました。特に、クロージャではない通常の関数が呼び出される際、Goの関数呼び出し規約では、コンテキストレジスタ(通常、クロージャの環境ポインタやメソッドのレシーバを保持するために使用されるレジスタ)が初期化されずに残ることがあります。
Goのランタイムは、関数がスタックを使い果たしそうになったときに、morestack
という特別な関数を呼び出してスタックを拡張します。このmorestack
関数は、呼び出し元の関数のレジスタ状態を保存します。この保存処理には、コンテキストレジスタも含まれます。
問題は、非クロージャ関数ではコンテキストレジスタが意味のある値を保持していないにもかかわらず、morestack
がその内容を保存し、その結果、ガベージコレクタがそのレジスタが指すメモリ領域を「生きている」と誤って判断してしまう可能性があったことです。もしこのレジスタが、本来は解放されるべきだが、たまたま以前の実行で使われた古い、しかし現在は不要なメモリ領域を指していた場合、GCはそのメモリを回収できず、結果としてメモリリークが発生します。
このコミットは、この潜在的なメモリリークを防ぐために、非クロージャ関数がmorestack
を呼び出す前に、コンテキストレジスタを明示的にゼロクリアするという対策を導入しています。これにより、GCが誤って不要なメモリを保持し続けることを防ぎます。コミットメッセージにある「ファイナライザの不安定性」への言及は、このようなGCの誤動作が、ファイナライザの実行タイミングや挙動に影響を与え、テストの不安定性として現れる可能性があったことを示唆しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の内部動作に関する知識が必要です。
-
Goの関数呼び出し規約とレジスタ使用:
- Goの関数は、特定のレジスタを特定の目的のために使用します。例えば、一部のレジスタは引数渡しや戻り値、あるいは特別なランタイム情報(
g
ポインタなど)のために予約されています。 - コンテキストレジスタ (Context Register): Goでは、クロージャ(匿名関数)が外部スコープの変数をキャプチャする際に、その環境へのポインタを保持するために特別なレジスタ(アーキテクチャによって異なる)を使用します。また、メソッド呼び出しにおけるレシーバもこのレジスタを通じて渡されることがあります。非クロージャ関数や通常の関数では、このレジスタは通常使用されません。
- Goの関数は、特定のレジスタを特定の目的のために使用します。例えば、一部のレジスタは引数渡しや戻り値、あるいは特別なランタイム情報(
-
スタック管理と
morestack
:- Goのランタイムは、動的にスタックを拡張するメカニズムを持っています。関数が呼び出され、そのスタックフレームが現在のスタックの残りの容量を超えそうになると、コンパイラによって挿入されたプロローグコードが
runtime.morestack
関数を呼び出します。 morestack
は、新しい、より大きなスタックセグメントを割り当て、現在のスタックの内容を新しいセグメントにコピーし、実行を継続します。このプロセス中に、呼び出し元の関数のレジスタ状態(プログラムカウンタ、スタックポインタ、汎用レジスタなど)が保存され、復元されます。
- Goのランタイムは、動的にスタックを拡張するメカニズムを持っています。関数が呼び出され、そのスタックフレームが現在のスタックの残りの容量を超えそうになると、コンパイラによって挿入されたプロローグコードが
-
ガベージコレクション (GC):
- Goはトレース型ガベージコレクタを使用しています。これは、プログラムが現在アクセス可能なすべてのメモリ(「生きている」オブジェクト)を特定し、それ以外のメモリ(「死んでいる」オブジェクト)を回収する方式です。
- GCは、プログラムのルート(グローバル変数、現在のスタック上のレジスタやスタックフレーム内の変数など)から到達可能なすべてのポインタをたどることで、生きているオブジェクトを識別します。
- ポインタスキャン: GCは、スタックやヒープ上のメモリ領域をスキャンし、ポインタらしき値を見つけると、それが有効なオブジェクトを指しているかどうかを判断します。レジスタもGCスキャンの対象となります。
-
メモリリーク:
- 本来は不要になったメモリが、GCによって「生きている」と誤って判断され、回収されずに残り続ける状態を指します。これにより、プログラムが使用するメモリ量が時間とともに増加し、パフォーマンスの低下やシステムリソースの枯渇につながる可能性があります。
これらの概念が組み合わさることで、非クロージャ関数で未使用のコンテキストレジスタがGCに誤認識され、メモリリークを引き起こすという問題が発生していました。
技術的詳細
このコミットの技術的な核心は、Goのコンパイラ(cmd/gc
)、リンカ(cmd/ld
)、およびランタイム(src/pkg/runtime
)が連携して、非クロージャ関数のmorestack
呼び出しパスを最適化し、コンテキストレジスタの不要な保持を防ぐ点にあります。
-
needctxt
フラグの導入:src/cmd/gc/go.h
のNode
構造体にuchar needctxt
という新しいフィールドが追加されました。これは、その関数がコンテキストレジスタ(クロージャ変数を持つか、部分適用された関数呼び出しであるかなど)を必要とするかどうかを示すフラグです。src/cmd/gc/closure.c
では、クロージャや部分適用された関数を生成する際に、このneedctxt
フラグが適切に設定されるようになりました。src/cmd/gc/pgen.c
では、コンパイル時にこのneedctxt
フラグが、リンカに渡される関数のテキストフラグ(TEXTFLAG
)にNEEDCTXT
として反映されるようになりました。
-
NEEDCTXT
テキストフラグの定義:src/cmd/ld/textflag.h
に新しいテキストフラグ#define NEEDCTXT 64
が追加されました。これは、リンカが関数の特性を識別するために使用します。
-
morestack_noctxt
関数の導入:- 各アーキテクチャ(386, amd64, arm)のランタイムアセンブリファイル(
src/pkg/runtime/asm_386.s
,src/pkg/runtime/asm_amd64.s
,src/pkg/runtime/asm_amd64p32.s
,src/pkg/runtime/asm_arm.s
)に、既存のruntime.morestack
関数群に対応するruntime.morestack_noctxt
関数群が追加されました。 - これらの
_noctxt
関数は非常にシンプルで、コンテキストレジスタ(386/amd64ではDX
、ARMではR7
)をゼロクリアしてから、対応する通常のruntime.morestack
関数にジャンプします。これにより、morestack
がレジスタを保存する際に、コンテキストレジスタには常にゼロが格納されるようになります。
- 各アーキテクチャ(386, amd64, arm)のランタイムアセンブリファイル(
-
リンカの変更:
- リンカ(
src/liblink/obj5.c
,src/liblink/obj6.c
,src/liblink/obj8.c
)は、関数のTEXTFLAG
にNEEDCTXT
が設定されているかどうかをチェックするようになりました。 addstacksplit
関数(スタック分割チェックコードを挿入する部分)内で、cursym->text->from.scale & NEEDCTXT
のチェックが行われます。NEEDCTXT
が設定されていない(つまり、非クロージャ関数である)場合、リンカは通常のmorestack
ではなく、新しく導入されたmorestack_noctxt
関数への呼び出しを生成します。include/link.h
のLink
構造体にあるsymmorestack
配列のサイズが10
から20
に拡張され、通常のmorestack
シンボルとmorestack_noctxt
シンボルを両方格納できるようになりました。
- リンカ(
これらの変更により、非クロージャ関数がスタック拡張を必要とする場合、コンテキストレジスタが明示的にクリアされた状態でmorestack
が呼び出されるため、ガベージコレクタが不要なメモリを「生きている」と誤認識する可能性がなくなります。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
include/link.h
: リンカの内部構造体Link
に、symmorestack
配列のサイズ変更(10
から20
へ)が行われました。これは、通常のmorestack
シンボルとmorestack_noctxt
シンボルの両方を格納するためです。src/cmd/gc/go.h
:Node
構造体にuchar needctxt;
フィールドが追加されました。これは、関数がコンテキストレジスタを必要とするかどうかを示すフラグです。src/cmd/gc/closure.c
: クロージャや部分適用された関数を生成するロジックに、xfunc->needctxt = ...
という行が追加され、needctxt
フラグが適切に設定されるようになりました。src/cmd/gc/pgen.c
: コンパイル時に、関数のneedctxt
フラグがTEXTFLAG
にNEEDCTXT
として設定されるようになりました。src/cmd/ld/textflag.h
: 新しいテキストフラグ#define NEEDCTXT 64
が定義されました。src/liblink/obj*.c
(obj5.c, obj6.c, obj8.c):stacksplit
関数のシグネチャにint noctxt
引数が追加されました。addstacksplit
関数内で、cursym->text->from.scale & NEEDCTXT
をチェックし、noctxt
引数にその結果(非クロージャ関数であれば1
、そうでなければ0
)を渡すようになりました。stacksplit
関数内で、ctxt->symmorestack
配列から呼び出すmorestack
シンボルを選択する際に、このnoctxt
引数を使用するようになりました(例:ctxt->symmorestack[noctxt]
やctxt->symmorestack[i*2+noctxt]
)。ctxt->symmorestack
配列の初期化時に、runtime.morestack_noctxt
シンボルもルックアップして格納するようになりました。
src/pkg/runtime/asm_*.s
(asm_386.s, asm_amd64.s, asm_amd64p32.s, asm_arm.s):- 各アーキテクチャのランタイムアセンブリファイルに、
runtime.morestack_noctxt
(およびamd64
ではmorestackXX_noctxt
のバリエーション)という新しいアセンブリ関数が追加されました。これらの関数は、対応するコンテキストレジスタをゼロクリアしてから、通常のruntime.morestack
関数にジャンプします。
- 各アーキテクチャのランタイムアセンブリファイルに、
コアとなるコードの解説
src/cmd/gc/go.h
および src/cmd/gc/closure.c
, src/cmd/gc/pgen.c
Node
構造体に追加されたneedctxt
は、コンパイラが特定の関数がコンテキストレジスタを必要とするかどうかを追跡するためのメタデータです。クロージャや部分適用された関数は、その性質上、外部環境への参照(コンテキスト)を必要とするため、needctxt
が1
に設定されます。通常の関数では0
のままです。
このneedctxt
情報は、最終的にリンカに渡される関数のTEXTFLAG
にNEEDCTXT
として埋め込まれます。これにより、リンカは各関数がコンテキストレジスタを必要とするかどうかを判断できます。
src/cmd/ld/textflag.h
#define NEEDCTXT 64
は、リンカが関数の特性を識別するためのビットフラグです。このフラグがセットされていれば、その関数はコンテキストレジスタを使用します。
src/liblink/obj*.c
リンカの役割は、コンパイラが生成したオブジェクトコードを結合し、実行可能ファイルを生成することです。この過程で、リンカはスタック拡張のためのmorestack
呼び出しを挿入します。
変更の核心は、stacksplit
関数(スタック分割チェックコードを生成する)とaddstacksplit
関数(スタック分割チェックを関数プロローグに追加する)にあります。
addstacksplit
は、現在の関数(cursym
)のTEXTFLAG
からNEEDCTXT
フラグを読み取り、その情報(!(cursym->text->from.scale & NEEDCTXT)
、つまりNEEDCTXT
がセットされていない場合はtrue
、セットされている場合はfalse
)をstacksplit
関数にnoctxt
引数として渡します。stacksplit
関数は、このnoctxt
引数に基づいて、呼び出すmorestack
シンボルを決定します。noctxt
がtrue
(非クロージャ関数)であれば、runtime.morestack_noctxt
のような_noctxt
サフィックスを持つシンボルを選択します。noctxt
がfalse
(クロージャ関数など)であれば、通常のruntime.morestack
シンボルを選択します。Link
構造体のsymmorestack
配列は、これらのmorestack
シンボルへのポインタを保持するために拡張されました。
src/pkg/runtime/asm_*.s
これらのアセンブリファイルは、Goランタイムの低レベルな部分、特にスタック管理やスケジューリングに関連するコードを実装しています。
新しく追加されたruntime.morestack_noctxt
関数群は、このコミットの最終的な目的を達成する部分です。例えば、src/pkg/runtime/asm_amd64.s
のTEXT runtime·morestack00_noctxt(SB),NOSPLIT,$0
は、DX
レジスタ(amd64アーキテクチャにおけるコンテキストレジスタの一つ)を0
にクリアしてから、元のruntime.morestack00
関数にジャンプします。
TEXT runtime·morestack00_noctxt(SB),NOSPLIT,$0
MOVL $0, DX // DXレジスタをゼロクリア
JMP runtime·morestack00(SB) // 通常のmorestack関数へジャンプ
これにより、非クロージャ関数がスタック拡張を必要とする際に、コンテキストレジスタが確実にゼロに設定され、ガベージコレクタがそのレジスタをスキャンしても、有効なポインタとして誤認識することがなくなります。
関連リンク
- Go Issue #7244: cmd/ld: clear unused ctxt before morestack
- Go Code Review CL 71030044: cmd/ld: clear unused ctxt before morestack
参考にした情報源リンク
- 上記のGitHub IssueとGo Code Reviewのリンクは、このコミットの背景と議論を理解する上で直接的な情報源となります。
- Go言語のランタイム、コンパイラ、リンカの内部動作に関する一般的なドキュメントやブログ記事(例: Goのスタック管理、GCの仕組み、関数呼び出し規約など)も、前提知識の理解に役立ちます。
- A Tour of Go's Runtime: https://go.dev/blog/go-runtime-tour (一般的なGoランタイムの概要)
- The Go Programming Language Specification: https://go.dev/ref/spec (Go言語の公式仕様、関数呼び出し規約など)
- Go's Garbage Collector: https://go.dev/blog/go15gc (GoのGCに関する情報)
- Go Assembly Language: https://go.dev/doc/asm (Goのアセンブリ言語に関する情報)
- Go Compiler Internals: https://go.dev/doc/compiler (Goコンパイラの内部に関する情報)
- Go Linker Internals: https://go.dev/doc/linker (Goリンカの内部に関する情報)
- これらの公式ドキュメントは、Goの内部動作を深く理解するための出発点となります。