[インデックス 17282] ファイルの概要
このコミットは、Goランタイムにおけるゴルーチンのスタックサイズに上限を設ける変更を導入しています。これにより、無限再帰などによってスタックが際限なく成長し、システムメモリを枯渇させることを防ぐことが目的です。具体的には、64ビットシステムでは1GB、32ビットシステムでは250MBのスタックサイズ制限が設定されます。
コミット
commit 757e0de89f80e89626cc8b7d6e670c0e5ea7f192
Author: Russ Cox <rsc@golang.org>
Date: Thu Aug 15 22:34:06 2013 -0400
runtime: impose stack size limit
The goal is to stop only those programs that would keep
going and run the machine out of memory, but before they do that.
1 GB on 64-bit, 250 MB on 32-bit.
That seems implausibly large, and it can be adjusted.
Fixes #2556.
Fixes #4494.
Fixes #5173.
R=khr, r, dvyukov
CC=golang-dev
https://golang.org/cl/12541052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/757e0de89f80e89626cc8b7d6e670c0e5ea7f192
元コミット内容
Goランタイムにおいて、スタックサイズに制限を課す。 目的は、無限にスタックを使い続け、最終的にマシンメモリを使い果たすプログラムを、その前に停止させることである。 64ビットシステムでは1GB、32ビットシステムでは250MBの制限を設定する。 この値は一見すると非常に大きいように思えるが、調整可能である。
以下のIssueを修正する:
- #2556
- #4494
- #5173
変更の背景
Goのゴルーチンは、非常に軽量なスレッドであり、そのスタックは必要に応じて動的に伸縮します。しかし、この動的なスタック成長メカニズムには、無限再帰のようなプログラミングエラーが発生した場合に、スタックが際限なく成長し続け、最終的にシステム全体のメモリを枯渇させてしまうという潜在的な問題がありました。
このコミットは、この問題に対処するために導入されました。具体的な背景としては、以下のIssueが挙げられます。
-
Issue #2556: runtime: stack overflow should be fatal このIssueでは、スタックオーバーフローが発生した場合に、プログラムがクラッシュするのではなく、より明確なエラーメッセージとともに終了すべきであるという議論がなされていました。従来のGoランタイムでは、スタックオーバーフローが直接的な致命的エラーとして扱われず、システム全体の不安定化につながる可能性がありました。
-
Issue #4494: runtime: stack overflow should be detected and reported このIssueは、スタックオーバーフローを検出し、ユーザーに報告するメカニズムの必要性を提起しています。単にクラッシュするだけでなく、何が問題であったかを開発者が理解できるように、適切なエラーメッセージや診断情報を提供することが求められていました。
-
Issue #5173: runtime: infinite recursion should not exhaust system memory このIssueは、無限再帰がシステムメモリを枯渇させるという直接的な問題提起です。Goの動的なスタック成長は、通常は非常に効率的ですが、無限再帰のような異常なケースでは、メモリを無制限に消費し続ける可能性があります。このコミットは、この問題に対する直接的な解決策を提供します。
これらのIssueは、Goランタイムの堅牢性と安定性を向上させるために、スタックの管理方法を見直し、無限のスタック成長を防ぐためのメカニズムを導入する必要があることを示していました。このコミットは、これらの懸念に対処し、Goプログラムがより予測可能で安全に動作するようにするための重要なステップです。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGoランタイムの概念とC言語の基本的な知識が必要です。
1. ゴルーチン (Goroutine) とスタック
- ゴルーチン: Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。ゴルーチンはGoランタイムによってスケジューリングされ、必要に応じてOSスレッドにマッピングされます。
- スタック: 各ゴルーチンは独自のスタックを持っています。スタックは、関数呼び出しの際にローカル変数、関数引数、戻りアドレスなどを格納するために使用されるメモリ領域です。Goのスタックは、必要に応じて動的にサイズが変更されます(スタックの伸縮)。これは、初期スタックサイズを小さく保ち、必要に応じて拡張することで、メモリ効率を高めるためのGoの重要な特徴です。
- スタックオーバーフロー: スタックが利用可能なメモリ領域を超えて成長しようとするときに発生するエラーです。通常、無限再帰のようなプログラミングエラーによって引き起こされます。
2. Goランタイムのメモリ管理
Goランタイムは、ガベージコレクション(GC)を含む独自のメモリ管理システムを持っています。スタックもこのメモリ管理の一部であり、必要に応じて確保・解放されます。スタックの動的な伸縮は、Goランタイムがメモリを効率的に利用するための重要なメカニズムです。
3. C言語の基本的な概念
Goランタイムの多くの部分はC言語で実装されています(特に初期のバージョンでは)。このコミットで変更されているファイルも、.c
や.h
といったC言語の拡張子を持つものが含まれています。
uintptr
: ポインタを保持できる符号なし整数型です。メモリアドレスを扱う際に使用されます。sizeof(void*)
:void*
型(汎用ポインタ)のサイズをバイト単位で返します。これは、システムが32ビットか64ビットかを判断するためによく使用されます。64ビットシステムでは通常8バイト、32ビットシステムでは4バイトです。runtime·throw
: Goランタイム内部で致命的なエラーが発生した場合に呼び出される関数です。プログラムをクラッシュさせます。runtime·printf
: Goランタイム内部で使用されるprintfライクな関数です。デバッグ情報やエラーメッセージを出力するために使用されます。FLUSH(&out)
: C言語のマクロまたは関数で、out
変数の値をメモリに確実に書き込むことを保証します。コンパイラの最適化によって値がレジスタに保持されたままになるのを防ぐために使用されることがあります。
4. src/pkg/runtime/debug
パッケージ
runtime/debug
パッケージは、Goプログラムのデバッグ情報やランタイムの動作に関する情報を提供するパッケージです。このコミットでは、このパッケージにスタックサイズ制限を設定するための新しい関数が追加されています。
これらの前提知識を理解することで、コミットがGoランタイムのどの部分に影響を与え、どのようなメカニズムでスタックサイズ制限が実装されているのかを深く理解することができます。
技術的詳細
このコミットは、Goランタイムにゴルーチンのスタックサイズ制限を導入し、無限再帰などによるメモリ枯渇を防ぐためのものです。主要な変更点は以下の通りです。
-
runtime·maxstacksize
変数の導入と初期化:src/pkg/runtime/runtime.h
にuintptr runtime·maxstacksize;
が追加され、グローバルなスタックサイズ制限を保持する変数が宣言されました。src/pkg/runtime/proc.c
のruntime·main
関数内で、このruntime·maxstacksize
がシステムのビット数に応じて初期化されます。- 64ビットシステム (
sizeof(void*) == 8
) の場合、1,000,000,000
バイト (1GB) に設定されます。 - 32ビットシステムの場合、
250,000,000
バイト (250MB) に設定されます。 - この初期値は、
src/pkg/runtime/stack.c
で1<<20
(1MB) として仮に初期化されていますが、runtime·main
で上書きされます。
- 64ビットシステム (
-
スタック成長時のサイズチェック:
src/pkg/runtime/stack.c
のruntime·newstack
関数(新しいスタックセグメントが必要になったときに呼び出される)内で、スタックの成長時に現在のゴルーチンのスタックサイズがruntime·maxstacksize
を超えていないかチェックするロジックが追加されました。gp->stacksize += framesize;
で新しいフレームサイズが現在のスタックサイズに追加された後、if(gp->stacksize > runtime·maxstacksize)
の条件でチェックが行われます。- もし制限を超えた場合、
runtime·printf
で「runtime: goroutine stack exceeds %D-byte limit」というメッセージが出力され、runtime·throw("stack overflow")
が呼び出されてプログラムが致命的なエラーで終了します。
-
runtime/debug.SetMaxStack
関数の追加:src/pkg/runtime/debug/garbage.go
にSetMaxStack
関数が追加されました。これはGo言語から呼び出すことができるAPIで、プログラム実行中にスタックサイズ制限を動的に変更することを可能にします。- このGo言語の関数は、内部的に
src/pkg/runtime/stack.c
に追加されたC言語の関数runtime∕debug·setMaxStack
を呼び出します。 runtime∕debug·setMaxStack
は、現在のruntime·maxstacksize
の値を返した後、新しい制限値でruntime·maxstacksize
を更新します。
-
スタックサイズ情報の更新:
src/pkg/runtime/runtime.h
のG
構造体(ゴルーチンを表す構造体)にuintptr stacksize;
フィールドが追加されました。これにより、各ゴルーチンが現在使用しているスタックの合計サイズを追跡できるようになります。src/pkg/runtime/proc.c
のruntime·malg
関数(新しいゴルーチンを割り当てる関数)で、新しく割り当てられるゴルーチンのg->stacksize
が初期化されます。src/pkg/runtime/panic.c
とsrc/pkg/runtime/stack.c
のruntime·unwindstack
およびruntime·oldstack
関数内で、スタックセグメントの解放時にgp->stacksize
から解放されたメモリサイズが減算されるようになりました。これにより、gp->stacksize
が常に正確なスタック使用量を反映するように保たれます。
-
テストケースの追加:
src/pkg/runtime/crash_test.go
にTestStackOverflow
という新しいテストケースが追加されました。このテストは、debug.SetMaxStack
を使用して意図的にスタックサイズ制限を小さく設定し、無限再帰を発生させることで、スタックオーバーフローが正しく検出され、プログラムが期待通りにクラッシュすることを確認します。
これらの変更により、Goランタイムはスタックの過度な成長を検出し、システムリソースの枯渇を防ぐことができるようになりました。SetMaxStack
関数は、デバッグや特定のユースケースにおいて、この制限を調整する柔軟性を提供します。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/pkg/runtime/runtime.h
:G
構造体にstacksize
フィールドが追加され、各ゴルーチンのスタックの合計サイズを追跡できるようになりました。runtime·maxstacksize
グローバル変数が宣言されました。
-
src/pkg/runtime/proc.c
:runtime·main
関数内で、runtime·maxstacksize
がシステムのビット数(32bit/64bit)に応じて初期化されるようになりました。runtime·malg
関数で、新しく割り当てられるゴルーチンのg->stacksize
が初期化されるようになりました。
-
src/pkg/runtime/stack.c
:runtime·newstack
関数に、スタック成長時にgp->stacksize
がruntime·maxstacksize
を超えていないかチェックするロジックが追加されました。制限を超えた場合はruntime·throw("stack overflow")
が呼び出されます。runtime∕debug·setMaxStack
というC言語関数が追加され、Goのdebug.SetMaxStack
から呼び出されることで、runtime·maxstacksize
を動的に変更できるようになりました。runtime·oldstack
関数で、スタックセグメントの解放時にgp->stacksize
が更新されるようになりました。
-
src/pkg/runtime/panic.c
:runtime·unwindstack
関数で、スタックセグメントの解放時にgp->stacksize
が更新されるようになりました。
-
src/pkg/runtime/debug/garbage.go
:SetMaxStack
というGo言語の関数が追加されました。これは、ユーザーがGoプログラムからスタックサイズ制限を設定するためのAPIです。
-
src/pkg/runtime/crash_test.go
:TestStackOverflow
という新しいテストケースが追加され、スタックオーバーフローの検出と処理が正しく行われることを検証します。
これらのファイルが連携して、Goランタイムにおけるスタックサイズ制限の導入と管理を実現しています。
コアとなるコードの解説
src/pkg/runtime/proc.c
の runtime·main
関数における runtime·maxstacksize
の初期化
// src/pkg/runtime/proc.c
void
runtime·main(void)
{
Defer d;
// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
// Using decimal instead of binary GB and MB because
// they look nicer in the stack overflow failure message.
if(sizeof(void*) == 8)
runtime·maxstacksize = 1000000000;
else
runtime·maxstacksize = 250000000;
newm(sysmon, nil);
// ... (後略)
}
このコードは、Goランタイムのメインエントリポイントである runtime·main
関数内で、ゴルーチンの最大スタックサイズ runtime·maxstacksize
を初期化しています。sizeof(void*)
を使用してシステムのビット数(64ビットか32ビットか)を判別し、それに応じて異なる制限値を設定しています。64ビットシステムでは1GB、32ビットシステムでは250MBという、当時としては非常に大きな値が設定されています。これは、通常のプログラムでは到達しにくいが、無限再帰のような異常なケースでメモリ枯渇を防ぐための安全弁として機能します。
src/pkg/runtime/stack.c
の runtime·newstack
関数におけるスタックサイズチェック
// src/pkg/runtime/stack.c
void
runtime·newstack(void)
{
// ... (前略)
if(framesize < StackMin)
framesize = StackMin;
framesize += StackSystem;
gp->stacksize += framesize; // 現在のスタックサイズに新しいフレームサイズを加算
if(gp->stacksize > runtime·maxstacksize) { // 最大スタックサイズを超えたかチェック
runtime·printf("runtime: goroutine stack exceeds %D-byte limit\n", (uint64)runtime·maxstacksize);
runtime·throw("stack overflow"); // 超えた場合は致命的エラー
}
stk = runtime·stackalloc(framesize);
// ... (後略)
}
runtime·newstack
関数は、ゴルーチンのスタックが不足し、新しいスタックセグメントが必要になったときに呼び出されます。この変更により、新しいフレームを割り当てる前に、現在のゴルーチン (gp
) の合計スタックサイズ (gp->stacksize
) に新しいフレームサイズ (framesize
) を加算し、それがグローバルな最大スタックサイズ (runtime·maxstacksize
) を超えていないかチェックするようになりました。もし超えていた場合、runtime·printf
でエラーメッセージを出力し、runtime·throw("stack overflow")
を呼び出してプログラムを強制終了させます。これにより、無限再帰などによるスタックの無制限な成長とそれに伴うシステムメモリの枯渇を防ぎます。
src/pkg/runtime/debug/garbage.go
の SetMaxStack
関数
// src/pkg/runtime/debug/garbage.go
package debug
// ... (前略)
// SetMaxStack sets the maximum amount of memory that
// can be used by a single goroutine stack.
// If any goroutine exceeds this limit while growing its stack,
// the program crashes.
// SetMaxStack returns the previous setting.
// The initial setting is 1 GB on 64-bit systems, 250 MB on 32-bit systems.
//
// SetMaxStack is useful mainly for limiting the damage done by
// goroutines that enter an infinite recursion. It only limits future
// stack growth.
func SetMaxStack(bytes int) int {
return setMaxStack(bytes)
}
//go:linkname setMaxStack runtime.setMaxStack
func setMaxStack(int) int
このGo言語の関数は、runtime/debug
パッケージの一部として公開され、Goプログラムからスタックサイズ制限を動的に設定するためのAPIを提供します。//go:linkname setMaxStack runtime.setMaxStack
ディレクティブは、Goコンパイラに対して、この setMaxStack
関数が runtime
パッケージ内の同名のC言語関数(またはGoアセンブリ関数)にリンクされるべきであることを指示しています。これにより、Goプログラムからランタイムの内部設定を変更することが可能になります。この関数は、主にデバッグやテスト、あるいは特定のパフォーマンス要件を持つアプリケーションで、デフォルトのスタックサイズ制限を調整するために使用されます。
src/pkg/runtime/stack.c
の runtime∕debug·setMaxStack
関数
// src/pkg/runtime/stack.c
void
runtime∕debug·setMaxStack(intgo in, intgo out)
{
out = runtime·maxstacksize; // 現在の値を out に設定
runtime·maxstacksize = in; // 新しい値を runtime·maxstacksize に設定
FLUSH(&out); // out の値を確実にメモリに書き込む
}
これは、Goの debug.SetMaxStack
から呼び出されるC言語の関数です。in
引数として新しいスタックサイズ制限を受け取り、runtime·maxstacksize
グローバル変数を更新します。また、変更前の runtime·maxstacksize
の値を out
引数(Go側では戻り値として受け取られる)に設定し、FLUSH(&out)
でその値が確実にメモリに書き込まれるようにします。これにより、Goプログラムはスタックサイズ制限を動的に変更し、その変更前の値を取得することができます。
これらのコード変更は、Goランタイムのスタック管理メカニズムに根本的な変更を加え、プログラムの安定性とリソース管理を向上させています。
関連リンク
- Go Issue #2556: runtime: stack overflow should be fatal: https://github.com/golang/go/issues/2556
- Go Issue #4494: runtime: stack overflow should be detected and reported: https://github.com/golang/go/issues/4494
- Go Issue #5173: runtime: infinite recursion should not exhaust system memory: https://github.com/golang/go/issues/5173
- Gerrit Change-Id: 12541052: https://golang.org/cl/12541052
参考にした情報源リンク
- Go言語の公式ドキュメント (runtime/debugパッケージ): https://pkg.go.dev/runtime/debug
- Go言語のスタック管理に関する一般的な情報源 (例: Goのブログ記事、技術解説など) (具体的なURLはコミット内容からは特定できないため、一般的な情報源として記載)
- C言語の基本的な概念に関する情報源 (例: C言語のチュートリアル、リファレンスなど) (具体的なURLはコミット内容からは特定できないため、一般的な情報源として記載)
- Goのソースコード (特に
src/runtime
ディレクトリ) https://github.com/golang/go/tree/master/src/runtime - Goのテストコード (特に
src/runtime/test
ディレクトリ) https://github.com/golang/go/tree/master/src/runtime/test - GoのIssueトラッカー https://github.com/golang/go/issues