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

[インデックス 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に関する知識が必要です。

  1. Goランタイム (Go Runtime): Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。Goランタイムは、Goルーチンのスケジューリング、ガベージコレクション、メモリ管理、システムコールとの連携など、Goプログラムの実行に必要な多くの低レベルな処理を担当します。

  2. Goルーチン (Goroutine): Goルーチンは、Goランタイムによって管理される軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のGoルーチンを同時に実行することも可能です。Goルーチンは、GoランタイムのスケジューラによってOSスレッドにマッピングされて実行されます。

  3. OSスレッド (OS Thread): オペレーティングシステムが管理する実行単位です。Goランタイムは、複数のGoルーチンを少数のOSスレッドに多重化して実行します。Goランタイムは、Goルーチンを実行するために必要なOSスレッドを自身で作成・管理します。

  4. m (Machine) と g (Goroutine): Goランタイムの内部では、mはOSスレッドを表す構造体であり、gはGoルーチンを表す構造体です。Goルーチンが実行される際には、特定のmにアタッチされ、そのmが持つスタックやレジスタなどのコンテキストを利用します。mg0と呼ばれる特別なGoルーチン(スケジューラやランタイムの低レベル処理を実行するためのスタックを持つ)を保持しています。

  5. cgo: cgoは、GoプログラムからC言語の関数を呼び出したり、C言語のプログラムからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoから利用したり、Goで書かれたコードをCから利用したりすることが可能になります。 cgoコールバックとは、C言語側からGo言語で定義された関数を呼び出すことを指します。

  6. スタック分割 (Stack Splitting): Goルーチンのスタックは、必要に応じて動的に拡張・縮小されます。これは「スタック分割」と呼ばれ、Goルーチンが関数呼び出しを行う際に、現在のスタックフレームが小さすぎる場合に新しい、より大きなスタックフレームを割り当て、古いスタックの内容を新しいスタックにコピーするメカニズムです。これにより、Goルーチンは非常に小さな初期スタックで開始でき、メモリ効率が向上します。しかし、この処理はGoランタイムの管理下で行われるため、Goランタイムが認識しないスレッドでGoの関数が呼び出されると、スタック分割のメカニズムが正しく機能せず、クラッシュの原因となることがあります。

  7. #pragma textflag 7: Goのコンパイラ(gc)に対するディレクティブで、このフラグが設定された関数はスタック分割を行わないことを意味します。これは、ランタイムの低レベルな関数や、Goランタイムのコンテキストが完全に確立されていない状況で呼び出される可能性のある関数(例えば、シグナルハンドラやcgoコールバックの初期エントリポイント)で使用されます。スタック分割を行わないことで、mgといったランタイムの内部状態に依存せずに安全に実行できることが保証されます。

技術的詳細

このコミットの主要な変更点は、cgoコールバックがGoランタイムによって作成されていないスレッドから呼び出された場合に、m(現在のOSスレッドに対応するGoランタイムの構造体)がnilになることを検出し、クラッシュする前に診断メッセージを出力するメカニズムを追加したことです。

具体的には、以下の変更が行われています。

  1. src/cmd/cgo/out.go の変更: cgoがGoの関数をCから呼び出せるようにエクスポートする際に生成するCコードに、#pragma textflag 7ディレクティブが追加されました。これは、_cgoexp*という形式のコールバックエントリポイント関数に適用されます。これにより、これらの関数がGoランタイムのスタック分割メカニズムに依存せずに実行されることが保証されます。これは、runtime·cgocallbackが呼び出される前の初期段階で、まだGoランタイムの完全なコンテキストが確立されていない可能性があるため重要です。

  2. 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): mnilでない場合は、通常の処理に進みます。
    • CALL runtime·badcallback(SB): mnilである場合、新しく追加されたruntime·badcallback関数を呼び出します。この関数は診断メッセージを出力し、プログラムを終了させます。
  3. 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·stdcallruntime·WriteFile)を使用して、標準エラー出力(ファイルディスクリプタ2)に診断メッセージを出力します。
    • Plan 9ではruntime·pwriteが使用されています。
    • この関数はメッセージを出力した後、暗黙的にプログラムを終了させるか、クラッシュに繋がるような状態のまま処理を継続します。コミットメッセージの出力例からわかるように、メッセージ出力後もsignal 11 (core dumped)は発生しています。これは、この修正が根本的な問題解決ではなく、診断情報の追加に留まることを示しています。

この変更により、Goランタイムが管理していないスレッドからcgoコールバックが発生した場合、Goプログラムは即座にクラッシュするのではなく、問題の原因を示すメッセージを出力してからクラッシュするようになり、デバッグが容易になりました。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

  1. 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)
    
  2. 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)
    
  3. 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·stdcallruntime·WriteFileを使用する点が異なりますが、基本的な目的は同じです。)

コアとなるコードの解説

src/cmd/cgo/out.go の変更

cgoは、Goの関数をCから呼び出せるようにするためのラッパー関数を生成します。このラッパー関数は、最終的にGoランタイムのruntime·cgocallbackを呼び出します。 追加された#pragma textflag 7は、Goコンパイラに対して、このラッパー関数がスタック分割を行わないように指示します。これは、runtime·cgocallbackが呼び出される時点では、まだGoランタイムの完全なコンテキスト(特にmg)が確立されていない可能性があるためです。スタック分割はmgに依存するため、このディレクティブによって安全な実行が保証されます。

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)であるかをチェックします。 もしBPnilであれば、それはGoランタイムが作成・管理していないOSスレッドからコールバックが呼び出されたことを意味します。この場合、JNE 2(PC)(Jump if Not Equal)命令がスキップされ、CALL runtime·badcallback(SB)命令が実行されます。これにより、Goランタイムがクラッシュする前に診断メッセージを出力する機会が得られます。

src/pkg/runtime/thread_*.c の変更

runtime·badcallback関数は、mnilであった場合に呼び出されるC言語の関数です。 この関数も#pragma textflag 7が適用されており、スタック分割を行いません。これは、この関数が呼び出される時点ではGoランタイムの内部状態が不安定である可能性が高いため、自己完結的に動作する必要があります。 関数内部では、"runtime: cgo callback on thread not created by Go.\\n"というエラーメッセージを定義し、runtime·write(またはOS固有の書き込み関数)を使って標準エラー出力にこのメッセージを書き込みます。 このメッセージが出力された後、プログラムは通常通りクラッシュするか、あるいは不安定な状態のまま処理を継続し、最終的にクラッシュに至ります。このコミットの目的は、クラッシュを回避することではなく、クラッシュの原因を明確にすることにあります。

関連リンク

参考にした情報源リンク

  • Go言語のcgoドキュメント: https://pkg.go.dev/cmd/cgo
  • Go言語のランタイムに関するドキュメントやブログ記事: Goの内部動作、特にスケジューラ、mgに関する情報は、公式ドキュメントやGo開発者によるブログ記事(例: Russ Coxのブログ)で詳しく解説されています。
  • Goのソースコード: 実際の動作を理解するためには、Goのランタイムとcgoのソースコード自体が最も正確な情報源となります。