[インデックス 12525] ファイルの概要
このコミットは、Go言語のcgo
(C言語との相互運用機能)およびruntime
(ランタイム)に関する改善であり、Goランタイムが生成していないスレッドからcgo
コールバックが実行された際に、より診断に役立つメッセージを出力するように変更を加えるものです。これにより、クラッシュが発生する前に問題の根本原因を特定しやすくなります。
コミット
commit 9b73238daa6a5d08eb2265fc38577cb6003f0d23
Author: Russ Cox <rsc@golang.org>
Date: Thu Mar 8 12:12:40 2012 -0500
cgo, runtime: diagnose callback on non-Go thread
Before:
$ go run x.go
signal 11 (core dumped)
$
After:
$ go run x.go
runtime: cgo callback on thread not created by Go.
signal 11 (core dumped)
$
For issue 3068.
Not a fix, but as much of a fix as we can do before Go 1.
R=golang-dev, rogpeppe, gri
CC=golang-dev
https://golang.org/cl/5781047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9b73238daa6a5d08eb2265fc38577cb6003f0d23
元コミット内容
このコミットは、GoプログラムがGoランタイムによって管理されていない(Goが作成していない)スレッドからcgo
コールバックを受け取った際に発生するクラッシュ(signal 11 (core dumped)
)に対して、より詳細な診断メッセージを出力するように改善するものです。
変更前は、単にセグメンテーション違反(signal 11
)でクラッシュするだけでしたが、変更後はクラッシュする前に「runtime: cgo callback on thread not created by Go.
」というメッセージを出力するようになります。これは、Go 1のリリース前に行われた暫定的な修正であり、根本的な解決ではないものの、デバッグの助けとなることを意図しています。
この変更は、GoのIssue 3068に関連しています。
変更の背景
Go言語は、そのランタイムがGoルーチン(goroutine)のスケジューリングやメモリ管理を効率的に行うために、自身が管理するOSスレッド上で動作することを前提としています。しかし、cgo
を通じてC言語などの外部ライブラリを呼び出す場合、その外部ライブラリがGoランタイムが認識しない形で新しいOSスレッドを作成し、そのスレッドからGoの関数(コールバック)を呼び出す可能性があります。
このような状況が発生すると、Goランタイムは予期しないスレッドコンテキストで動作しようとし、Goルーチンやm
(machine、OSスレッドを表すGoランタイムの構造体)、g
(goroutine)といった内部状態にアクセスできず、結果としてセグメンテーション違反などのクラッシュを引き起こしていました。
このコミットの背景にあるのは、Go 1リリース前の段階で、このようなクラッシュが発生した際に、開発者が問題の原因を特定しやすくするための診断情報の強化です。根本的な解決策(Goランタイムが外部スレッドからのコールバックを適切に処理できるようにする)はより複雑であり、Go 1のリリーススケジュールには間に合わないため、まずはデバッグを支援するためのメッセージ追加という形で対応されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の内部動作とcgo
に関する知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。Goランタイムは、Goルーチンのスケジューリング、ガベージコレクション、メモリ管理、システムコールとの連携など、Goプログラムの実行に必要な多くの低レベルな処理を担当します。
-
Goルーチン (Goroutine): Goルーチンは、Goランタイムによって管理される軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のGoルーチンを同時に実行することも可能です。Goルーチンは、GoランタイムのスケジューラによってOSスレッドにマッピングされて実行されます。
-
OSスレッド (OS Thread): オペレーティングシステムが管理する実行単位です。Goランタイムは、複数のGoルーチンを少数のOSスレッドに多重化して実行します。Goランタイムは、Goルーチンを実行するために必要なOSスレッドを自身で作成・管理します。
-
m
(Machine) とg
(Goroutine): Goランタイムの内部では、m
はOSスレッドを表す構造体であり、g
はGoルーチンを表す構造体です。Goルーチンが実行される際には、特定のm
にアタッチされ、そのm
が持つスタックやレジスタなどのコンテキストを利用します。m
はg0
と呼ばれる特別なGoルーチン(スケジューラやランタイムの低レベル処理を実行するためのスタックを持つ)を保持しています。 -
cgo
:cgo
は、GoプログラムからC言語の関数を呼び出したり、C言語のプログラムからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoから利用したり、Goで書かれたコードをCから利用したりすることが可能になります。cgo
コールバックとは、C言語側からGo言語で定義された関数を呼び出すことを指します。 -
スタック分割 (Stack Splitting): Goルーチンのスタックは、必要に応じて動的に拡張・縮小されます。これは「スタック分割」と呼ばれ、Goルーチンが関数呼び出しを行う際に、現在のスタックフレームが小さすぎる場合に新しい、より大きなスタックフレームを割り当て、古いスタックの内容を新しいスタックにコピーするメカニズムです。これにより、Goルーチンは非常に小さな初期スタックで開始でき、メモリ効率が向上します。しかし、この処理はGoランタイムの管理下で行われるため、Goランタイムが認識しないスレッドでGoの関数が呼び出されると、スタック分割のメカニズムが正しく機能せず、クラッシュの原因となることがあります。
-
#pragma textflag 7
: Goのコンパイラ(gc
)に対するディレクティブで、このフラグが設定された関数はスタック分割を行わないことを意味します。これは、ランタイムの低レベルな関数や、Goランタイムのコンテキストが完全に確立されていない状況で呼び出される可能性のある関数(例えば、シグナルハンドラやcgo
コールバックの初期エントリポイント)で使用されます。スタック分割を行わないことで、m
やg
といったランタイムの内部状態に依存せずに安全に実行できることが保証されます。
技術的詳細
このコミットの主要な変更点は、cgo
コールバックがGoランタイムによって作成されていないスレッドから呼び出された場合に、m
(現在のOSスレッドに対応するGoランタイムの構造体)がnil
になることを検出し、クラッシュする前に診断メッセージを出力するメカニズムを追加したことです。
具体的には、以下の変更が行われています。
-
src/cmd/cgo/out.go
の変更:cgo
がGoの関数をCから呼び出せるようにエクスポートする際に生成するCコードに、#pragma textflag 7
ディレクティブが追加されました。これは、_cgoexp*
という形式のコールバックエントリポイント関数に適用されます。これにより、これらの関数がGoランタイムのスタック分割メカニズムに依存せずに実行されることが保証されます。これは、runtime·cgocallback
が呼び出される前の初期段階で、まだGoランタイムの完全なコンテキストが確立されていない可能性があるため重要です。 -
src/pkg/runtime/asm_386.s
およびsrc/pkg/runtime/asm_amd64.s
の変更:runtime·cgocallback
というアセンブリ関数は、CからGoへのコールバックの主要なエントリポイントです。この関数内で、現在のOSスレッドに対応するm
ポインタ(BP
レジスタに格納される)がnil
であるかどうかをチェックするロジックが追加されました。CMPL BP, $0
(386) またはCMPQ BP, $0
(amd64):m
ポインタがnil
(0)であるかを比較します。JNE 2(PC)
:m
がnil
でない場合は、通常の処理に進みます。CALL runtime·badcallback(SB)
:m
がnil
である場合、新しく追加されたruntime·badcallback
関数を呼び出します。この関数は診断メッセージを出力し、プログラムを終了させます。
-
src/pkg/runtime/thread_*.c
ファイル群の変更: 各OS(Darwin, FreeBSD, Linux, NetBSD, OpenBSD, Plan 9, Windows)に対応するthread_*.c
ファイルに、runtime·badcallback
関数が追加されました。- この関数は、
static int8 badcallback[] = "runtime: cgo callback on thread not created by Go.\\n";
という文字列を定義しています。 #pragma textflag 7
が適用されており、この関数もスタック分割を行いません。これは、この関数がGoランタイムのコンテキストが壊れている可能性のある状況で呼び出されるため、非常に重要です。runtime·write
(またはWindowsの場合はruntime·stdcall
とruntime·WriteFile
)を使用して、標準エラー出力(ファイルディスクリプタ2)に診断メッセージを出力します。- Plan 9では
runtime·pwrite
が使用されています。 - この関数はメッセージを出力した後、暗黙的にプログラムを終了させるか、クラッシュに繋がるような状態のまま処理を継続します。コミットメッセージの出力例からわかるように、メッセージ出力後も
signal 11 (core dumped)
は発生しています。これは、この修正が根本的な問題解決ではなく、診断情報の追加に留まることを示しています。
- この関数は、
この変更により、Goランタイムが管理していないスレッドからcgo
コールバックが発生した場合、Goプログラムは即座にクラッシュするのではなく、問題の原因を示すメッセージを出力してからクラッシュするようになり、デバッグが容易になりました。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードスニペットは以下の通りです。
-
src/cmd/cgo/out.go
:cgo
が生成するCコードに#pragma textflag 7
を追加。--- a/src/cmd/cgo/out.go +++ b/src/cmd/cgo/out.go @@ -573,8 +573,9 @@ func (p *Package) writeExports(fgo2, fc, fm *os.File) { goname = "_cgoexpwrap" + cPrefix + "_" + fn.Recv.List[0].Names[0].Name + "_" + goname } fmt.Fprintf(fc, "#pragma dynexport %s %s\\n", goname, goname) - fmt.Fprintf(fc, "extern void ·%s();\\n", goname) - fmt.Fprintf(fc, "\\nvoid\\n") + fmt.Fprintf(fc, "extern void ·%s();\\n\\n", goname) + fmt.Fprintf(fc, "#pragma textflag 7\\n") // no split stack, so no use of m or g + fmt.Fprintf(fc, "void\\n") fmt.Fprintf(fc, "_cgoexp%s_%s(void *a, int32 n)\\n", cPrefix, exp.ExpName) fmt.Fprintf(fc, "{\\n") fmt.Fprintf(fc, "\\truntime·cgocallback(·%s, a, n);\\n", goname)
-
src/pkg/runtime/asm_386.s
およびsrc/pkg/runtime/asm_amd64.s
:runtime·cgocallback
関数内でm
ポインタのnil
チェックとruntime·badcallback
の呼び出しを追加。src/pkg/runtime/asm_386.s
:--- a/src/pkg/runtime/asm_386.s +++ b/src/pkg/runtime/asm_386.s @@ -425,6 +425,14 @@ TEXT runtime·cgocallback(SB),7,$12 // Save current m->g0->sched.sp on stack and then set it to SP. get_tls(CX) MOVL m(CX), BP + + // If m is nil, it is almost certainly because we have been called + // on a thread that Go did not create. We're going to crash as + // soon as we try to use m; instead, try to print a nice error and exit. + CMPL BP, $0 + JNE 2(PC) + CALL runtime·badcallback(SB) + MOVL m_g0(BP), SI PUSHL (g_sched+gobuf_sp)(SI) MOVL SP, (g_sched+gobuf_sp)(SI)
src/pkg/runtime/asm_amd64.s
:--- a/src/pkg/runtime/asm_amd64.s +++ b/src/pkg/runtime/asm_amd64.s @@ -471,6 +471,14 @@ TEXT runtime·cgocallback(SB),7,$24 // Save current m->g0->sched.sp on stack and then set it to SP. get_tls(CX) MOVQ m(CX), BP + + // If m is nil, it is almost certainly because we have been called + // on a thread that Go did not create. We're going to crash as + // soon as we try to use m; instead, try to print a nice error and exit. + CMPQ BP, $0 + JNE 2(PC) + CALL runtime·badcallback(SB) + MOVQ m_g0(BP), SI PUSHQ (g_sched+gobuf_sp)(SI) MOVQ SP, (g_sched+gobuf_sp)(SI)
-
src/pkg/runtime/thread_darwin.c
(および他のOSのthread_*.c
ファイル):runtime·badcallback
関数の定義を追加。--- a/src/pkg/runtime/thread_darwin.c +++ b/src/pkg/runtime/thread_darwin.c @@ -477,3 +477,13 @@ runtime·setprof(bool on)\n else\n \truntime·sigprocmask(SIG_BLOCK, &sigset_prof, nil);\n }\n+\n+static int8 badcallback[] = "runtime: cgo callback on thread not created by Go.\\n";\n+\n+// This runs on a foreign stack, without an m or a g. No stack split.\n+#pragma textflag 7\n+void\n+runtime·badcallback(void)\n+{\n+\truntime·write(2, badcallback, sizeof badcallback - 1);\n+}\
(Windows版は
runtime·stdcall
とruntime·WriteFile
を使用する点が異なりますが、基本的な目的は同じです。)
コアとなるコードの解説
src/cmd/cgo/out.go
の変更
cgo
は、Goの関数をCから呼び出せるようにするためのラッパー関数を生成します。このラッパー関数は、最終的にGoランタイムのruntime·cgocallback
を呼び出します。
追加された#pragma textflag 7
は、Goコンパイラに対して、このラッパー関数がスタック分割を行わないように指示します。これは、runtime·cgocallback
が呼び出される時点では、まだGoランタイムの完全なコンテキスト(特にm
とg
)が確立されていない可能性があるためです。スタック分割はm
とg
に依存するため、このディレクティブによって安全な実行が保証されます。
src/pkg/runtime/asm_*.s
の変更
runtime·cgocallback
は、CからGoへのコールバックの最初のアセンブリレベルのエントリポイントです。この関数は、Goランタイムが管理するOSスレッド(m
)のコンテキストを確立しようとします。
変更点では、get_tls(CX)
でスレッドローカルストレージから現在のm
ポインタを取得し、BP
レジスタに格納します。その直後、CMPL BP, $0
(32ビット)またはCMPQ BP, $0
(64ビット)命令でBP
レジスタがnil
(0)であるかをチェックします。
もしBP
がnil
であれば、それはGoランタイムが作成・管理していないOSスレッドからコールバックが呼び出されたことを意味します。この場合、JNE 2(PC)
(Jump if Not Equal)命令がスキップされ、CALL runtime·badcallback(SB)
命令が実行されます。これにより、Goランタイムがクラッシュする前に診断メッセージを出力する機会が得られます。
src/pkg/runtime/thread_*.c
の変更
runtime·badcallback
関数は、m
がnil
であった場合に呼び出されるC言語の関数です。
この関数も#pragma textflag 7
が適用されており、スタック分割を行いません。これは、この関数が呼び出される時点ではGoランタイムの内部状態が不安定である可能性が高いため、自己完結的に動作する必要があります。
関数内部では、"runtime: cgo callback on thread not created by Go.\\n"
というエラーメッセージを定義し、runtime·write
(またはOS固有の書き込み関数)を使って標準エラー出力にこのメッセージを書き込みます。
このメッセージが出力された後、プログラムは通常通りクラッシュするか、あるいは不安定な状態のまま処理を継続し、最終的にクラッシュに至ります。このコミットの目的は、クラッシュを回避することではなく、クラッシュの原因を明確にすることにあります。
関連リンク
- Go Issue 3068: https://github.com/golang/go/issues/3068 このコミットが対応しているGoのIssueです。詳細な議論や背景情報が記載されている可能性があります。
参考にした情報源リンク
- Go言語の
cgo
ドキュメント: https://pkg.go.dev/cmd/cgo - Go言語のランタイムに関するドキュメントやブログ記事: Goの内部動作、特にスケジューラ、
m
、g
に関する情報は、公式ドキュメントやGo開発者によるブログ記事(例: Russ Coxのブログ)で詳しく解説されています。 - Goのソースコード: 実際の動作を理解するためには、Goのランタイムと
cgo
のソースコード自体が最も正確な情報源となります。