[インデックス 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ディレクティブに関する情報