[インデックス 17986] ファイルの概要
このコミットは、Goランタイムにおける runtime.GoroutineProfile
関数がクラッシュする可能性のあるバグを修正するものです。特に、システムコールから復帰中のゴルーチンに対してプロファイルを取得する際に発生する可能性のある、スタックポインタの誤った参照に起因する問題を解決しています。この修正はGo 1.2.1の候補とされており、安定性向上に寄与します。
コミット
commit bc135f6492035024e778fe7dedb451ebaa06d3e8
Author: Russ Cox <rsc@golang.org>
Date: Fri Dec 13 15:44:57 2013 -0500
runtime: fix crash in runtime.GoroutineProfile
This is a possible Go 1.2.1 candidate.
Fixes #6946.
R=iant, r
CC=golang-dev
https://golang.org/cl/41640043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bc135f6492035024e778fe7dedb451ebaa06d3e8
元コミット内容
runtime: fix crash in runtime.GoroutineProfile
このコミットは、runtime.GoroutineProfile
関数におけるクラッシュを修正します。これはGo 1.2.1の候補となる修正であり、Issue #6946を解決します。
変更の背景
runtime.GoroutineProfile
は、Goプログラム内の全ゴルーチンのスタックトレースを収集し、プロファイリング情報を提供する関数です。しかし、特定の条件下、特にゴルーチンがシステムコール(syscall)の実行中またはシステムコールから復帰する際に、この関数がクラッシュする可能性がありました。
従来の scanstack
関数(mgc0.c
内)や saveg
関数(mprof.goc
内)、runtime·tracebackothers
関数(proc.c
内)では、ゴルーチンのスタックをスキャンする際に、ゴルーチンのスケジューラ情報 (gp->sched.pc
, gp->sched.sp
, gp->sched.lr
) を直接使用していました。システムコール中のゴルーチンは、通常のGoルーチンとは異なるスタック状態を持つことがあり、特にシステムコールから復帰する際にスタックポインタ (sp
) が一時的に不安定になることがあります。この不安定な状態の sp
を runtime·gentraceback
に渡すと、不正なメモリ参照が発生し、クラッシュにつながる可能性がありました。
Issue #6946は、この runtime.GoroutineProfile
がシステムコールから復帰中のゴルーチンに対してプロファイルを取得しようとした際に発生するクラッシュを報告していました。この問題は、Go 1.2.1のリリースにおいて優先的に修正すべき重要なバグと認識されました。
前提知識の解説
- ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。Goランタイムによって管理され、OSのスレッドに多重化されて実行されます。
- スタックトレース (Stack Trace): プログラムの実行中に、現在実行中の関数から呼び出し元の関数へと遡って、関数の呼び出し履歴を一覧表示したものです。デバッグやプロファイリングに不可欠な情報です。
- プロファイリング (Profiling): プログラムの実行時のパフォーマンス特性(CPU使用率、メモリ使用量、関数呼び出し回数など)を測定・分析することです。
runtime.GoroutineProfile
は、ゴルーチンのスタックトレースを収集することで、どの関数がどのゴルーチンで実行されているかを把握し、パフォーマンスボトルネックの特定に役立ちます。 - システムコール (System Call): アプリケーションがオペレーティングシステム (OS) のサービス(ファイルI/O、ネットワーク通信、メモリ管理など)を利用するためのインターフェースです。Goのゴルーチンがシステムコールを実行する際、その実行コンテキストは通常のGoコードの実行時とは異なる状態になることがあります。
runtime·gentraceback
: Goランタイム内部で使用される関数で、指定されたプログラムカウンタ (PC)、スタックポインタ (SP)、リンクレジスタ (LR) を基に、ゴルーチンのスタックトレースを生成します。uintptr
: Goにおけるポインタ型で、任意のポインタ値を保持できる整数型です。アドレスを表現する際に使用されます。~(uintptr)0
: これはuintptr
型のビット反転した0、つまり全てのビットが1である値を意味します。Goランタイム内部で、特定の関数に「特別な意味を持つ値」として渡されることがあります。このコミットでは、runtime·gentraceback
にこの値を渡すことで、「PC/SP/LRの値を直接使用するのではなく、ゴルーチン構造体 (G* gp
) から適切な値を読み取ってスタックトレースを生成してほしい」というシグナルとして利用されています。
技術的詳細
このコミットの主要な変更点は、runtime.GoroutineProfile
がゴルーチンのスタックトレースを収集する際に、スタックポインタ (SP) やプログラムカウンタ (PC) の取得方法を変更したことです。
以前の実装では、scanstack
や saveg
、runtime·tracebackothers
といった関数が、プロファイル対象のゴルーチン gp
のスケジューラ情報 (gp->sched.pc
, gp->sched.sp
, gp->sched.lr
) を直接 runtime·gentraceback
に渡していました。
しかし、ゴルーチンがシステムコールを実行している最中や、システムコールからGoコードに復帰する過渡期にある場合、gp->sched.sp
が指すスタックポインタが一時的に無効な状態になることがありました。特に、システムコールから復帰する際に、スタックが再調整される前にプロファイルが取得されると、不正なスタックポインタが runtime·gentraceback
に渡され、結果としてクラッシュを引き起こしていました。
この修正では、runtime·gentraceback
関数に渡すPCとSPの値として、直接 gp->sched.pc
や gp->sched.sp
を渡すのではなく、~(uintptr)0
という特殊な値を渡すように変更されました。この ~(uintptr)0
は、runtime·gentraceback
に対する「シグナル」として機能します。
runtime·gentraceback
関数(traceback_arm.c
および traceback_x86.c
で実装)は、この ~(uintptr)0
というシグナルを受け取ると、PCとSPの値を直接使用する代わりに、引数として渡されたゴルーチン構造体 gp
から、より安全で安定したスタック情報を取得するように変更されました。具体的には、ゴルーチンがシステムコールスタック (gp->syscallstack
) を持っている場合は gp->syscallpc
と gp->syscallsp
を、そうでない場合は gp->sched.pc
と gp->sched.sp
を使用します。これにより、システムコール中のゴルーチンや、システムコールから復帰中のゴルーチンに対しても、常に正しいスタックポインタとプログラムカウンタを使用してスタックトレースを生成できるようになり、クラッシュが回避されます。
また、この修正には runtime_unix_test.go
という新しいテストファイルが追加されています。このテストは、runtime.GoroutineProfile
がシステムコールから復帰中のゴルーチンに対してクラッシュしないことを検証するために設計されています。複数のゴルーチンを起動し、それぞれが syscall.Close(-1)
のような高速なシステムコールを繰り返し実行するループに入ります。同時に、メインゴルーチンが runtime.GoroutineProfile
を繰り返し呼び出し、競合状態を意図的に発生させて、以前のクラッシュが再現されないことを確認します。
コアとなるコードの変更箇所
src/pkg/runtime/mgc0.c
scanstack
関数が変更されました。
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1515,27 +1515,7 @@ scanframe(Stkframe *frame, void *arg)
static void
scanstack(G* gp, void *scanbuf)
{
- uintptr pc;
- uintptr sp;
- uintptr lr;
-
- if(gp->syscallstack != (uintptr)nil) {
- // Scanning another goroutine that is about to enter or might
- // have just exited a system call. It may be executing code such
- // as schedlock and may have needed to start a new stack segment.
- // Use the stack segment and stack pointer at the time of
- // the system call instead, since that won't change underfoot.
- sp = gp->syscallsp;
- pc = gp->syscallpc;
- lr = 0;
- } else {
- // Scanning another goroutine's stack.
- // The goroutine is usually asleep (the world is stopped).
- sp = gp->sched.sp;
- pc = gp->sched.pc;
- lr = gp->sched.lr;
- }
- runtime·gentraceback(pc, sp, lr, gp, 0, nil, 0x7fffffff, scanframe, scanbuf, false);
+ runtime·gentraceback(~(uintptr)0, ~(uintptr)0, 0, gp, 0, nil, 0x7fffffff, scanframe, scanbuf, false);
}
src/pkg/runtime/mprof.goc
saveg
関数と GoroutineProfile
関数内の saveg
呼び出しが変更されました。
--- a/src/pkg/runtime/mprof.goc
+++ b/src/pkg/runtime/mprof.goc
@@ -528,7 +528,7 @@ saveg(uintptr pc, uintptr sp, G *gp, TRecord *r)
{
int32 n;
- n = runtime·gentraceback((uintptr)pc, (uintptr)sp, 0, gp, 0, r->stk, nelem(r->stk), nil, nil, false);
+ n = runtime·gentraceback(pc, sp, 0, gp, 0, r->stk, nelem(r->stk), nil, nil, false);
if(n < nelem(r->stk))
r->stk[n] = 0;
}
@@ -556,7 +556,7 @@ func GoroutineProfile(b Slice) (n int, ok bool) {
for(gp = runtime·allg; gp != nil; gp = gp->alllink) {
if(gp == g || gp->status == Gdead)
continue;
- saveg(gp->sched.pc, gp->sched.sp, gp, r++);
+ saveg(~(uintptr)0, ~(uintptr)0, gp, r++);
}
}
src/pkg/runtime/proc.c
runtime·tracebackothers
関数内の runtime·traceback
呼び出しが変更されました。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -276,7 +276,7 @@ runtime·tracebackothers(G *me)
if((gp = m->curg) != nil && gp != me) {
runtime·printf("\n");
runtime·goroutineheader(gp);
- runtime·traceback(gp->sched.pc, gp->sched.sp, gp->sched.lr, gp);
+ runtime·traceback(~(uintptr)0, ~(uintptr)0, 0, gp);
}
for(gp = runtime·allg; gp != nil; gp = gp->alllink) {
@@ -290,7 +290,7 @@ runtime·tracebackothers(G *me)
runtime·printf("\tgoroutine running on other thread; stack unavailable\n");
runtime·printcreatedby(gp);
} else
- runtime·traceback(gp->sched.pc, gp->sched.sp, gp->sched.lr, gp);
+ runtime·traceback(~(uintptr)0, ~(uintptr)0, 0, gp);
}
}
src/pkg/runtime/traceback_arm.c
および src/pkg/runtime/traceback_x86.c
runtime·gentraceback
関数に、~(uintptr)0
シグナルを処理するロジックが追加されました。
traceback_arm.c
の変更点:
--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -20,6 +20,18 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,
Stktop *stk;
String file;
+ if(pc0 == ~(uintptr)0 && sp0 == ~(uintptr)0) { // Signal to fetch saved values from gp.
+ if(gp->syscallstack != (uintptr)nil) {
+ pc0 = gp->syscallpc;
+ sp0 = gp->syscallsp;
+ lr0 = 0;
+ } else {
+ pc0 = gp->sched.pc;
+ sp0 = gp->sched.sp;
+ lr0 = gp->sched.lr;
+ }
+ }
+
nprint = 0;
runtime·memclr((byte*)&frame, sizeof frame);
frame.pc = pc0;
traceback_x86.c
の変更点:
--- a/src/pkg/runtime/traceback_x86.c
+++ b/src/pkg/runtime/traceback_x86.c
@@ -30,6 +30,16 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,
String file;
USED(lr0);
+
+ if(pc0 == ~(uintptr)0 && sp0 == ~(uintptr)0) { // Signal to fetch saved values from gp.
+ if(gp->syscallstack != (uintptr)nil) {
+ pc0 = gp->syscallpc;
+ sp0 = gp->syscallsp;
+ } else {
+ pc0 = gp->sched.pc;
+ sp0 = gp->sched.sp;
+ }
+ }
nprint = 0;
runtime·memclr((byte*)&frame, sizeof frame);
src/pkg/runtime/runtime_unix_test.go
新しいテストファイルが追加されました。
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Only works on systems with syscall.Close.
// We need a fast system call to provoke the race,
// and Close(-1) is nearly universally fast.
// +build darwin dragonfly freebsd linux netbsd openbsd plan9
package runtime_test
import (
"runtime"
"sync"
"sync/atomic"
"syscall"
"testing"
)
func TestGoroutineProfile(t *testing.T) {
// GoroutineProfile used to use the wrong starting sp for
// goroutines coming out of system calls, causing possible
// crashes.
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(100))
var stop uint32
defer atomic.StoreUint32(&stop, 1) // in case of panic
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
for atomic.LoadUint32(&stop) == 0 {
syscall.Close(-1)
}
wg.Done()
}()
}
max := 10000
if testing.Short() {
max = 100
}
stk := make([]runtime.StackRecord, 100)
for n := 0; n < max; n++ {
_, ok := runtime.GoroutineProfile(stk)
if !ok {
t.Fatalf("GoroutineProfile failed")
}
}
// If the program didn't crash, we passed.
atomic.StoreUint32(&stop, 1)
wg.Wait()
}
コアとなるコードの解説
-
scanstack
関数 (mgc0.c
) の変更:- 以前は、ゴルーチンがシステムコール中か否かによって
pc
,sp
,lr
を条件分岐で取得し、その値をruntime·gentraceback
に渡していました。 - 変更後、この条件分岐が削除され、常に
~(uintptr)0
をpc
とsp
の引数としてruntime·gentraceback
に渡すようになりました。これは、「PCとSPの実際の値はruntime·gentraceback
内部でゴルーチン構造体gp
から取得してほしい」という意図を伝えます。
- 以前は、ゴルーチンがシステムコール中か否かによって
-
saveg
関数 (mprof.goc
) の変更:saveg
関数は、プロファイル情報を保存する際にruntime·gentraceback
を呼び出します。GoroutineProfile
関数内でsaveg
を呼び出す際も、同様に~(uintptr)0
をpc
とsp
の引数として渡すように変更されました。
-
runtime·tracebackothers
関数 (proc.c
) の変更:- この関数は、他のゴルーチンのスタックトレースをダンプする際に
runtime·traceback
を呼び出します。 - ここでも、
gp->sched.pc
とgp->sched.sp
を直接渡す代わりに、~(uintptr)0
を渡すように変更されました。
- この関数は、他のゴルーチンのスタックトレースをダンプする際に
-
runtime·gentraceback
関数 (traceback_arm.c
,traceback_x86.c
) の変更:- この関数は、スタックトレース生成の核心部分です。
- 追加された
if(pc0 == ~(uintptr)0 && sp0 == ~(uintptr)0)
ブロックが、新しいシグナルを処理します。 - このシグナルを受け取ると、
gp->syscallstack
がnil
でない(つまりシステムコールスタックを使用している)場合は、gp->syscallpc
とgp->syscallsp
を使用します。これは、システムコール中のゴルーチンのスタック情報がsyscallpc
とsyscallsp
に保存されているためです。 - それ以外の場合(通常のGoスタックを使用している場合)は、
gp->sched.pc
とgp->sched.sp
を使用します。 - このロジックにより、
runtime·gentraceback
は常にゴルーチンの現在の状態に応じた適切なスタック情報を取得し、クラッシュを回避します。
-
runtime_unix_test.go
の追加:- このテストは、
runtime.GoroutineProfile
がシステムコールから復帰中のゴルーチンに対してクラッシュしないことを確認するためのものです。 - 複数のゴルーチンが
syscall.Close(-1)
を繰り返し呼び出すことで、システムコールへの出入りを頻繁に行います。 - メインゴルーチンは
runtime.GoroutineProfile
を繰り返し呼び出し、この競合状態下でプロファイリングがクラッシュしないことを検証します。syscall.Close(-1)
は、高速で副作用が少ないシステムコールであるため、テストのトリガーとして適しています。
- このテストは、
これらの変更により、runtime.GoroutineProfile
は、ゴルーチンがシステムコールを実行しているかどうかにかかわらず、安定してスタックトレースを収集できるようになり、Goランタイムの堅牢性が向上しました。
関連リンク
- Go Issue #6946: https://github.com/golang/go/issues/6946 (このコミットが修正したとされるIssue)
- Go CL 41640043: https://golang.org/cl/41640043 (このコミットに対応するGoの変更リスト)
参考にした情報源リンク
- Go言語のソースコード (特に
src/pkg/runtime
ディレクトリ内のファイル) - Go言語のIssueトラッカー (Issue #6946 および関連するIssue)
- Go言語のドキュメント (runtimeパッケージに関する情報)
- Go言語のプロファイリングに関する一般的な情報