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

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

このコミットは、Goランタイムにおける、まだ開始されていないゴルーチン(goroutine)のスタックをスキャンする際のバグを修正するものです。具体的には、引数領域のサイズが不明な場合に、スタックが正しくスキャンされない問題を解決します。

コミット

commit 50ba6e13b4b552117d4c9d966729eda1948e7a96
Author: Carl Shapiro <cshapiro@google.com>
Date:   Thu May 16 10:42:39 2013 -0700

    runtime: fix scanning of not started goroutines
    
    The stack scanner for not started goroutines ignored the arguments
    area when its size was unknown.  With this change, the distance
    between the stack pointer and the stack base will be used instead.
    
    Fixes #5486
    
    R=golang-dev, bradfitz, iant, dvyukov
    CC=golang-dev
    https://golang.org/cl/9440043
---
 src/pkg/runtime/mgc0.c | 13 ++++++++++---\n 1 file changed, 10 insertions(+), 3 deletions(-)\n

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

https://github.com/golang/go/commit/50ba6e13b4b552117d4c9d966729eda1948e7a96

元コミット内容

runtime: fix scanning of not started goroutines

The stack scanner for not started goroutines ignored the arguments
area when its size was unknown. With this change, the distance
between the stack pointer and the stack base will be used instead.

Fixes #5486

変更の背景

Go言語のランタイムは、ガベージコレクション(GC)の一環として、実行中のゴルーチンのスタックをスキャンし、到達可能なオブジェクトを特定します。これにより、不要になったメモリを解放することができます。

このコミットが修正しようとしている問題は、特に「まだ開始されていないゴルーチン」のスタックをスキャンする際に発生していました。まだ開始されていないゴルーチンとは、go キーワードによって作成されたものの、まだ実行が開始されていない、あるいは初期化段階にあるゴルーチンのことです。

問題の核心は、これらのゴルーチンのスタック上にある「引数領域」のサイズが、ランタイムにとって不明な場合があるという点でした。スタックをスキャンする際、ランタイムはどこからどこまでが有効なデータ(ポインタなど)であるかを正確に知る必要があります。引数領域のサイズが不明だと、スタックの一部がスキャン対象から漏れてしまい、その領域に存在するはずのポインタがGCによって見つけられず、結果として誤って解放されてしまう(Use-After-Freeなどの)深刻なバグにつながる可能性がありました。

このコミットは、この「引数領域のサイズが不明」という状況に対処し、スタックポインタとスタックベース(スタックの最下部)の間の距離を、スキャンすべき領域のサイズとして利用することで、この問題を解決しています。これにより、まだ開始されていないゴルーチンのスタックも、その引数領域を含めて確実にスキャンされるようになります。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムとガベージコレクションに関する基本的な知識が必要です。

Goランタイム (Go Runtime)

Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネル通信、システムコールなど、Goプログラムの実行に必要な多くの低レベルな機能を提供します。C言語で書かれたsrc/pkg/runtime/mgc0.cのようなファイルは、このランタイムのコア部分を構成しています。

ゴルーチン (Goroutine)

ゴルーチンはGo言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。各ゴルーチンは独自のスタックを持ち、関数呼び出しの引数、ローカル変数、戻りアドレスなどがこのスタックに積まれます。

スタック (Stack)

プログラムの実行中に、関数呼び出しのたびに、その関数のローカル変数、引数、戻りアドレスなどがスタックと呼ばれるメモリ領域に積まれていきます。関数が終了すると、その情報がスタックから取り除かれます。スタックは通常、メモリ上の高いアドレスから低いアドレスに向かって成長します。

  • スタックポインタ (Stack Pointer, SP): 現在のスタックの最上部(または最下部、アーキテクチャによる)を指すレジスタ。
  • スタックベース (Stack Base): スタックの開始位置(通常はスタックの最下部)を指すアドレス。

スタックスキャン (Stack Scanning)

ガベージコレクションのプロセスにおいて、GCはプログラムが現在使用しているメモリ(到達可能なオブジェクト)を特定する必要があります。スタックスキャンは、各ゴルーチンのスタックを走査し、スタック上に存在するポインタ(他のメモリ領域を指すアドレス)を見つけ出すプロセスです。これらのポインタが指すオブジェクトは「到達可能」とみなされ、GCによって解放されません。

ガベージコレクション (Garbage Collection, GC)

GoのGCは、自動的に不要になったメモリを解放する仕組みです。プログラマが手動でメモリを管理する必要がなくなるため、メモリリークやダングリングポインタといったバグのリスクを軽減します。GoのGCは、並行マーク&スイープ方式を採用しており、プログラムの実行と並行して動作することで、GCによる一時停止(Stop-The-World)時間を最小限に抑えています。

fnstart->fnf->args

Goランタイムの内部では、関数に関するメタデータが管理されています。

  • gp->fnstart->fn: ゴルーチン gp が実行しようとしている関数の開始アドレスを指します。fnstart は関数ポインタのようなもので、その先の fn は関数自体の情報(メタデータ)を保持しています。
  • f->args: 関数 f が取る引数の合計サイズ(バイト単位)を示します。この情報は、スタックをスキャンする際に、引数領域がどこまでかを判断するために使用されます。

uintptr, byte*, Obj

これらはGoランタイムのCコードで使われる内部的な型です。

  • uintptr: ポインタを保持できる符号なし整数型。メモリアドレスを表すのに使われます。
  • byte*: バイト列へのポインタ。メモリブロックを扱う際によく使われます。
  • Obj: Goランタイム内部で、スキャン対象のメモリ領域を表す構造体。通常、開始アドレス、サイズ、およびその他のフラグを含みます。addroot 関数に渡され、GCのルートセット(スキャン開始点)に追加されます。

技術的詳細

このコミットの技術的な詳細は、src/pkg/runtime/mgc0.c 内の addstackroots 関数に焦点を当てています。この関数は、ガベージコレクションの際に、特定のゴルーチンのスタックをスキャンしてルート(到達可能なオブジェクトの開始点)を追加する役割を担っています。

修正前のコードでは、まだ開始されていないゴルーチン(gp->status == Gdead または gp->status == Grunning ではない状態)のスタックをスキャンする際に、関数の引数サイズ f->args0 より大きい場合にのみ、その引数領域をスキャン対象としていました。

// 修正前
if(f->args > 0) {
    if(thechar == '5')
        sp += sizeof(uintptr);
    addroot((Obj){sp, f->args, 0});
}

問題は、f->args0 の場合、つまり引数のサイズが不明な場合(または引数がない場合)に、引数領域が全くスキャンされないことでした。しかし、Goの内部的なメカニズムや特定のコンパイルパターンによっては、引数がない関数であっても、スタック上に何らかのデータ(例えば、戻り値のためのスペースや、レジスタからスピルされた値など)が存在し、それがポインタである可能性がありました。f->args0 の場合、この領域は無視され、GCがその中のポインタを見逃す可能性があったのです。

この修正は、この問題を解決するために、f->args0 でない(つまり引数サイズが既知である)場合に加えて、f->args0 の場合(引数サイズが不明または引数がない場合)にも対応するようにロジックを変更しました。

新しいロジックでは、f->args0 でない場合はこれまで通り引数サイズ f->args を使ってスキャンします。しかし、f->args0 の場合は、スタックポインタ sp からスタックベース stk までの全領域をスキャン対象とします。stk はゴルーチンのスタックの最下部(または最上部、成長方向による)を指すため、stk - sp はスタックポインタからスタックベースまでの距離、つまり現在のスタックフレームの残りの部分のサイズを表します。これにより、引数領域のサイズが不明な場合でも、スタック上の有効なポインタが確実にスキャンされるようになります。

thechar == '5' の条件は、Goのコンパイルターゲットアーキテクチャが32ビット(例えばx86)であるかどうかをチェックしています。32ビットアーキテクチャでは、スタックポインタの調整が必要になる場合があります。sizeof(uintptr) はポインタのサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)です。

この修正により、まだ開始されていないゴルーチンのスタックがより堅牢にスキャンされるようになり、GCの正確性が向上し、潜在的なメモリ関連のバグが回避されます。

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

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1454,11 +1454,18 @@ addstackroots(G *gp)\
 			// be scanned.  No other live values should be on the
 			// stack.
 			f = runtime·findfunc((uintptr)gp->fnstart->fn);\
-			if(f->args > 0) {
+			if(f->args != 0) {
 				if(thechar == '5')
 					sp += sizeof(uintptr);\
-				addroot((Obj){sp, f->args, 0});
-			}\
+				// If the size of the arguments is known
+				// scan just the incoming arguments.
+				// Otherwise, scan everything between the
+				// top and the bottom of the stack.
+				if(f->args > 0)
+					addroot((Obj){sp, f->args, 0});
+				else
+					addroot((Obj){sp, (byte*)stk - sp, 0}); 
+			} 
 			return;
 		}
 	}

コアとなるコードの解説

変更は src/pkg/runtime/mgc0.c ファイル内の addstackroots 関数にあります。この関数は、ガベージコレクションの際に、ゴルーチンのスタックからルート(GCがスキャンを開始するポインタ)を追加する役割を担っています。

  1. if(f->args > 0) から if(f->args != 0) への変更:

    • 修正前: if(f->args > 0) は、「関数の引数サイズが0より大きい場合」、つまり引数が存在し、そのサイズが既知の場合にのみ、引数領域をスキャンしていました。
    • 修正後: if(f->args != 0) は、「関数の引数サイズが0ではない場合」に条件が変更されました。これは、引数サイズが既知である限り、その値が正であるかどうかにかかわらず(実際には正の値しか取りませんが、論理的な変更点として)、処理を継続することを示唆しています。しかし、この変更の真の意図は、その後のネストされた if/else ブロックで明らかになります。
  2. 新しい if/else ブロックの追加:

    • コメントの追加:

      // If the size of the arguments is known
      // scan just the incoming arguments.
      // Otherwise, scan everything between the
      // top and the bottom of the stack.
      

      このコメントは、新しいロジックの意図を明確に説明しています。引数サイズが既知の場合は引数のみをスキャンし、そうでない場合はスタックのトップからボトムまで全てをスキャンするという方針です。

    • if(f->args > 0):

      if(f->args > 0)
          addroot((Obj){sp, f->args, 0});
      

      これは修正前のロジックとほぼ同じです。関数の引数サイズ f->args0 より大きい場合(つまり、引数が存在し、そのサイズが既知の場合)、スタックポインタ sp から f->args のサイズ分だけをスキャン対象として addroot します。sp は、thechar == '5' の条件に基づいて既に調整されている可能性があります。

    • else ブロックの追加:

      else
          addroot((Obj){sp, (byte*)stk - sp, 0}); 
      

      これがこのコミットの最も重要な変更点です。 f->args > 0 の条件が偽の場合、つまり f->args0 の場合(引数サイズが不明または引数がない場合)にこの else ブロックが実行されます。 ここでは、sp から (byte*)stk - sp のサイズ分をスキャン対象として addroot しています。

      • stk: ゴルーチンのスタックのベース(通常は最下部)を指すポインタ。
      • (byte*)stk - sp: スタックベース stk から現在のスタックポインタ sp までの距離をバイト単位で計算しています。これは、現在のスタックフレームの残りの部分、つまりスタックポインタからスタックの最下部までの全領域のサイズを表します。 この変更により、引数サイズが不明な場合でも、スタック上の潜在的なポインタが確実にスキャンされるようになり、GCの正確性が向上します。

関連リンク

  • Go CL (Code Review) リンク: https://golang.org/cl/9440043
  • 修正されたIssue #5486: Web検索では直接的な情報が見つかりませんでしたが、GoのIssue Trackerで過去のIssueとして存在していた可能性があります。

参考にした情報源リンク

  • 提供されたコミットデータ (./commit_data/16326.txt)
  • Go言語のランタイム、ガベージコレクション、ゴルーチンに関する一般的な知識
  • C言語のポインタ演算とメモリ管理の概念