[インデックス 12652] ファイルの概要
このコミットは、Go言語のランタイムとリンカにおけるスタック分割ロジックのバグを修正するものです。特に、デフォルトのスタックセグメントサイズに近いスタックを使用する関数において、スタックの枯渇や不正なスタックガードチェックが発生する問題を解決します。この修正には、リンカでのスタックサイズ計算の改善、ランタイムにおけるスタック関連定数の導入、および広範なテストケースの追加が含まれます。
コミット
commit 9e5db8c90a319b30a409a266853e8053f7b534d9
Author: Russ Cox <rsc@golang.org>
Date: Thu Mar 15 15:22:30 2012 -0400
5l, 6l, 8l: fix stack split logic for stacks near default segment size
Fixes #3310.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5823051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9e5db8c90a319b30a409a266853e8053f7b534d9
元コミット内容
5l, 6l, 8l: fix stack split logic for stacks near default segment size
Fixes #3310.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5823051
変更の背景
このコミットは、Go言語のIssue #3310「runtime: stack split bug」を修正するために行われました。Goのランタイムは、ゴルーチン(goroutine)のスタックを効率的に管理するために「セグメントスタック」というメカニズムを採用しています。これは、ゴルーチンが小さなスタックで開始し、必要に応じてスタックを動的に拡張(スタック分割)する仕組みです。
問題は、リンカ(5l
, 6l
, 8l
はそれぞれARM, x86 (32-bit), x86-64アーキテクチャ用のGoリンカを指します)が、関数のスタックフレームサイズを計算する際に、特にデフォルトのスタックセグメントサイズ(一般的に4KB)に近い値で誤った計算をしていたことにありました。これにより、スタック分割が必要な状況で正しくスタックが拡張されず、スタックの枯渇(stack exhaustion)や、スタックガードページ(stack guard page)のチェックが誤動作する可能性がありました。
具体的には、リンカが関数が必要とするスタック領域の総量を過小評価していたため、関数が実行を開始する前にスタックガードページに到達してしまうことがありました。これは、特に再帰呼び出しや大きなローカル変数を持つ関数で顕著に現れる可能性があり、プログラムのクラッシュや予期せぬ動作を引き起こす原因となっていました。
前提知識の解説
Goのゴルーチンとスタック管理
Go言語の並行処理の根幹をなすのが「ゴルーチン」です。ゴルーチンはOSのスレッドよりも軽量であり、数百万個のゴルーチンを同時に実行することも可能です。この軽量性を実現するための一つの重要な要素が、Goランタイムによるスタックの効率的な管理です。
Goは「セグメントスタック(segmented stack)」または「可変長スタック(resizable stack)」と呼ばれる方式を採用しています。これは、各ゴルーチンが最初は非常に小さなスタック(例えば2KBや4KB)で開始し、関数呼び出しによってスタックが不足しそうになった場合に、自動的に新しい、より大きなスタックセグメントを割り当てて既存のスタックをコピーし、新しいスタックで実行を継続するという仕組みです。このプロセスを「スタック分割(stack split)」と呼びます。
スタックガードページ
スタックのオーバーフローを防ぎ、スタック分割をトリガーするために、Goランタイムは「スタックガードページ」を使用します。これは、スタックの現在の限界を示すメモリページであり、通常はアクセスするとセグメンテーション違反(segmentation fault)などのハードウェア例外が発生するように設定されています。
Goの関数は、実行開始時にスタックガードページに到達していないかチェックを行います。もし到達しそうであれば、スタック分割処理が実行され、より大きなスタックが確保されます。このチェックは、コンパイラとリンカによって生成されるコードに組み込まれています。
リンカの役割
Goのリンカ(5l
, 6l
, 8l
など)は、コンパイルされたオブジェクトファイル群を結合して実行可能ファイルを生成するだけでなく、スタック管理においても重要な役割を担っています。リンカは、各関数が必要とするスタックフレームのサイズを正確に計算し、スタック分割チェックに必要な情報を埋め込みます。この計算には、関数のローカル変数、引数、戻り値、および関数呼び出しに必要な追加のスペース(例えば、呼び出し先のPCを保存する領域など)が含まれます。
autoffset
とスタックフレーム
Goのリンカでは、autoffset
という概念が使われます。これは、関数が使用する自動変数(ローカル変数)の合計サイズを示すオフセットです。リンカは、この autoffset
を基に、関数が実行時に必要とするスタック領域の総量を決定します。
技術的詳細
このコミットの技術的詳細は、主にリンカがスタックフレームサイズを計算する方法の改善と、ランタイムがスタックの状態を管理する方法の調整にあります。
リンカのスタック計算の修正
以前のリンカでは、スタックの必要量を計算する際に、autosize + 160
のようなマジックナンバー(160
)を使用していました。この 160
は、コメントによると「3回の呼び出し(38)、4つのセーフ(48)、および104のガード」から来ていると説明されていましたが、これは特定のアーキテクチャや状況下での近似値であり、すべてのケース、特にデフォルトセグメントサイズに近いスタック使用量の場合に正確ではありませんでした。
この修正では、このマジックナンバーをより正確で意味のある定数に置き換えることで、スタック計算の堅牢性を高めています。新しい計算式は以下の要素を考慮しています。
StackTop
: スタックトップのデータブロックの想定サイズ。textarg
: 関数の引数と戻り値のサイズ。PtrSize
: ポインタのサイズ(アーキテクチャ依存)。autoffset
: 関数のローカル変数のサイズ。StackLimit
: スタックガードページが設定されるスタックの限界値。StackMin
: スタック分割時に確保される最小スタックサイズ。
新しい計算式 StackTop + textarg + PtrSize + autoffset + PtrSize + StackLimit >= StackMin
は、関数が安全に実行するために必要な最小スタックサイズをより正確に反映しています。これにより、リンカはスタック分割が必要かどうかをより正確に判断できるようになります。
また、p->from.offset = (autoffset+7) & ~7LL;
のような変更は、スタックオフセットを8バイト境界にアラインメントするためのものです。これは、メモリのアクセス効率を向上させ、特定のアーキテクチャでのパフォーマンスを最適化するために重要です。
ランタイムの変更
src/pkg/runtime/stack.h
:StackTop = 72
という新しい定数が導入されました。これは、スタックトップのデータブロックの想定サイズを示します。この値は、リンカのスタック計算で使用されるようになります。src/pkg/runtime/proc.c
:runtime·malg
関数(ゴルーチンのスタックを割り当てる関数)に、StackTop
の値が適切であるかどうかのチェックが追加されました。これにより、stack.h
で定義されたStackTop
の値が小さすぎる場合にランタイムエラーを発生させ、不正な設定を早期に検出できるようになります。src/pkg/runtime/asm_386.s
,src/pkg/runtime/asm_amd64.s
,src/pkg/runtime/asm_arm.s
: 各アーキテクチャのアセンブリコードにruntime·stackguard
という新しい関数が追加されました。この関数は、現在のスタックポインタ(SP)とスタックガードの限界値を取得するために使用されます。これは、特にテストコードからスタックの状態を詳細に検査するために導入されました。src/pkg/runtime/export_test.go
:runtime·stackguard
関数がテスト目的でエクスポートされ、Stackguard
という名前で利用可能になりました。
テストの追加
最も重要な変更の一つは、src/pkg/runtime/stack_test.go
という新しいテストファイルの追加です。このテストは、スタック分割ロジックを徹底的に検証するために設計されました。
TestStackSplit
関数は、0バイトに近いサイズからデフォルトセグメントサイズ(4KB)を超えるサイズまで、あらゆるスタックフレームサイズを持つ関数を呼び出します。- 各テスト関数(例:
stack4
,stack8
, ...,stack5000
)は、指定されたサイズのローカルバッファを割り当て、runtime.Stackguard()
を呼び出して現在のスタックポインタとスタックガードの限界値を取得します。 TestStackSplit
は、これらの値が期待される範囲内にあることを確認し、スタックが正しく拡張されているか、またはスタックガードに不適切に到達していないかを検証します。- このテストは、以前のリンカが特定のスタックサイズ(例: 3812, 3816, 3820バイトなど)で誤動作していたことを具体的に示しており、このコミットが修正しようとしている問題の性質を明確にしています。
この包括的なテストスイートの追加により、将来的に同様のスタック関連のバグが再発するのを防ぐための強力なセーフティネットが提供されます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
-
src/cmd/5l/noop.c
:--- a/src/cmd/5l/noop.c +++ b/src/cmd/5l/noop.c @@ -226,8 +226,7 @@ noops(void) tp->as = AMOVW; tp->scond = C_SCOND_LO; tp->from.type = D_CONST; - /* 160 comes from 3 calls (3*8) 4 safes (4*8) and 104 guard */ - tp->from.offset = autosize+160; + tp->from.offset = autosize; tp->to.type = D_REG; tp->to.reg = 1;
autosize+160
からautosize
へ変更。 -
src/cmd/6l/pass.c
:--- a/src/cmd/6l/pass.c +++ b/src/cmd/6l/pass.c @@ -501,10 +501,17 @@ dostkoff(void)\n q = p;\n }\n \n - /* 160 comes from 3 calls (3*8) 4 safes (4*8) and 104 guard */\n + // If we ask for more stack, we'll get a minimum of StackMin bytes.\n + // We need a stack frame large enough to hold the top-of-stack data,\n + // the function arguments+results, our caller's PC, our frame,\n + // a word for the return PC of the next call, and then the StackLimit bytes\n + // that must be available on entry to any function called from a function\n + // that did a stack check. If StackMin is enough, don't ask for a specific\n + // amount: then we can use the custom functions and save a few\n + // instructions.\n moreconst1 = 0;\n - if(autoffset+160+textarg > 4096)\n - moreconst1 = (autoffset+160) & ~7LL;\n + if(StackTop + textarg + PtrSize + autoffset + PtrSize + StackLimit >= StackMin)\n + moreconst1 = autoffset;\n moreconst2 = textarg;\n \n // 4 varieties varieties (const1==0 cross const2==0)
スタックサイズ計算ロジックが
autoffset+160+textarg > 4096
からStackTop + textarg + PtrSize + autoffset + PtrSize + StackLimit >= StackMin
に変更。 -
src/cmd/8l/pass.c
:--- a/src/cmd/8l/pass.c +++ b/src/cmd/8l/pass.c @@ -527,10 +527,18 @@ dostkoff(void)\n p = appendp(p); // save frame size in DX\n p->as = AMOVL;\n p->to.type = D_DX;\n - /* 160 comes from 3 calls (3*8) 4 safes (4*8) and 104 guard */\n p->from.type = D_CONST;\n - if(autoffset+160+cursym->text->to.offset2 > 4096)\n - p->from.offset = (autoffset+160) & ~7LL;\n +\n + // If we ask for more stack, we'll get a minimum of StackMin bytes.\n + // We need a stack frame large enough to hold the top-of-stack data,\n + // the function arguments+results, our caller's PC, our frame,\n + // a word for the return PC of the next call, and then the StackLimit bytes\n + // that must be available on entry to any function called from a function\n + // that did a stack check. If StackMin is enough, don't ask for a specific\n + // amount: then we can use the custom functions and save a few\n + // instructions.\n + if(StackTop + cursym->text->to.offset2 + PtrSize + autoffset + PtrSize + StackLimit >= StackMin)\n + p->from.offset = (autoffset+7) & ~7LL;\n \n p = appendp(p); // save arg size in AX
6l/pass.c
と同様にスタックサイズ計算ロジックが変更され、autoffset+7
のアラインメントが追加。 -
src/pkg/runtime/stack.h
:--- a/src/pkg/runtime/stack.h +++ b/src/pkg/runtime/stack.h @@ -94,4 +94,9 @@ enum {\n // The maximum number of bytes that a chain of NOSPLIT\n // functions can use.\n StackLimit = StackGuard - StackSystem - StackSmall,\n +\n + // The assumed size of the top-of-stack data block.\n + // The actual size can be smaller than this but cannot be larger.\n + // Checked in proc.c's runtime.malg.\n + StackTop = 72,\n };
StackTop = 72
の定義が追加。 -
src/pkg/runtime/asm_386.s
,src/pkg/runtime/asm_amd64.s
,src/pkg/runtime/asm_arm.s
: 各アーキテクチャのアセンブリファイルにTEXT runtime·stackguard(SB),7,$0
で始まるruntime·stackguard
関数の実装が追加。この関数は、現在のスタックポインタとスタックガードの値をレジスタから取得し、呼び出し元に返す。 -
src/pkg/runtime/export_test.go
:--- a/src/pkg/runtime/export_test.go +++ b/src/pkg/runtime/export_test.go @@ -19,7 +19,9 @@ var F64toint = f64toint\n func entersyscall()\n func exitsyscall()\n func golockedOSThread() bool\n +func stackguard() (sp, limit uintptr)\n \n var Entersyscall = entersyscall\n var Exitsyscall = exitsyscall\n var LockedOSThread = golockedOSThread\n +var Stackguard = stackguard
stackguard
関数がテストのためにエクスポートされるように変更。 -
src/pkg/runtime/proc.c
:--- a/src/pkg/runtime/proc.c +++ b/src/pkg/runtime/proc.c @@ -1161,6 +1161,11 @@ runtime·malg(int32 stacksize)\n {\n G *newg;\n byte *stk;\n +\n + if(StackTop < sizeof(Stktop)) {\n + runtime·printf("runtime: SizeofStktop=%d, should be >=%d\\n", (int32)StackTop, (int32)sizeof(Stktop));\n + runtime·throw("runtime: bad stack.h");\n + }\n \n newg = runtime·malloc(sizeof(G));\n if(stacksize >= 0) {
runtime·malg
関数にStackTop
のサイズチェックが追加。 -
src/pkg/runtime/stack_test.go
: 新規ファイルとして追加。スタック分割ロジックを網羅的にテストするための多数の関数(stack4
からstack5000
まで)とTestStackSplit
関数が含まれる。
コアとなるコードの解説
このコミットの核心は、Goのスタック管理におけるリンカとランタイム間の連携をより正確かつ堅牢にすることです。
リンカのスタック計算の改善
src/cmd/5l/noop.c
, src/cmd/6l/pass.c
, src/cmd/8l/pass.c
における変更は、リンカが関数に必要なスタックサイズを決定する方法を修正しています。以前は 160
という固定値が使われていましたが、これはスタックフレームのオーバーヘッドを概算するためのマジックナンバーでした。この値は、特定のアーキテクチャやコンテキストでは機能しても、Goのセグメントスタックの動的な性質や、様々な関数呼び出しパターン、特にスタックセグメントの境界付近での挙動において不正確さを生じさせていました。
新しい計算式 StackTop + textarg + PtrSize + autoffset + PtrSize + StackLimit >= StackMin
は、以下の要素を明示的に考慮することで、より正確なスタック必要量を導き出します。
StackTop
: スタックの最上位に位置するランタイム内部データ構造のサイズ。textarg
: 関数に渡される引数と、関数から返される戻り値が占めるスタック領域。PtrSize
: ポインタのサイズ。これは、呼び出し元のPC(プログラムカウンタ)やフレームポインタなど、スタックに保存されるアドレスのサイズを考慮するために必要です。autoffset
: 関数内で宣言されたローカル変数(自動変数)が占めるスタック領域。StackLimit
: スタックガードページが設定される、スタックの安全な下限。この領域は、スタックオーバーフローを防ぐために確保されるバッファです。StackMin
: Goランタイムがスタックを拡張する際に割り当てる最小のスタックセグメントサイズ。
これらの要素を組み合わせることで、リンカは関数が実行を開始する前に、十分なスタックスペースが確保されているか、またはスタック分割が必要かをより正確に判断できるようになります。特に、autoffset+7) & ~7LL
のようなアラインメント操作は、スタックポインタが常に8バイト境界に揃うようにすることで、パフォーマンスの最適化と特定のCPUアーキテクチャでのアライメント要件を満たしています。
runtime·stackguard
関数の導入とテストの強化
src/pkg/runtime/asm_*.s
に追加された runtime·stackguard
関数は、Goランタイムの内部状態、特に現在のスタックポインタとスタックガードの限界値を、Goコードから安全に取得するためのメカニズムを提供します。これは、src/pkg/runtime/export_test.go
を介してテストコードに公開され、src/pkg/runtime/stack_test.go
で活用されます。
stack_test.go
は、このコミットの検証において極めて重要な役割を果たします。このテストは、様々なサイズのスタックフレームを持つ関数を大量に生成し、それぞれの関数が呼び出された後に runtime.Stackguard()
を使用してスタックの状態をチェックします。これにより、リンカが生成したスタック分割ロジックが、あらゆるスタックサイズ、特に以前バグがあったデフォルトセグメントサイズ付近で正しく機能するかどうかを網羅的に検証します。
テストコードのコメントにあるように、以前のリンカは stack3812
, stack3816
などの特定のサイズで sp < limit
となる(スタックポインタがスタック限界を下回る、つまりスタックオーバーフローの危険がある)問題を抱えていました。この新しいテストは、このようなエッジケースを体系的に検出し、修正が正しく適用されたことを保証します。
StackTop
定数と runtime·malg
のチェック
src/pkg/runtime/stack.h
で定義された StackTop = 72
は、スタックの最上位に位置するランタイム内部データ構造のサイズを明示的に定数として定義したものです。この定数をリンカの計算に組み込むことで、スタックのオーバーヘッドに関する仮定がより正確になります。
src/pkg/runtime/proc.c
の runtime·malg
関数に追加された if(StackTop < sizeof(Stktop))
チェックは、StackTop
の値がランタイムの内部構造 Stktop
のサイズよりも小さい場合にパニックを発生させます。これは、StackTop
の定義が誤っている場合に早期に問題を検出するための健全性チェックであり、ランタイムの堅牢性を高めます。
これらの変更は、Goのスタック管理の正確性と信頼性を大幅に向上させ、特にスタックセグメントの境界付近で発生していた潜在的なクラッシュや不正な動作を排除することを目的としています。
関連リンク
- Go Issue #3310: https://github.com/golang/go/issues/3310
- Go Change List (CL) 5823051: https://golang.org/cl/5823051
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/
,src/pkg/runtime/
ディレクトリ) - Go言語の公式ドキュメント (スタック管理、ゴルーチンに関する記述)
- Go言語のIssueトラッカー (Issue #3310の詳細)
- Go言語のリンカに関する一般的な情報 (Goのリンカがどのように動作するかについての記事やドキュメント)
- セグメントスタックに関する一般的な情報 (コンピュータサイエンスの概念としてのセグメントスタック)
- アセンブリ言語の基礎 (スタックポインタ、レジスタ、メモリ管理に関する知識)