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

[インデックス 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という値は、通常のスタックサイズ4KBStackSystem2KB512 * sizeof(uintptr)、当時のuintptrが4バイトの場合)が加算されたものと推測されています。

修正内容は、StackSystemの値を4KBに設定することで、stackallocが期待するスタックサイズ(おそらく2のべき乗)に合致させ、問題を解決するというものです。この変更は、既にwindows/amd64アーキテクチャではStackSystem4KBに設定されていることとの整合性も考慮されています。

変更の背景

Go言語のランタイムは、ゴルーチン(goroutine)と呼ばれる軽量な並行処理単位のために、動的にサイズ変更可能なスタックを管理しています。従来のOSスレッドが固定サイズのスタックを持つことが多いのに対し、Goのゴルーチンは小さな初期スタックで開始し、必要に応じてスタックを拡張します。これにより、メモリ使用量を最適化し、スタックオーバーフローのリスクを軽減します。

このコミットの背景には、Windows/386環境におけるスタックアロケーションの特定の挙動がありました。Goランタイムは、OS固有の目的(シグナルハンドリングなど)のために、通常のスタックガード領域の下に追加のメモリ領域を確保することがあります。この領域はStackSystemと呼ばれ、特にWindowsやPlan 9のように、シグナルハンドリングに別途スタックを使用しないOSで重要になります。

当時のWindows/386環境では、StackSystem512 * sizeof(uintptr)(約2KB)に設定されていました。しかし、Goランタイムのスタックアロケーションロジック(stackalloc)が、特定の条件下で4KBの基本スタックサイズにStackSystem2KBが加算された6KBというサイズでスタックを要求した際に、問題が発生していました。これは、新しいスタックコードがスタックサイズを2のべき乗にすることを要求していたためと考えられます。6KBは2のべき乗ではないため、stackallocが期待する条件を満たせず、ビルドエラーやランタイムエラーを引き起こしていた可能性があります。

この問題は、windows/amd64では既にStackSystem4KBに設定されており、同様の問題が発生していなかったことから、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バイトであるため、StackSystem2048バイト(2KB)となります。

Goのゴルーチンは、初期スタックとして通常4KB0x1000)を割り当てます。これにStackSystem2KBが加わると、合計で6KB0x1800)のスタックが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/386StackSystem4096バイト(4KB)に統一することで、この不整合を解消しました。これにより、初期スタック4KBStackSystem4KBを合わせた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など)は、StackSystem0に設定されます。これは、これらの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が関連するため)