[インデックス 10316] ファイルの概要
このコミットは、Go言語のCgo機能において、Cgoが8KB以上のスタックを消費し、コールバックを行う際に発生していたクラッシュを修正するものです。具体的には、Goランタイムのg0
スタックのガード機構に問題があり、スタックが不足した際に適切に検出・処理されなかったことが原因でした。この修正により、CgoとGoランタイム間のスタック管理が改善され、安定性が向上しました。
コミット
commit fbfed49134bca038184dbc1a427e82647fc1f12e
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Nov 9 23:11:48 2011 +0300
cgo: fix g0 stack guard
Fixes crash when cgo consumes more than 8K
of stack and makes a callback.
Fixes #1328.
R=golang-dev, rogpeppe, rsc
CC=golang-dev, mpimenov
https://golang.org/cl/5371042
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fbfed49134bca038184dbc1a427e82647fc1f12e
元コミット内容
このコミットは、Cgoが8KB以上のスタックを消費し、Goへのコールバックを行う際に発生するクラッシュを修正します。これはGoのIssue #1328で報告された問題に対応するものです。
変更の背景
Go言語のCgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数をコールバックしたりするためのメカニズムを提供します。この相互運用性には、GoランタイムとCランタイム間のスタック管理の複雑さが伴います。
Goランタイムには、Goルーチンが使用するスタックとは別に、ランタイム自身が内部処理(スケジューリング、ガベージコレクションなど)のために使用する特別なスタック「g0スタック」が存在します。Cgoを介してCコードがGoの関数をコールバックする際、GoランタイムはCスタックからg0スタックに切り替えて処理を行います。
報告された問題(Issue #1328)は、Cgoを介したコールバックにおいて、Cコード側で8KBを超えるスタックを消費した場合にクラッシュが発生するというものでした。これは、g0スタックのガードページ(スタックオーバーフローを検出するための保護領域)が適切に設定されていなかったか、またはその検出ロジックに不備があったためと考えられます。結果として、Cgoコールバック中にg0スタックがオーバーフローしても、それが検出されずに不正なメモリ領域へのアクセスが発生し、プログラムがクラッシュしていました。
このコミットは、このスタックガードの問題を修正し、Cgoコールバック時のg0スタックの安全性を確保することを目的としています。
前提知識の解説
Goランタイムとスタック管理
Goランタイムは、Goルーチンごとに可変長のスタックを割り当て、必要に応じてスタックを拡張・縮小します。これは、Goルーチンが非常に軽量であり、数百万ものGoルーチンを同時に実行できるGoの並行処理モデルの基盤となっています。
しかし、Goランタイム自身も内部的な処理のためにスタックを必要とします。これが「g0スタック」と呼ばれるものです。g0スタックは、Goルーチンのスタックとは異なり、固定サイズ(通常は8KBまたは16KB)で、システムコール、Cgoコール、スケジューラ関連の処理など、Goランタイムの低レベルな操作に使用されます。
スタックガード
スタックガードは、スタックオーバーフローを検出するための一般的なメカニズムです。スタックの末尾(通常はスタックが成長する方向の逆側)に「ガードページ」と呼ばれる保護されたメモリページを配置します。プログラムがこのガードページにアクセスしようとすると、ページフォルトが発生し、ランタイムはそのスタックオーバーフローを検出して適切なエラー処理(パニックなど)を行うことができます。これにより、スタックオーバーフローによる不正なメモリアクセスやクラッシュを防ぎます。
Cgoとスタック切り替え
Cgoを介してGoからC関数を呼び出す際、Goランタイムは現在のGoルーチンのスタックからCスタックに切り替えます。同様に、C関数からGo関数をコールバックする際には、CスタックからGoランタイムのg0スタックに切り替える必要があります。このスタック切り替えの過程で、g0スタックのガードが正しく機能しないと、Cコードが大量のスタックを消費した場合に問題が発生する可能性があります。
pthread_attr_getstacksize
pthread_attr_getstacksize
はPOSIXスレッド(pthread)ライブラリの関数で、スレッド属性オブジェクト(pthread_attr_t
)からスタックサイズを取得するために使用されます。CgoがCスレッドからGoにコールバックする際、GoランタイムはCスレッドのスタック情報を利用してg0スタックのガードを設定する必要がある場合があります。この関数は、Cスレッドのスタックのベースアドレスとサイズを決定するために利用されます。
技術的詳細
このコミットの主要な変更点は、Goランタイムの初期化プロセスとCgo関連のコードにおけるg0
スタックガードの設定方法の修正です。
-
misc/cgo/test/callback_c.c
の変更: テストケースに、意図的に大きなスタック領域(64KB)を使用するコードが追加されました。これは、修正が正しく機能するかどうかを検証するためのものです。volatile char data[64*1024];
のような宣言は、コンパイラによる最適化を防ぎ、実際にスタック領域が確保されることを保証します。 -
src/pkg/runtime/386/asm.s
およびsrc/pkg/runtime/amd64/asm.s
の変更: これらのアセンブリファイルは、Goランタイムの初期化ルーチン(_rt0_386
,_rt0_amd64
)を含んでいます。変更の核心は、initcgo
関数が呼び出される際に、g0
スタックのガードが適切に設定されるようにすることです。- 以前は、
initcgo
が呼び出される前にg0
スタックガードが固定値(例えば-64*1024+104
や-8192+104
)で設定されていました。 - 修正後は、
initcgo
が存在し、それがスタックガードを設定する責任がある場合(JNZ stackok
)、ランタイムは独自のスタックガード設定をスキップします。 initcgo
がG*
(Goルーチン構造体へのポインタ)を引数として受け取るように変更され、initcgo
内でg->stackguard
を設定できるようになりました。これにより、CgoがOSのスレッドスタック情報を利用して、より正確なg0
スタックガードを設定することが可能になります。
- 以前は、
-
src/pkg/runtime/cgo/*.c
ファイル群の変更: Darwin, FreeBSD, Linux, Windowsの各アーキテクチャ(386, amd64, arm)に対応するCgoランタイムファイルが変更されました。xinitcgo
関数のシグネチャがvoid xinitcgo(void)
からvoid xinitcgo(G *g)
に変更されました。これにより、xinitcgo
関数内で現在のGoルーチン(g0)の情報を直接操作できるようになります。xinitcgo
関数内で、pthread_attr_getstacksize
(POSIXシステムの場合)またはローカル変数と定義済みスタックサイズ(Windowsの場合)を使用して、現在のCスレッドのスタック情報を取得し、それに基づいてg->stackguard
を設定するロジックが追加されました。- 具体的には、
g->stackguard = (uintptr)&attr - size + 4096;
のように、Cスレッドのスタックのベースアドレスからスタックサイズを引いた値にオフセット(4096バイト)を加えることで、スタックガードの境界を設定しています。この4096バイトは、スタックガードページとして確保される領域のサイズを示唆しています。 - Windowsの場合、
g->stackguard = (uintptr)&tmp - STACKSIZE + 4096;
のように、ローカル変数のアドレスと定義済みのSTACKSIZE
(1MBまたは2MB)を使用して計算しています。
- 具体的には、
- これにより、CgoがGoにコールバックする際に使用するg0スタックのガードが、Cスレッドの実際のスタックサイズを考慮して動的に設定されるようになり、スタックオーバーフローの検出精度が向上しました。
これらの変更により、CgoがGoにコールバックする際に、Cコードが大量のスタックを消費しても、g0スタックのガードが適切に機能し、クラッシュが防止されるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルに集中しています。
-
src/pkg/runtime/386/asm.s
およびsrc/pkg/runtime/amd64/asm.s
: Goランタイムの初期化ルーチンにおいて、initcgo
の呼び出しとg0
スタックガードの設定ロジックが変更されています。src/pkg/runtime/386/asm.s
の例:--- a/src/pkg/runtime/386/asm.s +++ b/src/pkg/runtime/386/asm.s @@ -26,12 +26,14 @@ TEXT _rt0_386(SB),7,$0 // we set up GS ourselves. MOVL initcgo(SB), AX TESTL AX, AX - JZ 4(PC) + JZ needtls + PUSHL $runtime·g0(SB) CALL AX + POPL AX // skip runtime·ldt0setup(SB) and tls test after initcgo for non-windows CMPL runtime·iswindows(SB), $0 JEQ ok -\n+needtls: // skip runtime·ldt0setup(SB) and tls test on Plan 9 in all cases CMPL runtime·isplan9(SB), $1 JEQ ok @@ -58,9 +60,15 @@ ok: MOVL CX, m_g0(AX) // create istack out of the OS stack +\t// if there is an initcgo, it had setup stackguard for us +\tMOVL initcgo(SB), AX +\tTESTL AX, AX +\tJNZ stackok LEAL (-64*1024+104)(SP), AX // TODO: 104? MOVL AX, g_stackguard(CX) +stackok: MOVL SP, g_stackbase(CX) +\n CALL runtime·emptyfunc(SB) // fault if stack check is wrong // convention is D is always cleared
-
src/pkg/runtime/cgo/*.c
ファイル群: 各OS/アーキテクチャごとのCgo初期化関数xinitcgo
のシグネチャと実装が変更されています。src/pkg/runtime/cgo/darwin_386.c
の例:--- a/src/pkg/runtime/cgo/darwin_386.c +++ b/src/pkg/runtime/cgo/darwin_386.c @@ -100,12 +100,20 @@ inittls(void)\n }\n \n static void\n-xinitcgo(void)\n+xinitcgo(G *g)\n {\n+\tpthread_attr_t attr;\n+\tsize_t size;\n+\n+\tpthread_attr_init(&attr);\n+\tpthread_attr_getstacksize(&attr, &size);\n+\tg->stackguard = (uintptr)&attr - size + 4096;\n+\tpthread_attr_destroy(&attr);\n+\n \tinittls();\n }\n \n-void (*initcgo)(void) = xinitcgo;\n+void (*initcgo)(G*) = xinitcgo;\n \n void\n libcgo_sys_thread_start(ThreadStart *ts)\n ```
コアとなるコードの解説
アセンブリコード (asm.s
) の変更
アセンブリコードの変更は、Goランタイムの起動シーケンスにおけるg0
スタックガードの設定ロジックを調整しています。
-
initcgo
の呼び出しとg0
の引き渡し: 以前はinitcgo
が引数なしで呼び出されていましたが、修正後はruntime·g0(SB)
(g0
Goルーチン構造体のアドレス)をスタックにプッシュしてからinitcgo
を呼び出し、呼び出し後にポップしています。これは、initcgo
関数がG* g
という引数を受け取るように変更されたためです。これにより、initcgo
はg0
の情報を直接受け取り、そのstackguard
フィールドを更新できるようになります。 -
条件付きスタックガード設定:
initcgo
が存在する場合(TESTL AX, AX
とJNZ stackok
)、Goランタイムは独自の固定スタックガード設定(LEAL (-64*1024+104)(SP), AX
など)をスキップし、initcgo
がスタックガードを設定する責任を持つようにします。これは、CgoがOSのスレッドスタック情報を利用してより正確なスタックガードを設定できるためです。
Cgoランタイムコード (.c
ファイル) の変更
CgoランタイムのCコードの変更は、各OS/アーキテクチャ固有のxinitcgo
関数内で、g0
スタックのガードを動的に設定するロジックを導入しています。
-
xinitcgo(G *g)
シグネチャの変更:xinitcgo
関数がG *g
という引数を受け取るようになりました。このg
は、Goランタイムの内部でg0
として知られる特別なGoルーチン構造体へのポインタです。これにより、xinitcgo
はg0
のstackguard
フィールドに直接アクセスし、その値を設定できるようになります。 -
スタックサイズの取得と
stackguard
の設定:-
POSIX系OS (Darwin, FreeBSD, Linux):
pthread_attr_t
構造体とpthread_attr_init
,pthread_attr_getstacksize
,pthread_attr_destroy
関数を使用して、現在のCスレッドのスタックサイズを取得します。g->stackguard = (uintptr)&attr - size + 4096;
ここで、(uintptr)&attr
はスタック上のローカル変数attr
のアドレス(スタックの現在のトップに近い位置)を示し、そこからsize
(スレッドのスタックサイズ)を引くことでスタックのベースアドレスに近い位置を特定します。さらに+ 4096
というオフセットを加えることで、スタックの末尾から4KB(ガードページサイズ)手前の位置をstackguard
として設定しています。これにより、スタックがこの境界を超えて成長しようとすると、ページフォルトが発生し、スタックオーバーフローが検出されます。 -
Windows: Windowsでは
pthread
は使用できないため、ローカル変数tmp
のアドレスと定義済みのSTACKSIZE
マクロ(1MBまたは2MB)を使用してスタックガードを設定します。g->stackguard = (uintptr)&tmp - STACKSIZE + 4096;
基本的な考え方はPOSIX系OSと同じで、スタックの現在の位置から定義済みのスタックサイズを引いてスタックのベースを推定し、そこに4KBのオフセットを加えることでガードページを設定しています。
-
これらの変更により、CgoがGoにコールバックする際に、Cスレッドの実際のスタックサイズを考慮した上でg0
スタックのガードが設定されるようになり、スタックオーバーフローによるクラッシュが効果的に防止されるようになりました。
関連リンク
- Go Issue #1328: https://github.com/golang/go/issues/1328
- Go CL 5371042: https://golang.org/cl/5371042
参考にした情報源リンク
- Go言語のスタック管理に関するドキュメントや記事 (一般的なGoランタイムのスタック管理、g0スタック、スタックガードの概念について)
- Cgoの内部動作に関するドキュメントや記事 (Cgoにおけるスタック切り替えのメカニズムについて)
- POSIXスレッドの
pthread_attr_getstacksize
関数のドキュメント (Cスレッドのスタックサイズ取得方法について) - Windowsにおけるスレッドスタック管理に関するドキュメント (Windowsでのスタックサイズ推定方法について)
- Go言語のソースコード (特に
src/pkg/runtime
およびsrc/pkg/runtime/cgo
ディレクトリ内のファイル) - Go言語のIssueトラッカー (Issue #1328の詳細な議論について)
- Go言語のコードレビューシステム (CL 5371042のレビューコメントについて)