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

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

このコミットは、Go言語のリンカ(ld)がWindows上でCgo(C言語との相互運用機能)を使用する際に、デフォルトのスタックサイズを増やす変更を導入しています。これにより、Cgoを利用するGoプログラムがWindows環境でスタックオーバーフローなどの問題を起こすのを防ぎます。特に、Goランタイムが管理するGoルーチンのスタックとは別に、Cgoが呼び出すC関数が使用するOSスレッドのスタックサイズが不足していた問題に対処しています。

コミット

commit 428062da4e5e35ce75178d993dd6d8ef5e3ecb5d
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Dec 7 16:53:17 2011 +0300

    ld: increase default stack size on Windows for cgo
    Fixes #2437.
    
    R=rsc, hectorchu, mattn.jp, alex.brainman, jdpoirier, snaury, n13m3y3r
    CC=golang-dev
    https://golang.org/cl/5371049

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

https://github.com/golang/go/commit/428062da4e5e35ce75178d993dd6d8ef5e3ecb5d

元コミット内容

元のコミットメッセージは「ld: increase default stack size on Windows for cgo」であり、Windows環境におけるCgo利用時のデフォルトスタックサイズ増加が目的であることを明確に示しています。これは、Go issue #2437を修正するためのものです。

変更の背景

Go言語は、Goルーチンと呼ばれる軽量な並行処理の単位を使用し、そのスタックはGoランタイムによって動的に管理されます。しかし、Cgoを介してC言語の関数を呼び出す場合、GoランタイムはOSのスレッドを生成し、そのスレッド上でC関数を実行します。Windowsオペレーティングシステムでは、新しいスレッドが作成される際にデフォルトのスタックサイズが割り当てられます。

Go issue #2437では、Windows上でCgoを使用する際に、デフォルトのスタックサイズが不足し、特に再帰的なC関数呼び出しや大きなスタックフレームを必要とするCライブラリを使用した場合に、スタックオーバーフローが発生するという問題が報告されていました。これは、GoランタイムがGoルーチンのスタックを効率的に管理しているにもかかわらず、Cgoが利用するOSスレッドのスタックがGoランタイムの制御外で不足するという、異なるレイヤーでの問題でした。

この問題を解決するためには、Goリンカが生成する実行可能ファイル(PEファイル)のヘッダ情報において、メインスレッドのデフォルトスタック予約サイズを増やす必要がありました。また、Cgoが新たにOSスレッドを生成する際にも、適切なスタックサイズを確保するように変更する必要がありました。

前提知識の解説

  • Cgo: Go言語とC言語(またはC互換のライブラリ)を相互運用するためのGoの機能です。Cgoを使用すると、Goプログラムから既存のCライブラリを呼び出したり、CコードをGoプログラムに組み込んだりできます。Cgoは、Goの実行モデルとCの実行モデル(特にスタック管理)の間に橋渡しをします。
  • スタック (Stack): プログラムの実行中に、関数呼び出しの引数、ローカル変数、リターンアドレスなどを一時的に格納するために使用されるメモリ領域です。スタックはLIFO(Last-In, First-Out)の原則で動作します。
  • スタックサイズ (Stack Size): プロセスやスレッドに割り当てられるスタックメモリの最大量です。このサイズを超えてスタックを使用しようとすると、スタックオーバーフローが発生し、プログラムがクラッシュする原因となります。
  • スタックガード (Stack Guard): Goランタイムにおけるスタック管理のメカニズムの一つです。Goルーチンのスタックは動的に伸縮しますが、スタックガードはスタックの境界を監視し、スタックが拡張される必要がある場合にランタイムに通知します。これにより、スタックオーバーフローを未然に防ぎ、必要に応じてスタックを拡張します。
  • Windows PEファイルフォーマット (Portable Executable): Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLLなどのファイルフォーマットです。PEファイルには、プログラムの実行に必要な様々な情報(コード、データ、リソース、インポート/エクスポートテーブルなど)が含まれています。特に、PEヘッダには、プログラムの初期スタックサイズやヒープサイズなどの情報が格納されます。
  • リンカ (Linker, ld): コンパイラによって生成されたオブジェクトファイル(機械語コード)を結合し、実行可能なプログラムやライブラリを生成するツールです。リンカは、外部参照の解決、メモリレイアウトの決定、実行可能ファイルのヘッダ情報の書き込みなどを行います。Goのツールチェーンでは、cmd/ldがGoプログラムのリンカとして機能します。
  • _beginthread / CreateThread: Windows APIでスレッドを作成するための関数です。
    • _beginthread はCランタイムライブラリの関数で、CreateThread のラッパーです。Cランタイムの初期化を適切に行い、スレッドローカルストレージなどを設定します。
    • CreateThread は低レベルのWindows APIで、より詳細な制御が可能です。この関数には、新しいスレッドのスタックサイズを指定する引数があります。スタックサイズを0に設定すると、実行可能ファイルのPEヘッダに指定されたデフォルトのスタックサイズが使用されます。

技術的詳細

このコミットの技術的詳細は、主に以下の3つの領域にわたる変更を含んでいます。

  1. Goリンカ (src/cmd/ld/pe.c, src/cmd/ld/lib.c, src/cmd/ld/lib.h):

    • src/cmd/ld/lib.csrc/cmd/ld/lib.hiscgo というグローバル変数が追加されました。これは、リンク対象のGoプログラムが runtime/cgo パッケージを使用しているかどうかを示すフラグです。loadlib 関数内で、runtime/cgo パッケージがロードされた場合に iscgotrue に設定されます。
    • src/cmd/ld/pe.casmbpe 関数は、Windows PEファイルのヘッダ情報を構築する部分です。以前は、メインスレッドのスタック予約サイズ (SizeOfStackReserve) とコミットサイズ (SizeOfStackCommit) が固定値(予約: 0x00010000 = 64KB, コミット: 0x0000ffff = 64KB - 1KB)に設定されていました。
    • このコミットでは、iscgo フラグに基づいてスタックサイズを条件分岐するロジックが追加されました。
      • iscgofalse (Cgoを使用しない場合) の場合、従来の64KBのスタックサイズが維持されます。
      • iscgotrue (Cgoを使用する場合) の場合、32-bit (PE32) では予約サイズを 0x00100000 (1MB)、64-bit (PE32+) では 0x00200000 (2MB) に増やします。コミットサイズは、予約サイズから2つのガードページ分 (0x2000 = 8KB) を引いた値に設定されます。これにより、Cgoを使用するGoプログラムのメインスレッドが、より大きなスタックを持つことになります。
  2. Goランタイムのアセンブリコード (src/pkg/runtime/386/asm.s, src/pkg/runtime/amd64/asm.s):

    • _rt0_386 (32-bit) および _rt0_amd64 (64-bit) は、Goプログラムのエントリポイントとなるアセンブリコードです。
    • これらのファイルでは、Goルーチン g0 (スケジューラが使用する特別なGoルーチン) の初期スタック境界 (g_stackguard, g_stackbase) の設定ロジックが変更されました。
    • 以前は initcgo が存在しない場合にのみスタックガードを設定していましたが、この変更により、initcgo の有無にかかわらず、Goランタイムが管理するスタックの初期設定がより一貫して行われるようになりました。これは、CgoがOSスレッドのスタックを管理する一方で、GoランタイムはGoルーチンのスタックを管理するという役割分担を明確にするためです。
  3. CgoランタイムのWindows固有コード (src/pkg/runtime/cgo/windows_386.c, src/pkg/runtime/cgo/windows_amd64.c, src/pkg/runtime/windows/thread.c):

    • src/pkg/runtime/cgo/windows_386.csrc/pkg/runtime/cgo/windows_amd64.c では、CgoがOSスレッドを作成する際のスタックサイズに関する変更が行われました。
    • STACKSIZE マクロの定義が、32-bitでは1MB、64-bitでは2MBに更新され、リンカのPEヘッダ設定と同期されることがコメントで明記されました。
    • xinitcgo 関数内で、Goルーチン gstackguard の計算が 4096 (4KB) から 8*1024 (8KB) に変更されました。これは、スタックガードページの設定に関連する調整です。
    • 最も重要な変更は libcgo_sys_thread_start 関数です。以前は _beginthread の第2引数(スタックサイズ)に STACKSIZE を直接渡していましたが、このコミットでは 0 を渡すように変更されました。_beginthread0 を渡すと、Windowsは実行可能ファイルのPEヘッダに指定されたデフォルトのスタックサイズを使用します。これにより、リンカによって設定された新しい大きなスタックサイズが、Cgoが生成するスレッドにも適用されるようになります。
    • src/pkg/runtime/windows/thread.c では、runtime·newosproc 関数(Goランタイムが新しいOSスレッドを作成する際に使用)において、CreateThread の呼び出しに STACK_SIZE_PARAM_IS_A_RESERVATION フラグと 0x20000 (128KB) のスタック予約サイズが追加されました。これは、Goランタイムが直接OSスレッドを作成する場合のスタックサイズ設定をより明示的に行うための変更です。

これらの変更により、Cgoを使用するGoプログラムがWindows上で実行される際に、メインスレッドおよびCgoが生成する補助スレッドの両方で、より十分なスタックサイズが確保されるようになり、スタックオーバーフローの問題が解決されました。

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

src/cmd/ld/pe.c (リンカのPEヘッダ設定)

--- a/src/cmd/ld/pe.c
+++ b/src/cmd/ld/pe.c
@@ -650,8 +650,21 @@ asmbpe(void)
 	// Commit size must be strictly less than reserve
 	// size otherwise reserve will be rounded up to a
 	// larger size, as verified with VMMap.
-	set(SizeOfStackReserve, 0x00010000);
-	set(SizeOfStackCommit, 0x0000ffff);
+
+	// Go code would be OK with 64k stacks, but we need larger stacks for cgo.
+	// That default stack reserve size affects only the main thread,
+	// for other threads we specify stack size in runtime explicitly
+	// (runtime knows whether cgo is enabled or not).
+	// If you change stack reserve sizes here,
+	// change them in runtime/cgo/windows_386/amd64.c as well.
+	if(!iscgo) {
+		set(SizeOfStackReserve, 0x00010000);
+		set(SizeOfStackCommit, 0x0000ffff);
+	} else {
+		set(SizeOfStackReserve, pe64 ? 0x00200000 : 0x00100000);
+		// account for 2 guard pages
+		set(SizeOfStackCommit, (pe64 ? 0x00200000 : 0x00100000) - 0x2000);
+	}
 	set(SizeOfHeapReserve, 0x00100000);
 	set(SizeOfHeapCommit, 0x00001000);
 	set(NumberOfRvaAndSizes, 16);

src/pkg/runtime/cgo/windows_386.c (Cgoランタイムの32-bit Windows固有コード)

--- a/src/pkg/runtime/cgo/windows_386.c
+++ b/src/pkg/runtime/cgo/windows_386.c
@@ -8,15 +8,16 @@
 
 static void *threadentry(void*);
 
-/* From what I\'ve read 1MB is default for 32-bit Linux. 
-   Allocation granularity on Windows is typically 64 KB. */
+/* 1MB is default stack size for 32-bit Windows.
+   Allocation granularity on Windows is typically 64 KB.
+   The constant is also hardcoded in cmd/ld/pe.c (keep synchronized). */
 #define STACKSIZE (1*1024*1024)
 
 static void
 xinitcgo(G *g)
 {
 	int tmp;
-\tg->stackguard = (uintptr)&tmp - STACKSIZE + 4096;\n+\tg->stackguard = (uintptr)&tmp - STACKSIZE + 8*1024;\n }
 
 void (*initcgo)(G*) = xinitcgo;
@@ -24,8 +25,7 @@ void (*initcgo)(G*) = xinitcgo;
 void
 libcgo_sys_thread_start(ThreadStart *ts)
 {
-\tts->g->stackguard = STACKSIZE;\n-\t_beginthread(threadentry, STACKSIZE, ts);\n+\t_beginthread(threadentry, 0, ts);\n }
 
 static void*
 threadentry(void *v)
@@ -38,12 +38,7 @@ threadentry(void *v)
 	free(v);
 
 	ts.g->stackbase = (uintptr)&ts;
-\n-\t/*\n-\t * libcgo_sys_thread_start set stackguard to stack size;\n-\t * change to actual guard pointer.\n-\t */\n-\tts.g->stackguard = (uintptr)&ts - ts.g->stackguard + 4096;\n+\tts.g->stackguard = (uintptr)&ts - STACKSIZE + 8*1024;\n 
 	/*
 	 * Set specific keys in thread local storage.

コアとなるコードの解説

src/cmd/ld/pe.c の変更

この変更は、Goリンカが生成するWindows実行可能ファイル(PEファイル)のヘッダに書き込むスタックサイズ情報を制御します。

  • iscgo 変数(src/cmd/ld/lib.cruntime/cgo パッケージがリンクされている場合に true に設定される)が導入されました。
  • if (!iscgo) ブロックでは、Cgoを使用しない通常のGoプログラムの場合、メインスレッドのスタック予約サイズを 0x00010000 (64KB) に、コミットサイズを 0x0000ffff (64KB - 1KB) に設定します。これは従来のGoプログラムの挙動を維持します。
  • else ブロックでは、Cgoを使用するGoプログラムの場合、メインスレッドのスタック予約サイズを大幅に増やします。
    • pe64 ? 0x00200000 : 0x00100000 は、64-bit (PE32+) の場合は 0x00200000 (2MB)、32-bit (PE32) の場合は 0x00100000 (1MB) をスタック予約サイズとして設定します。
    • コミットサイズは、予約サイズから 0x2000 (8KB) を引いた値に設定されます。この 0x2000 は、Windowsがスタックの拡張に使用するガードページ(通常4KB/ページ)2枚分のサイズを考慮したものです。 この変更により、Cgoを使用するGoプログラムのメインスレッドは、OSによってより大きなスタックが割り当てられるようになります。

src/pkg/runtime/cgo/windows_386.c (および windows_amd64.c) の変更

これらのファイルは、CgoがWindows上で新しいOSスレッドを作成する際の挙動を定義しています。

  • STACKSIZE マクロのコメントが更新され、この値がWindowsのデフォルトスタックサイズ(32-bitで1MB、64-bitで2MB)と一致し、リンカの cmd/ld/pe.c と同期されるべきであることが明記されました。
  • xinitcgo 関数内の g->stackguard の計算が + 4096 から + 8*1024 に変更されました。これは、Goランタイムが管理するGoルーチンのスタックガードの初期設定における微調整であり、スタックガードページのサイズ変更に対応している可能性があります。
  • libcgo_sys_thread_start 関数内の _beginthread の呼び出しが変更されました。
    • 以前は _beginthread(threadentry, STACKSIZE, ts) のように、STACKSIZE を明示的に渡していました。
    • 変更後は _beginthread(threadentry, 0, ts) のように、スタックサイズ引数に 0 を渡すようになりました。
    • Windowsの _beginthread (およびその基盤となる CreateThread) では、スタックサイズに 0 を指定すると、実行可能ファイルのPEヘッダに指定されたデフォルトのスタックサイズが使用されます。 この変更により、Cgoが生成するOSスレッドは、リンカによってPEヘッダに書き込まれた新しい大きなデフォルトスタックサイズを自動的に継承するようになり、スタックオーバーフローの問題が解決されます。

関連リンク

参考にした情報源リンク