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

[インデックス 17080] ファイルの概要

このコミットは、GoランタイムにおけるCgoプログラムのトレースバックに関する問題を修正するものです。具体的には、Cgo呼び出し中に発生するパニックやトレースバックの挙動を改善し、より正確なスタックトレースが得られるようにするための変更が含まれています。

コミット

commit 326ae8d14e17227086239757ef2f131028997a72
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Aug 8 00:31:52 2013 +0400

    runtime: fix traceback in cgo programs
    Fixes #6061.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/12609043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/326ae8d14e17227086239757ef2f131028997a72

元コミット内容

runtime: fix traceback in cgo programs
Fixes #6061.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12609043

変更の背景

このコミットは、GoのIssue #6061「runtime: traceback in cgo programs is broken」を修正するために行われました。GoプログラムがCgo(C言語との相互運用)を使用している場合、パニックが発生した際に生成されるスタックトレース(トレースバック)が不正確であるという問題がありました。特に、Cgo呼び出し中にGoランタイムがパニックを処理しようとすると、スタック情報が正しく取得できず、デバッグが困難になる状況が発生していました。

Goランタイムは、Goルーチン(goroutine)のスタックを管理し、パニック発生時にはそのスタックを辿ってトレースバックを生成します。しかし、Cgo呼び出し中はGoルーチンがCコードを実行している状態であり、Goランタイムのスタック管理とは異なるコンテキストで動作します。このため、CgoとGoの境界を越えてパニックが発生した場合に、Goランタイムが正確なスタック情報を取得できないことが問題となっていました。

この問題は、Goプログラムが外部のCライブラリを利用する際に、予期せぬエラーやクラッシュが発生した場合のデバッグ体験を著しく損なうものでした。開発者は、不正確なトレースバックから問題の原因を特定することが難しく、生産性に影響を与えていました。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理する中核的なコンポーネントです。これには、ガベージコレクション、スケジューラ(Goルーチンの管理)、メモリ管理、そしてパニック処理などが含まれます。GoルーチンはGoランタイムによって管理される軽量なスレッドであり、そのスタックもランタイムが管理します。

Cgo

Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。Cgoを使用すると、既存のCライブラリをGoプロジェクトに統合したり、パフォーマンスが重要な部分をCで記述したりすることができます。

Cgoの呼び出しは、GoルーチンがGoの実行コンテキストからCの実行コンテキストに切り替わることを意味します。この切り替えには、スタックの切り替えやレジスタの状態の保存など、低レベルな処理が伴います。

トレースバック (Traceback) / スタックトレース (Stack Trace)

トレースバック(またはスタックトレース)は、プログラムがエラーやパニックで停止した際に、その時点までの関数呼び出しの履歴を示すものです。これにより、どの関数がどの関数を呼び出し、最終的にどこでエラーが発生したのかを特定できます。デバッグにおいて非常に重要な情報です。

Goランタイムは、Goルーチンのスタックフレームを解析してトレースバックを生成します。各スタックフレームには、関数名、ファイル名、行番号などの情報が含まれます。

runtime.Stack 関数

runtime.Stack は、現在のGoルーチンのスタックトレースをバイトスライスに書き込むGoの標準ライブラリ関数です。デバッグやログ出力の際に、プログラムの実行パスを把握するために使用されます。

g 構造体と m 構造体

Goランタイムの内部では、Goルーチンはg構造体で表現され、OSスレッドはm構造体で表現されます。gはGoルーチンの状態(スタックポインタ、プログラムカウンタ、ステータスなど)を保持し、mはOSスレッドの状態(現在のGoルーチン、スケジューラへのポインタなど)を保持します。

Cgo呼び出しが行われる際、Goルーチン(g)はGsyscallステータスに遷移し、OSスレッド(m)はCコードの実行に専念します。この状態遷移とスタックの管理が、トレースバックの正確性に影響を与えます。

技術的詳細

このコミットの技術的な核心は、Cgo呼び出し中にGoルーチンがパニックを起こした場合に、Goランタイムがスタック情報を正しく取得できるようにすることです。

変更は主に以下の2つのファイルで行われています。

  1. src/pkg/runtime/panic.c:

    • runtime·startpanic 関数内で、パニック開始時に現在のGoルーチン(g)のwritebufnilに設定する行が追加されています。writebufは、Goルーチンが書き込みバッファを持っている場合にそれを指すポインタです。Cgo呼び出し中にパニックが発生した場合、このバッファの状態がトレースバックの生成に影響を与える可能性があったため、これをクリアすることで、クリーンな状態からトレースバックが開始されるようにしています。
  2. src/pkg/runtime/proc.c:

    • runtime·newextram 関数内で、新しいGoルーチン(gp)が作成される際に、gp->syscallpc, gp->syscallsp, gp->syscallstack, gp->syscallguard の各フィールドが設定されるようになりました。
      • gp->syscallpc: システムコール(Cgo呼び出しも含む)に入る直前のプログラムカウンタ。
      • gp->syscallsp: システムコールに入る直前のスタックポインタ。
      • gp->syscallstack: システムコールに入る直前のスタックのベースアドレス。
      • gp->syscallguard: システムコールに入る直前のスタックガード(スタックオーバーフロー検出用)。
    • これらのフィールドは、GoルーチンがCgo呼び出し中にパニックを起こした場合に、GoランタイムがGo側のスタックフレームを正確に再構築するために使用されます。Cgo呼び出しからGoに戻る際に、これらの情報を使ってGoルーチンの状態を復元するのと同様に、パニック時にもこれらの情報が利用されます。
    • また、gp->goidgp->racectx の設定が、runtime·newextram 関数の後半から、上記のsyscallpcなどの設定の直後に移動されています。これは、GoルーチンのIDやレース検出コンテキストの設定が、Goルーチンのシステムコール関連の状態設定と論理的に関連付けられるようにするためと考えられます。

これらの変更により、Cgo呼び出し中にGoルーチンがパニック状態に陥った場合でも、Goランタイムはsyscallpcなどの情報を用いてGo側のスタックフレームを正確に特定し、適切なトレースバックを生成できるようになります。

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

src/pkg/runtime/panic.c の変更

--- a/src/pkg/runtime/panic.c
+++ b/src/pkg/runtime/panic.c
@@ -415,6 +415,8 @@ runtime·startpanic(void)
 		runtime·exit(3);
 	}
 	m->dying = 1;
+	if(g != nil)
+		g->writebuf = nil;
 	runtime·xadd(&runtime·panicking, 1);
 	runtime·lock(&paniclk);
 }

src/pkg/runtime/proc.c の変更

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -712,11 +712,18 @@ runtime·newextram(void)
 	gp->sched.sp = gp->stackbase;
 	gp->sched.lr = 0;
 	gp->sched.g = gp;
+	gp->syscallpc = gp->sched.pc;
+	gp->syscallsp = gp->sched.sp;
+	gp->syscallstack = gp->stackbase;
+	gp->syscallguard = gp->stackguard;
 	gp->status = Gsyscall;
 	mp->curg = gp;
 	mp->locked = LockInternal;
 	mp->lockedg = gp;
 	gp->lockedm = mp;
+	gp->goid = runtime·xadd64(&runtime·sched.goidgen, 1);
+	if(raceenabled)
+		gp->racectx = runtime·racegostart(runtime·newextram);
 	// put on allg for garbage collector
 	runtime·lock(&runtime·sched);
 	if(runtime·lastg == nil)
@@ -725,9 +732,6 @@ runtime·newextram(void)
 		runtime·lastg->alllink = gp;
 	runtime·lastg = gp;
 	runtime·unlock(&runtime·sched);
-	gp->goid = runtime·xadd64(&runtime·sched.goidgen, 1);\n-	if(raceenabled)\n-		gp->racectx = runtime·racegostart(runtime·newextram);
 
 	// Add m to the extra list.
 	mnext = lockextra(true);

新規追加されたテストファイル

src/pkg/runtime/crash_cgo_test.goTestCgoTraceback という新しいテストケースが追加されています。このテストは、Cgo関数を呼び出した後にruntime.Stackを呼び出し、その結果が期待通りであることを確認することで、Cgoプログラムにおけるトレースバックの修正を検証します。

func TestCgoTraceback(t *testing.T) {
	got := executeTest(t, cgoTracebackSource, nil)
	want := "OK\n"
	if got != want {
		t.Fatalf("expected %q, but got %q", want, got)
	}
}

const cgoTracebackSource = `
package main

/* void foo(void) {} */
import "C"

import (
	"fmt"
	"runtime"
)

func main() {
	C.foo()
	buf := make([]byte, 1)
	runtime.Stack(buf, true)
	fmt.Printf("OK\n")
}
`

このテストは、Cgo関数C.foo()を呼び出した直後にruntime.Stackを呼び出し、スタックトレースが正しく取得できることを確認しています。OK\nという出力は、プログラムがクラッシュせずに正常に実行され、runtime.Stackが呼び出されたことを示唆しています。このテストの目的は、runtime.StackがCgo呼び出し後でも正しく機能し、トレースバックが壊れていないことを保証することです。

コアとなるコードの解説

src/pkg/runtime/panic.c の変更点

runtime·startpanic 関数は、Goプログラムでパニックが発生した際に最初に呼び出されるランタイム関数です。この関数内でif(g != nil) g->writebuf = nil;という行が追加されました。

  • g は現在のGoルーチンを表すポインタです。
  • g->writebuf は、Goルーチンが内部的に使用する書き込みバッファを指すポインタです。
  • この変更は、パニック処理を開始する際に、もし現在のGoルーチンが書き込みバッファを保持している場合、それをクリアすることを意味します。Cgo呼び出し中にパニックが発生した場合、このバッファの状態が不整合を引き起こし、トレースバックの生成を妨げる可能性がありました。nilに設定することで、パニック処理がクリーンな状態から開始され、トレースバックの生成が妨げられないようにしています。

src/pkg/runtime/proc.c の変更点

runtime·newextram 関数は、新しいOSスレッド(m)がGoルーチン(g)を実行するために準備される際に呼び出されます。特に、Cgo呼び出しのようなシステムコールに入るGoルーチンのコンテキストを初期化する際に重要です。

追加された以下の行が重要です。

	gp->syscallpc = gp->sched.pc;
	gp->syscallsp = gp->sched.sp;
	gp->syscallstack = gp->stackbase;
	gp->syscallguard = gp->stackguard;
  • gp は新しく作成されるGoルーチン(g構造体)へのポインタです。
  • gp->sched.pcgp->sched.sp は、Goルーチンのスケジューリング情報に含まれるプログラムカウンタとスタックポインタです。これらは、Goルーチンが次に実行を再開するGoコードの場所を示します。
  • gp->stackbase はGoルーチンのスタックの基底アドレスです。
  • gp->stackguard はスタックオーバーフローを検出するためのガードページのアドレスです。

これらのフィールドは、GoルーチンがCgo呼び出し(システムコール)に入る直前のGo側のコンテキストを保存するために使用されます。Cgo呼び出し中はGoルーチンはCコードを実行しており、Goランタイムの通常のスタック管理の範囲外にあります。しかし、パニックが発生した場合、Goランタイムはこれらの保存された情報(syscallpc, syscallsp, syscallstack, syscallguard)を使用して、Go側のスタックフレームを正確に再構築し、完全なトレースバックを生成できるようになります。これにより、CgoとGoの境界を越えたパニックでも、Go側の呼び出し履歴が正しく表示されるようになります。

gp->goidgp->racectx の設定位置の変更は、これらのGoルーチン固有の識別子とレース検出コンテキストが、システムコール関連の状態設定とより密接に関連付けられるようにするための、コードの論理的な再編成と考えられます。

これらの変更は、GoランタイムがCgo呼び出し中のGoルーチンの状態をより正確に追跡し、パニック発生時に堅牢なトレースバックを提供することを可能にします。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (runtimeパッケージ、cgoパッケージ)
  • Go言語のソースコード (特に src/runtime ディレクトリ)
  • Go言語のIssueトラッカー (Issue #6061)
  • Go言語のコードレビューシステム (CL 12609043)
  • Go言語のランタイムに関する一般的な知識 (Goルーチン、スケジューラ、スタック管理など)
  • Cgoに関する一般的な知識 (GoとCの相互運用)
  • スタックトレースとデバッグに関する一般的な知識