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

[インデックス 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をコールバックします。

この構造により、以下のような複雑なスタック遷移がテストされます。

  1. Goのテスト関数(testCallbackStack)からstackN関数が呼び出される。
  2. stackN関数内で、指定されたサイズのスタックがGoランタイムによって割り当てられる。
  3. stackN関数からCのcallGoStackCheckが呼び出される(GoからCへの遷移)。
  4. CのcallGoStackCheckからGoのgoStackCheckがコールバックされる(CからGoへの遷移)。この際、GoランタイムはCのスタックからGoのスタックに切り替える必要がある。
  5. goStackCheck内でさらに256バイトのスタックが消費される。

この一連の呼び出しとスタック消費のパターンを、様々な初期スタックサイズ(stackNN)で実行することで、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