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

[インデックス 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言語の内部動作に関する知識が必要です。

  1. Goの関数呼び出し規約とレジスタ使用:

    • Goの関数は、特定のレジスタを特定の目的のために使用します。例えば、一部のレジスタは引数渡しや戻り値、あるいは特別なランタイム情報(gポインタなど)のために予約されています。
    • コンテキストレジスタ (Context Register): Goでは、クロージャ(匿名関数)が外部スコープの変数をキャプチャする際に、その環境へのポインタを保持するために特別なレジスタ(アーキテクチャによって異なる)を使用します。また、メソッド呼び出しにおけるレシーバもこのレジスタを通じて渡されることがあります。非クロージャ関数や通常の関数では、このレジスタは通常使用されません。
  2. スタック管理とmorestack:

    • Goのランタイムは、動的にスタックを拡張するメカニズムを持っています。関数が呼び出され、そのスタックフレームが現在のスタックの残りの容量を超えそうになると、コンパイラによって挿入されたプロローグコードがruntime.morestack関数を呼び出します。
    • morestackは、新しい、より大きなスタックセグメントを割り当て、現在のスタックの内容を新しいセグメントにコピーし、実行を継続します。このプロセス中に、呼び出し元の関数のレジスタ状態(プログラムカウンタ、スタックポインタ、汎用レジスタなど)が保存され、復元されます。
  3. ガベージコレクション (GC):

    • Goはトレース型ガベージコレクタを使用しています。これは、プログラムが現在アクセス可能なすべてのメモリ(「生きている」オブジェクト)を特定し、それ以外のメモリ(「死んでいる」オブジェクト)を回収する方式です。
    • GCは、プログラムのルート(グローバル変数、現在のスタック上のレジスタやスタックフレーム内の変数など)から到達可能なすべてのポインタをたどることで、生きているオブジェクトを識別します。
    • ポインタスキャン: GCは、スタックやヒープ上のメモリ領域をスキャンし、ポインタらしき値を見つけると、それが有効なオブジェクトを指しているかどうかを判断します。レジスタもGCスキャンの対象となります。
  4. メモリリーク:

    • 本来は不要になったメモリが、GCによって「生きている」と誤って判断され、回収されずに残り続ける状態を指します。これにより、プログラムが使用するメモリ量が時間とともに増加し、パフォーマンスの低下やシステムリソースの枯渇につながる可能性があります。

これらの概念が組み合わさることで、非クロージャ関数で未使用のコンテキストレジスタがGCに誤認識され、メモリリークを引き起こすという問題が発生していました。

技術的詳細

このコミットの技術的な核心は、Goのコンパイラ(cmd/gc)、リンカ(cmd/ld)、およびランタイム(src/pkg/runtime)が連携して、非クロージャ関数のmorestack呼び出しパスを最適化し、コンテキストレジスタの不要な保持を防ぐ点にあります。

  1. needctxtフラグの導入:

    • src/cmd/gc/go.hNode構造体にuchar needctxtという新しいフィールドが追加されました。これは、その関数がコンテキストレジスタ(クロージャ変数を持つか、部分適用された関数呼び出しであるかなど)を必要とするかどうかを示すフラグです。
    • src/cmd/gc/closure.cでは、クロージャや部分適用された関数を生成する際に、このneedctxtフラグが適切に設定されるようになりました。
    • src/cmd/gc/pgen.cでは、コンパイル時にこのneedctxtフラグが、リンカに渡される関数のテキストフラグ(TEXTFLAG)にNEEDCTXTとして反映されるようになりました。
  2. NEEDCTXTテキストフラグの定義:

    • src/cmd/ld/textflag.hに新しいテキストフラグ#define NEEDCTXT 64が追加されました。これは、リンカが関数の特性を識別するために使用します。
  3. 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がレジスタを保存する際に、コンテキストレジスタには常にゼロが格納されるようになります。
  4. リンカの変更:

    • リンカ(src/liblink/obj5.c, src/liblink/obj6.c, src/liblink/obj8.c)は、関数のTEXTFLAGNEEDCTXTが設定されているかどうかをチェックするようになりました。
    • addstacksplit関数(スタック分割チェックコードを挿入する部分)内で、cursym->text->from.scale & NEEDCTXTのチェックが行われます。
    • NEEDCTXTが設定されていない(つまり、非クロージャ関数である)場合、リンカは通常のmorestackではなく、新しく導入されたmorestack_noctxt関数への呼び出しを生成します。
    • include/link.hLink構造体にある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フラグがTEXTFLAGNEEDCTXTとして設定されるようになりました。
  • 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は、コンパイラが特定の関数がコンテキストレジスタを必要とするかどうかを追跡するためのメタデータです。クロージャや部分適用された関数は、その性質上、外部環境への参照(コンテキスト)を必要とするため、needctxt1に設定されます。通常の関数では0のままです。

このneedctxt情報は、最終的にリンカに渡される関数のTEXTFLAGNEEDCTXTとして埋め込まれます。これにより、リンカは各関数がコンテキストレジスタを必要とするかどうかを判断できます。

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シンボルを決定します。noctxttrue(非クロージャ関数)であれば、runtime.morestack_noctxtのような_noctxtサフィックスを持つシンボルを選択します。noctxtfalse(クロージャ関数など)であれば、通常のruntime.morestackシンボルを選択します。
  • Link構造体のsymmorestack配列は、これらのmorestackシンボルへのポインタを保持するために拡張されました。

src/pkg/runtime/asm_*.s

これらのアセンブリファイルは、Goランタイムの低レベルな部分、特にスタック管理やスケジューリングに関連するコードを実装しています。

新しく追加されたruntime.morestack_noctxt関数群は、このコミットの最終的な目的を達成する部分です。例えば、src/pkg/runtime/asm_amd64.sTEXT 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関数へジャンプ

これにより、非クロージャ関数がスタック拡張を必要とする際に、コンテキストレジスタが確実にゼロに設定され、ガベージコレクタがそのレジスタをスキャンしても、有効なポインタとして誤認識することがなくなります。

関連リンク

参考にした情報源リンク

  • 上記のGitHub IssueとGo Code Reviewのリンクは、このコミットの背景と議論を理解する上で直接的な情報源となります。
  • Go言語のランタイム、コンパイラ、リンカの内部動作に関する一般的なドキュメントやブログ記事(例: Goのスタック管理、GCの仕組み、関数呼び出し規約など)も、前提知識の理解に役立ちます。