[インデックス 18800] ファイルの概要
このドキュメントは、Go言語のランタイムにおける特定のコミット(ハッシュ: 02903f8395c4d62c0e07f5ed114252635b29e92f
)について、その技術的な詳細と背景を深く掘り下げて解説します。特に、Windows/386環境におけるスタック管理の改善に焦点を当てます。
コミット
commit 02903f8395c4d62c0e07f5ed114252635b29e92f
Author: Russ Cox <rsc@golang.org>
Date: Fri Mar 7 14:19:05 2014 -0500
runtime: fix windows/386 build
From the trace it appears that stackalloc is being
called with 0x1800 which is 6k = 4k + (StackSystem=2k).
Make StackSystem 4k too, to make stackalloc happy.
It's already 4k on windows/amd64.
TBR=khr
CC=golang-codereviews
https://golang.org/cl/72600043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/02903f8395c4d62c0e07f5ed114252635b29e92f
元コミット内容
このコミットは、GoランタイムにおけるWindows/386ビルドの問題を修正することを目的としています。具体的には、スタックアロケーション(stackalloc
)が0x1800
(6KB)という値で呼び出された際に問題が発生していることがトレースから判明しました。この6KB
という値は、通常のスタックサイズ4KB
にStackSystem
の2KB
(512 * sizeof(uintptr)
、当時のuintptr
が4バイトの場合)が加算されたものと推測されています。
修正内容は、StackSystem
の値を4KB
に設定することで、stackalloc
が期待するスタックサイズ(おそらく2のべき乗)に合致させ、問題を解決するというものです。この変更は、既にwindows/amd64
アーキテクチャではStackSystem
が4KB
に設定されていることとの整合性も考慮されています。
変更の背景
Go言語のランタイムは、ゴルーチン(goroutine)と呼ばれる軽量な並行処理単位のために、動的にサイズ変更可能なスタックを管理しています。従来のOSスレッドが固定サイズのスタックを持つことが多いのに対し、Goのゴルーチンは小さな初期スタックで開始し、必要に応じてスタックを拡張します。これにより、メモリ使用量を最適化し、スタックオーバーフローのリスクを軽減します。
このコミットの背景には、Windows/386環境におけるスタックアロケーションの特定の挙動がありました。Goランタイムは、OS固有の目的(シグナルハンドリングなど)のために、通常のスタックガード領域の下に追加のメモリ領域を確保することがあります。この領域はStackSystem
と呼ばれ、特にWindowsやPlan 9のように、シグナルハンドリングに別途スタックを使用しないOSで重要になります。
当時のWindows/386環境では、StackSystem
が512 * sizeof(uintptr)
(約2KB)に設定されていました。しかし、Goランタイムのスタックアロケーションロジック(stackalloc
)が、特定の条件下で4KB
の基本スタックサイズにStackSystem
の2KB
が加算された6KB
というサイズでスタックを要求した際に、問題が発生していました。これは、新しいスタックコードがスタックサイズを2のべき乗にすることを要求していたためと考えられます。6KB
は2のべき乗ではないため、stackalloc
が期待する条件を満たせず、ビルドエラーやランタイムエラーを引き起こしていた可能性があります。
この問題は、windows/amd64
では既にStackSystem
が4KB
に設定されており、同様の問題が発生していなかったことから、windows/386
固有の設定が原因であることが示唆されていました。
前提知識の解説
Go言語のスタック管理
Go言語のゴルーチンは、動的にサイズが変化するスタックを使用します。
- 初期スタックサイズ: ゴルーチンは通常、小さな初期スタック(例: 2KB)で開始します。
- スタックの拡張: スタックが不足しそうになると、Goランタイムはより大きな連続したメモリブロックを割り当て、古いスタックの内容を新しい場所にコピーします。この「スタックコピー」方式は、以前の「セグメントスタック」方式に代わるもので、スタックの縮小や再成長をより効率的に行えます。
runtime.stackalloc
: この内部関数は、新しいゴルーチンの初期スタックメモリを割り当てる役割を担います。ゴルーチンの初期化プロセス中に呼び出されます。ゴルーチンのスタックは概念的には独立していますが、そのメモリはGoランタイムのメモリ管理システムによってヒープ上に確保されます。
StackSystem
StackSystem
は、GoランタイムがOS固有の目的のために、各スタックの通常のガード領域の下に確保する追加のメモリ領域です。
- 目的: 主にシグナルハンドリングや例外処理など、OSレベルのメカニズムに対応するために使用されます。WindowsやPlan 9、iOSなど、OSがシグナルハンドリングに別途スタックを使用しないシステムで特に重要です。
- Windowsにおける役割: Windows環境では、Goランタイムの例外ハンドラ(
runtime.sigtramp
など)が正しく機能するために、このStackSystem
領域が必要となります。これにより、パニックやその他の低レベルイベントが発生した際に、Goランタイムが適切に対応できるようになります。 - 歴史的経緯: 過去には、
windows/386
におけるStackSystem
と構造化例外処理(SEH)の統合に関して、スタック破損やアクセス違反といった問題が報告されたこともあります。これは、GoランタイムとOS固有の例外処理メカニズムとの連携の複雑さを示しています。
uintptr
uintptr
は、Go言語における符号なし整数型で、ポインタを保持するのに十分な大きさがあります。そのサイズはアーキテクチャに依存し、32ビットシステムでは4バイト、64ビットシステムでは8バイトです。このコミットの文脈では、StackSystem = 512 * sizeof(uintptr)
という記述から、uintptr
のサイズがStackSystem
の計算に影響を与えていたことがわかります。windows/386
は32ビットアーキテクチャであるため、sizeof(uintptr)
は4バイトとなり、512 * 4 = 2048
バイト(2KB)となります。
技術的詳細
このコミットの技術的な核心は、GoランタイムがWindows/386環境でスタックを割り当てる際の、スタックサイズ要件とStackSystem
の値の不整合を解消することにあります。
Goランタイムの新しいスタック管理コードは、スタックサイズが2のべき乗であることを要求していました。これは、メモリ管理の効率化や、特定のメモリアライメント要件を満たすためであると考えられます。
コミット前のwindows/386
におけるStackSystem
の値は512 * sizeof(uintptr)
でした。32ビットシステムであるwindows/386
ではsizeof(uintptr)
が4バイトであるため、StackSystem
は2048
バイト(2KB)となります。
Goのゴルーチンは、初期スタックとして通常4KB
(0x1000
)を割り当てます。これにStackSystem
の2KB
が加わると、合計で6KB
(0x1800
)のスタックがstackalloc
に要求されることになります。しかし、6KB
は2のべき乗ではありません(2のべき乗は1, 2, 4, 8, 16...KB)。このため、stackalloc
が期待する2のべき乗という条件を満たせず、スタックアロケーションが失敗するか、予期せぬ動作を引き起こしていました。
一方、windows/amd64
(64ビットシステム)では、StackSystem
は既に4096
バイト(4KB)に設定されていました。64ビットシステムではsizeof(uintptr)
が8バイトであるため、もし512 * sizeof(uintptr)
の計算式が適用されていたとすると、512 * 8 = 4096
バイトとなり、結果的に4KB
になります。しかし、コミットのコメントから、windows/amd64
では明示的に4k
に設定されていたことが示唆されています。この4KB
という値は2のべき乗であり、初期スタックの4KB
と合わせても8KB
となり、これも2のべき乗であるため、stackalloc
は問題なく動作していました。
このコミットでは、windows/386
のStackSystem
も4096
バイト(4KB)に統一することで、この不整合を解消しました。これにより、初期スタック4KB
とStackSystem
の4KB
を合わせた8KB
というスタックサイズがstackalloc
に要求されるようになり、これは2のべき乗であるため、スタックアロケーションが正常に行われるようになりました。
この変更は、src/pkg/runtime/stack.h
ファイル内のenum
定義におけるStackSystem
の値を直接変更することで実現されています。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/stack.h
ファイルにあります。
--- a/src/pkg/runtime/stack.h
+++ b/src/pkg/runtime/stack.h
@@ -57,13 +57,15 @@ enum {
// to each stack below the usual guard area for OS-specific
// purposes like signal handling. Used on Windows and on
// Plan 9 because they do not use a separate stack.
+\t// The new stack code requires stacks to be a power of two,
+\t// and the default start size is 4k, so make StackSystem also 4k
+\t// to keep the sum a power of two. StackSystem used to be
+\t// 512*sizeof(uintptr) on Windows and 512 bytes on Plan 9.
#ifdef GOOS_windows
-\tStackSystem = 512 * sizeof(uintptr),
+\tStackSystem = 4096,
#else
#ifdef GOOS_plan9
-\t// The size of the note handler frame varies among architectures,
-\t// but 512 bytes should be enough for every implementation.
-\tStackSystem = 512,\
+\tStackSystem = 4096,\
#else
StackSystem = 0,
#endif // Plan 9
コアとなるコードの解説
このコードスニペットは、Goランタイムがスタック管理に使用する定数を定義しているstack.h
ファイルの一部です。
enum { ... }
: C言語のスタイルで定数を定義するブロックです。StackSystem
: この定数が、OS固有の目的のためにスタックに確保される追加のメモリ領域のサイズを定義しています。#ifdef GOOS_windows
: これはプリプロセッサディレクティブで、コンパイル対象のOSがWindowsである場合に続くコードブロックが有効になります。- StackSystem = 512 * sizeof(uintptr),
: 変更前のWindowsにおけるStackSystem
の定義です。uintptr
のサイズ(32ビットシステムでは4バイト)に512
を乗じることで、2048
バイト(2KB)が設定されていました。+ StackSystem = 4096,
: 変更後のWindowsにおけるStackSystem
の定義です。4096
バイト(4KB)に固定されました。これにより、スタックアロケーションが期待する2のべき乗のサイズ要件を満たすようになります。
#ifdef GOOS_plan9
: 同様に、Plan 9 OSの場合のコードブロックです。- StackSystem = 512,
: 変更前のPlan 9におけるStackSystem
の定義です。512
バイトが設定されていました。コメントには「note handler frameのサイズはアーキテクチャによって異なるが、512バイトで十分なはず」とあります。+ StackSystem = 4096,
: 変更後のPlan 9におけるStackSystem
の定義です。こちらも4096
バイト(4KB)に統一されました。これは、新しいスタックコードの要件に合わせるためと考えられます。
#else StackSystem = 0,
: 上記のいずれのOSでもない場合(例: Linux, macOSなど)は、StackSystem
は0
に設定されます。これは、これらのOSがシグナルハンドリングに別途スタックを使用するため、Goランタイムが追加の領域を確保する必要がないことを意味します。
追加されたコメントは、この変更の理由を明確に説明しています。 「新しいスタックコードはスタックが2のべき乗であることを要求し、デフォルトの開始サイズは4kなので、合計が2のべき乗になるようにStackSystemも4kにする。StackSystemはWindowsでは512*sizeof(uintptr)、Plan 9では512バイトだった。」
この変更により、Windows/386環境でのスタックアロケーションの不整合が解消され、ビルドおよびランタイムの安定性が向上しました。
関連リンク
- Go言語のスタック管理に関する公式ドキュメントやブログ記事(Goのバージョンアップに伴い、スタック管理の内部実装は進化しています。特にセグメントスタックからスタックコピーへの移行は重要な変更点です。)
- Goのランタイムソースコード(
src/runtime/
ディレクトリ) - GoのIssueトラッカー(
StackSystem
やスタックアロケーションに関する過去の議論やバグ報告)
参考にした情報源リンク
- Go言語のスタック管理に関する解説記事(例: Cloudflare, Mediumなどの技術ブログ)
- Go言語のソースコードリポジトリ(特に
src/runtime/
以下のファイル) - Go言語のIssueトラッカー(GitHub Issuesや旧Go Bug Tracker)
- Go言語の公式ドキュメント
- Go言語のコミット履歴と関連するコードレビュー(
https://golang.org/cl/72600043
など) uintptr
の定義とアーキテクチャ依存性に関する情報- Windowsの構造化例外処理(SEH)に関する情報(
StackSystem
が関連するため)