[インデックス 16841] ファイルの概要
このコミットは、Go言語のmisc/cgo/test
ディレクトリに、Cgoコールバックにおけるスタック空間の異なる量でのテストを追加するものです。具体的には、GoとCの相互運用において、Goのランタイムが管理するスタック(スプリットスタック)が、Cgoを介したコールバック時に適切に機能するかどうかを検証するための広範なテストケースが追加されています。
コミット
commit 9fe4a9ecdd674efafc173f3c7e99f7b34a6c544a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jul 22 21:53:20 2013 +0400
misc/cgo/test: add test for cgo callbacks with different amount of stack space available
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/11677043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9fe4a9ecdd674efafc173f3c7e99f7b34a6c544a
元コミット内容
misc/cgo/test: add test for cgo callbacks with different amount of stack space available
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/11677043
変更の背景
Go言語は、C言語との相互運用を可能にするCgoというメカニズムを提供しています。Cgoを使用すると、GoのコードからCの関数を呼び出したり、CのコードからGoの関数を呼び出したりすることができます(コールバック)。Goのランタイムは、効率的なスタック管理のために「スプリットスタック(Split Stack)」という技術を採用しています。これは、関数呼び出し時に必要なスタックサイズに応じてスタックを動的に拡張・縮小する仕組みです。
しかし、Cgoを介してGoの関数がCからコールバックされる際、Goランタイムのスタック管理とCのスタック管理の間で複雑な相互作用が発生します。特に、CのスタックフレームからGoの関数が呼び出される場合、Goランタイムは自身のスプリットスタックの制約内で適切にスタックを確保し、管理できる必要があります。もし、Cのスタック空間がGoの関数が必要とするスタック空間と競合したり、GoのスプリットスタックのメカニズムがCgoコールバックのコンテキストで正しく機能しない場合、スタックオーバーフローやクラッシュといった問題が発生する可能性があります。
このコミットは、このような潜在的な問題を特定し、GoランタイムがCgoコールバック時に様々なスタック使用状況下で安定して動作することを保証するために追加されました。特に、Goの関数がCから呼び出された際に、Goの関数が様々な量のスタック空間を使用するシナリオを網羅的にテストすることで、スプリットスタックの挙動が期待通りであることを確認することが目的です。
前提知識の解説
Cgo
Cgoは、GoプログラムがC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。Goのソースファイル内にimport "C"
という行を記述することで、Cのコードを埋め込み、GoとCの間で関数やデータ構造をやり取りできるようになります。
- GoからCの呼び出し: GoのコードからCの関数を呼び出す場合、GoランタイムはCの呼び出し規約に合わせてスタックを調整し、Cの関数を実行します。
- CからGoのコールバック: CのコードからGoの関数を呼び出す場合(コールバック)、GoランタイムはCのスタックからGoのスタックに切り替える必要があります。この切り替えが、スタック管理の複雑さを増す要因となります。
Goのスプリットスタック (Split Stack)
Goのランタイムは、効率的なメモリ使用と高速な関数呼び出しのために「スプリットスタック」というスタック管理モデルを採用していました(Go 1.3以降は連続スタックに移行しましたが、このコミットの時点ではスプリットスタックが主流でした)。
- 動的なスタック拡張: Goの関数は、最初は小さなスタックで開始します。関数がより多くのスタック空間を必要とすると、ランタイムは自動的に新しい、より大きなスタックセグメントを割り当て、既存のスタックセグメントとリンクします。これにより、スタックオーバーフローを回避しつつ、必要な分だけスタックを使用できます。
- スタックの縮小: 関数が終了し、スタック空間が不要になると、未使用のスタックセグメントは解放されます。
- 利点: スタックの初期サイズを小さくできるため、多数のゴルーチンを生成してもメモリ消費を抑えられます。また、スタックオーバーフローによるクラッシュを防ぐことができます。
- 課題: スタックの拡張・縮小にはオーバーヘッドが伴います。特に、Cgoのような外部関数呼び出しとの連携では、スタックの切り替えが複雑になる可能性があります。
スタックオーバーフロー
プログラムが利用可能なスタックメモリを超えてスタックを使用しようとすると発生するエラーです。これにより、プログラムはクラッシュしたり、未定義の動作を引き起こしたりします。Goのスプリットスタックはこれを防ぐためのメカニズムですが、Cgoのような異なるスタック管理モデルを持つ言語との連携では、予期せぬスタック使用パターンが発生し、問題を引き起こす可能性があります。
技術的詳細
このコミットは、GoのスプリットスタックとCgoコールバックの相互作用における堅牢性を検証するためのテストを追加しています。テストの核心は、Goの関数がCからコールバックされる際に、Goの関数が様々な量のスタック空間を消費するシナリオをシミュレートすることです。
Goの関数goStackCheck
は、[256]byte
のバッファを宣言し、use
関数を呼び出すことで、意図的にスタックメモリを消費します。これにより、Goランタイムのスプリットスタックメカニズムがトリガーされることを期待しています。
//export goStackCheck
func goStackCheck() {
// use some stack memory to trigger split stack check
var buf [256]byte
use(buf[:])
}
var Used byte
func use(buf []byte) {
for _, c := range buf {
Used += c
}
}
Cのコード側では、callGoStackCheck
という関数が追加され、GoのgoStackCheck
関数を呼び出します。
void
callGoStackCheck(void)
{
extern void goStackCheck(void);
goStackCheck();
}
そして、Goのテストコードでは、splitTests
というスライスに、様々なサイズのスタックを消費するstackN
関数(Nはスタックサイズ)が大量に定義されています。これらのstackN
関数はそれぞれ、[N]byte
のバッファを宣言し、use
関数を呼び出した後、CのcallGoStackCheck
関数を呼び出します。
func stack4() { var buf [4]byte; use(buf[:]); C.callGoStackCheck() }
func stack8() { var buf [8]byte; use(buf[:]); C.callGoStackCheck() }
// ... (大量のstackN関数が続く)
func stack5000() { var buf [5000]byte; use(buf[:]); C.callGoStackCheck() }
testCallbackStack
関数は、このsplitTests
スライス内のすべての関数を順番に実行します。
func testCallbackStack(t *testing.T) {
// Make cgo call and callback with different amount of stack stack available.
// We do not do any explicit checks, just ensure that it does not crash.
for _, f := range splitTests {
f()
}
}
このテストの目的は、明示的なアサーションを行うことではなく、「クラッシュしないこと」を保証することです。つまり、Cgoを介したコールバックにおいて、Goの関数が様々な量のスタック空間を消費しても、Goランタイムがスタックを適切に管理し、スタックオーバーフローやその他のランタイムエラーを引き起こさないことを確認します。
このテストは、Goランタイムのスタック管理、特にスプリットスタックの挙動が、Cgoとの複雑な相互作用のシナリオにおいても堅牢であることを検証するための重要な追加です。
コアとなるコードの変更箇所
misc/cgo/test/callback.go
callGoStackCheck
というC関数の宣言が追加されました。testCallbackStack
という新しいテスト関数が追加されました。この関数は、splitTests
スライスに定義された多数のstackN
関数を順次呼び出します。goStackCheck
というGo関数が追加されました。これはCからコールバックされる関数で、256バイトのスタックを消費します。use
ヘルパー関数が追加されました。これは、コンパイラによる最適化でスタック割り当てが削除されないように、ダミーのスタック使用をシミュレートします。splitTests
というfunc()
型のスライスが追加されました。このスライスには、4バイトから5000バイトまで4バイト刻みでスタックを消費するstackN
関数が大量に定義されています。各stackN
関数は、指定されたサイズのバッファをスタックに割り当て、use
関数を呼び出し、最終的にCのcallGoStackCheck
を呼び出します。
misc/cgo/test/callback_c.c
callGoStackCheck
というC関数が追加されました。この関数は、GoのgoStackCheck
関数を呼び出すためのラッパーです。
misc/cgo/test/cgo_test.go
TestCallbackStack
テスト関数が、既存のテストスイートに追加されました。これにより、新しいスタックテストが自動的に実行されるようになります。
コアとなるコードの解説
このコミットの核心は、GoのスプリットスタックとCgoコールバックの相互作用を徹底的にテストするための、大量の自動生成されたテストケースです。
callback.go
の以下の部分が特に重要です。
//export goStackCheck
func goStackCheck() {
// use some stack memory to trigger split stack check
var buf [256]byte
use(buf[:])
}
goStackCheck
は、Cgoの//export
ディレクティブにより、Cコードから呼び出し可能なGo関数として公開されます。この関数内で[256]byte
のローカル変数buf
を宣言し、use(buf[:])
を呼び出すことで、Goランタイムに256バイトのスタック空間を確保させます。use
関数は、コンパイラがbuf
の割り当てを最適化して削除しないように、ダミーの処理を行います。これにより、Goの関数がCからコールバックされた際に、Goランタイムがスタックを拡張する必要があるシナリオをシミュレートします。
そして、このテストの大部分を占めるのが、splitTests
スライスとそれに含まれるstackN
関数群です。
var splitTests = []func(){
// Edit .+1,/^}/-1|seq 4 4 5000 | sed \'s/.*/\tstack&,/\' | fmt
stack4, stack8, stack12, stack16, stack20, stack24, stack28,
// ... (大量のstackN関数への参照)
stack4996, stack5000,
}
// Edit .+1,$ | seq 4 4 5000 | sed \'s/.*/func stack&() { var buf [&]byte; use(buf[:]); C.callGoStackCheck() }/\'
func stack4() { var buf [4]byte; use(buf[:]); C.callGoStackCheck() }
func stack8() { var buf [8]byte; use(buf[:]); C.callGoStackCheck() }
// ... (大量のstackN関数定義)
func stack5000() { var buf [5000]byte; use(buf[:]); C.callGoStackCheck() }
これらのstackN
関数は、それぞれ異なるサイズのスタックバッファ(4バイトから5000バイトまで)を割り当て、その後にCのcallGoStackCheck
関数を呼び出します。このcallGoStackCheck
は、さらにGoのgoStackCheck
をコールバックします。
この構造により、以下のような複雑なスタック遷移がテストされます。
- Goのテスト関数(
testCallbackStack
)からstackN
関数が呼び出される。 stackN
関数内で、指定されたサイズのスタックがGoランタイムによって割り当てられる。stackN
関数からCのcallGoStackCheck
が呼び出される(GoからCへの遷移)。- Cの
callGoStackCheck
からGoのgoStackCheck
がコールバックされる(CからGoへの遷移)。この際、GoランタイムはCのスタックからGoのスタックに切り替える必要がある。 goStackCheck
内でさらに256バイトのスタックが消費される。
この一連の呼び出しとスタック消費のパターンを、様々な初期スタックサイズ(stackN
のN
)で実行することで、GoランタイムがCgoコールバックの複雑なシナリオにおいても、スプリットスタックの拡張・縮小を正しく処理し、スタックオーバーフローを起こさないことを検証しています。テストのコメントにある「We do not do any explicit checks, just ensure that it does not crash.」という文言は、このテストがクラッシュしないこと自体を成功の指標としていることを明確に示しています。
関連リンク
- Go言語のCgoに関する公式ドキュメント: https://go.dev/blog/c-go-cgo
- Go言語のスタック管理に関する議論(Go 1.3での連続スタックへの移行など):
- Go 1.3 Release Notes - Runtime: https://go.dev/doc/go1.3#runtime
- Goのスタック管理の進化に関するブログ記事など(例: "Go's work-stealing scheduler" by Dmitry Vyukov, "Go: The Good, Bad and Ugly" by Dave Cheneyなど)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
runtime
およびcmd/cgo
ディレクトリ) - Go言語のIssueトラッカーやメーリングリストでの議論
- Go言語のスタック管理に関する技術ブログや記事
- コミットメッセージと変更されたファイルの内容
- Go CL 11677043: https://golang.org/cl/11677043