[インデックス 17772] ファイルの概要
このコミットは、GoランタイムのCGO(C言語との相互運用)コールバック関数、特に_cgo_allocate
と_cgo_panic
にNOSPLIT
フラグを付与することで、スタック管理の堅牢性を向上させるものです。これにより、CコードからGo関数が呼び出される際のスタックオーバーフローのリスクを軽減し、特にパニック処理の信頼性を高めます。
コミット
runtime/cgo: mark callback functions as NOSPLIT
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/14448044
---
misc/cgo/test/callback.go | 14 ++++++++++++++
misc/cgo/test/callback_c.c | 14 ++++++++++++++
misc/cgo/test/cgo_test.go | 1 +
src/pkg/runtime/cgo/callbacks.c | 3 +++
4 files changed, 32 insertions(+)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cb309173874e3dd0dca19c904fd368b59e373906
元コミット内容
runtime/cgo: mark callback functions as NOSPLIT
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/14448044
変更の背景
GoとC言語を連携させるCGOでは、CコードからGo関数を呼び出す「コールバック」の仕組みが重要です。しかし、Goの関数は通常、必要に応じてスタックを動的に拡張(スプリット)しますが、CGOを介した呼び出しでは、GoランタイムがCスタック上で動作しているため、通常のスタック拡張メカニズムがうまく機能しない場合があります。
特に、メモリ確保を行う_cgo_allocate
や、Goのパニックを処理する_cgo_panic
のような重要なコールバック関数が、スタック拡張に失敗すると、プログラムのクラッシュや予期せぬ動作を引き起こす可能性があります。このコミットは、これらの関数がスタック拡張を試みないようにNOSPLIT
フラグを付与することで、CGOコールバックの堅牢性を高めることを目的としています。これにより、CコードからGoのパニックを安全に発生させたり、メモリを確保したりできるようになります。
前提知識の解説
CGO
CGOは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoから利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。
Goランタイムとスタック管理
Goの関数は、実行時に必要に応じてスタックを動的に拡張します。これは「スタックスプリット」と呼ばれ、関数が呼び出される際に、現在のスタックフレームが小さすぎる場合、より大きなスタックフレームを割り当てて、古いスタックの内容を新しいスタックにコピーします。これにより、Goプログラムは固定サイズのスタックを持つ他の言語と比較して、スタックオーバーフローのリスクを軽減し、より効率的なメモリ利用を実現します。
NOSPLIT
pragma
NOSPLIT
は、Goコンパイラに対する指示(pragma)の一つで、特定の関数がスタックスプリットを行わないように指定します。つまり、その関数は呼び出し時にスタックの拡張を試みず、現在のスタックフレーム内で実行されます。これは、スタックスプリットが不適切な状況(例えば、ランタイムの低レベルな部分や、CGOコールバックのようにCスタック上で動作する場合)で使用されます。NOSPLIT
関数は、呼び出し元が十分なスタック空間を確保していることを前提とします。
crosscall2
crosscall2
は、CGOの内部で使用される低レベルな関数で、CコードからGo関数を呼び出すための橋渡しをします。これは、GoのランタイムがCスタック上でGo関数を実行するために必要なコンテキストの切り替えや引数の受け渡しを行います。
_cgo_panic
_cgo_panic
は、CGOの内部で使用されるGoランタイム関数で、CコードからGoのパニックを発生させるために呼び出されます。SWIG(Simplified Wrapper and Interface Generator)のようなツールが、C/C++コードからGoの例外的な状況をGoのパニックとして伝播させる際に利用します。
技術的詳細
このコミットの核心は、src/pkg/runtime/cgo/callbacks.c
ファイル内の_cgo_allocate
と_cgo_panic
という2つのCGOコールバック関数に#pragma textflag NOSPLIT
ディレクティブを追加したことです。
通常、Go関数が呼び出されると、Goランタイムは現在のスタックの残量をチェックし、必要であればスタックを拡張します。しかし、CGOコールバックの場合、Go関数はCスタック上で実行されます。この状況でGoランタイムが通常のスタック拡張ロジックを実行しようとすると、Cスタックの管理と競合したり、予期せぬ動作を引き起こしたりする可能性があります。
NOSPLIT
フラグをこれらの関数に適用することで、コンパイラはこれらの関数がスタックスプリットを行わないようにします。これは、これらの関数が非常に短く、スタックをほとんど消費しないか、または呼び出し元(この場合はcrosscall2
を介したCコード)が十分なスタック空間を確保していることを前提としています。
特に_cgo_panic
の場合、CコードからGoのパニックを安全に発生させるためには、パニック処理自体がスタックオーバーフローによって中断されないことが極めて重要です。NOSPLIT
を適用することで、パニック処理が開始された際に、スタック拡張の失敗による二次的な問題を防ぎ、パニックが確実にGoランタイムによって捕捉・処理されるようにします。
また、misc/cgo/test/callback_c.c
にcallPanic
という新しいC関数が追加され、これが_cgo_panic
をcrosscall2
経由で呼び出すテストケースが追加されました。これは、SWIGのようなツールがCからGoのパニックをトリガーするシナリオをシミュレートしており、このコミットの変更が正しく機能することを確認するためのものです。
コアとなるコードの変更箇所
misc/cgo/test/callback.go
:callPanic
という新しいC関数をインポートし、testPanicFromC
というテスト関数を追加。misc/cgo/test/callback_c.c
:callPanic
というC関数を実装。この関数はcrosscall2
を介して_cgo_panic
を呼び出し、Goのパニックをトリガーする。misc/cgo/test/cgo_test.go
:TestPanicFromC
テスト関数を登録。src/pkg/runtime/cgo/callbacks.c
:#include "../../../cmd/ld/textflag.h"
を追加。_cgo_allocate
関数の定義の前に#pragma textflag NOSPLIT
を追加。_cgo_panic
関数の定義の前に#pragma textflag NOSPLIT
を追加。
コアとなるコードの解説
src/pkg/runtime/cgo/callbacks.c
の変更
#include "../../../cmd/ld/textflag.h"
// ...
#pragma textflag NOSPLIT
void
_cgo_allocate(void *a, int32 n)
{
// ...
}
// ...
#pragma textflag NOSPLIT
void
_cgo_panic(void *a, int32 n)
{
// ...
}
この変更がこのコミットの最も重要な部分です。#pragma textflag NOSPLIT
は、Goコンパイラ(正確にはリンカ)に対して、続く関数(_cgo_allocate
と_cgo_panic
)がスタックスプリットを行わないように指示します。これにより、これらの関数がCGOコールバックとしてCスタック上で実行される際に、Goランタイムが不必要なスタック拡張を試みて問題を引き起こすことを防ぎます。
textflag.h
は、Goのリンカが認識する特殊なフラグを定義しており、NOSPLIT
はその一つです。
misc/cgo/test/callback_c.c
の変更
/* Test calling panic from C. This is what SWIG does. */
extern void crosscall2(void (*fn)(void *, int), void *, int);
extern void _cgo_panic(void *, int);
void
callPanic(void)
{
struct { const char *p; } a;
a.p = "panic from C";
crosscall2(_cgo_panic, &a, sizeof a);
*(int*)1 = 1; // This line is unreachable if panic works correctly
}
callPanic
関数は、CGOのcrosscall2
メカニズムを使用して、Goランタイムの_cgo_panic
関数を呼び出しています。これは、CコードからGoのパニックを意図的に発生させるためのテストケースです。_cgo_panic
に渡される引数は、パニックメッセージを含む構造体です。*(int*)1 = 1;
の行は、_cgo_panic
が正常に機能すれば到達しないコードであり、パニックが正しく処理されたことを確認するためのものです。
misc/cgo/test/callback.go
の変更
func testPanicFromC(t *testing.T) {
defer func() {
r := recover()
if r == nil {
t.Fatal("did not panic")
}
if r.(string) != "panic from C" {
t.Fatal("wrong panic:", r)
}
}()
C.callPanic()
}
このGoのテスト関数は、CGOを介してCのcallPanic
関数を呼び出します。defer
とrecover()
を使用して、Goのパニックが正しく捕捉され、そのメッセージが期待通りであるかを確認しています。これにより、CからGoへのパニック伝播がNOSPLIT
の変更後も正しく機能することが保証されます。
関連リンク
- Go CGO ドキュメント: https://pkg.go.dev/cmd/cgo
- Go ランタイムのスタック管理に関する議論 (Go issue tracker など): 関連するGoのIssueやデザインドキュメントを検索すると、スタックスプリットやCGOのスタックに関する詳細な情報が見つかる可能性があります。
参考にした情報源リンク
- Goのソースコード (特に
src/pkg/runtime/cgo/callbacks.c
およびcmd/ld/textflag.h
) - GoのCGOに関する公式ドキュメント
- Goのスタック管理に関する技術記事やブログポスト
- SWIGのGoバインディングに関するドキュメント (CからGoのパニックをトリガーするメカニズムの理解のため)
- Goのコミット履歴と関連するコードレビュー (CL 14448044)
- Goの
textflag
に関する情報 (例:go tool compile -help
や関連するGoのIssue) - Goの
crosscall2
に関する情報 (Goのソースコード内のコメントや関連するGoのIssue) - Goの
recover
とpanic
に関するドキュメント - Goのテストフレームワークに関するドキュメント
- C言語の
#pragma
ディレクティブに関する情報