[インデックス 1271] ファイルの概要
このコミットは、Go言語のランタイムにおけるスタックトレースの生成ロジックをクリーンアップし、改善するものです。特に、スタックトレースがすべてのフレームを表示するように修正され、以前のループが1フレーム早く停止していた問題が解決されています。
コミット
commit 2b39165f1eabc309bc774f6b1ac7c0ce62270c5d
Author: Russ Cox <rsc@golang.org>
Date: Wed Dec 3 14:20:23 2008 -0800
clean stack trace code.
format is unchanged but shows all frames
(old loop stopped one frame early).
wreck=; 6.out
cannot convert type *main.S·interface2 to interface main.I·interface2: missing method Foo
throw: interface conversion
SIGSEGV: segmentation violation
Faulting address: 0x0
pc: 0x256d
throw+0x46 /home/rsc/go/src/runtime/runtime.c:68
throw(0x863a, 0x0)
hashmap+0x188 /home/rsc/go/src/runtime/iface.c:167
hashmap(0x8760, 0x0, 0x85b0, 0x0, 0x0, ...)
sys·ifaceT2I+0xa8 /home/rsc/go/src/runtime/iface.c:201
sys·ifaceT2I(0x8760, 0x0, 0x85b0, 0x0, 0x0, ...)
main·main+0x4e /home/rsc/go/src/runtime/rt0_amd64_darwin.s:87
main·main()
mainstart+0xf /home/rsc/go/src/runtime/rt0_amd64.s:70
mainstart()
sys·goexit /home/rsc/go/src/runtime/proc.c:110
sys·goexit()
R=r
DELTA=44 (5 added, 15 deleted, 24 changed)
OCL=20358
CL=20368
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2b39165f1eabc309bc774f6b1ac7c0ce62270c5d
元コミット内容
このコミットの元の内容は、Goランタイムのスタックトレースコードのクリーンアップと、すべてのスタックフレームを表示するように修正することです。以前のバージョンでは、スタックトレースのループが1フレーム早く終了してしまい、完全なスタック情報が得られない問題がありました。コミットメッセージには、具体的なスタックトレースの例が示されており、インターフェース変換エラーやセグメンテーション違反が発生した際のトレースが改善されることが示唆されています。
変更の背景
Go言語の初期段階において、ランタイムの安定性とデバッグ機能は継続的に改善されていました。スタックトレースは、プログラムのクラッシュや予期せぬ動作が発生した際に、問題の原因を特定するための非常に重要な情報源です。しかし、当時のスタックトレースの実装には、すべてのコールスタックフレームを正確にキャプチャできないという不具合がありました。具体的には、スタックフレームを辿るループが途中で終了してしまい、完全な実行パスがデバッガーや開発者に提供されないという問題です。
この不完全なスタックトレースは、特に複雑なエラー(例:インターフェースの不正な型変換、セグメンテーション違反)が発生した場合に、デバッグ作業を著しく困難にしていました。開発者は、エラーが発生した正確なコンテキストを把握できず、問題の根本原因を特定するのに多くの時間を費やす必要がありました。
このコミットは、このようなデバッグの困難さを解消し、Goプログラムの堅牢性と開発者の生産性を向上させることを目的としています。完全なスタックトレースを提供することで、エラー発生時のプログラムの状態をより正確に把握し、迅速な問題解決を可能にします。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンピュータサイエンスの基本的な概念を理解しておく必要があります。
- スタックトレース (Stack Trace): プログラムが実行されている間に、現在アクティブなサブルーチン(関数)のリストと、それらがどのように呼び出されたかを示すレポートです。通常、エラーや例外が発生した際に生成され、問題の発生源を特定するのに役立ちます。各エントリは「スタックフレーム」と呼ばれ、関数名、ファイル名、行番号などの情報を含みます。
- ランタイム (Runtime): プログラムの実行中に、そのプログラムをサポートするために必要なサービスを提供するソフトウェア層です。Go言語の場合、ガベージコレクション、スケジューリング、スタック管理、エラーハンドリングなど、多くの低レベルな機能がランタイムによって提供されます。
- コールスタック (Call Stack): プログラムが実行中の関数の情報を格納するデータ構造です。関数が呼び出されるたびに、その関数の情報(ローカル変数、引数、戻りアドレスなど)がスタックにプッシュされ、関数が終了するとポップされます。
- PC (Program Counter): 次に実行される命令のアドレスを保持するCPUレジスタです。スタックトレースでは、各スタックフレームのPC値が、その関数が呼び出された場所を示します。
- SP (Stack Pointer): 現在のスタックフレームの最上位(または最下位、アーキテクチャによる)のアドレスを指すCPUレジスタです。スタックフレームのサイズや構造を理解することで、SPを操作してスタックを辿ることができます。
Stktop
構造体: Goランタイムにおけるスタック管理に関連する構造体で、特にスタックの拡張や切り替え(goroutineのスケジューリングなど)の際に使用されます。oldbase
やoldguard
といったフィールドは、以前のスタックセグメントの情報やスタックガード(スタックオーバーフロー検出用)の情報を保持します。Func
構造体: Goランタイムが持つ関数に関するメタデータを含む構造体です。関数名 (name
)、ソースファイル (src
)、フレームサイズ (frame
)、引数の数 (args
)、関数のエントリポイント (entry
) などの情報が含まれます。findfunc
関数: 特定のプログラムカウンタ (PC) に対応するFunc
構造体を見つけるためのランタイム関数です。これにより、PCアドレスから関数名やソースファイルなどの情報を取得できます。retfromnewstack
: Goランタイム内部の特殊な関数で、スタックの切り替え(例えば、goroutineの切り替えやスタックの拡張)が行われた際に、新しいスタックから元の呼び出し元に戻るためのメカニズムの一部です。スタックトレースを生成する際には、この関数をスキップして真の呼び出し元を特定する必要があります。
技術的詳細
このコミットの主要な変更は、src/runtime/rt2_amd64.c
ファイル内の traceback
関数にあります。この関数は、Goプログラムがパニックやエラーで終了した際に、現在のコールスタックを辿ってスタックトレースを生成する役割を担っています。
変更前は、traceback
関数がスタックフレームを辿るループにおいて、いくつかの問題がありました。
- ループの早期終了: コミットメッセージにもあるように、「old loop stopped one frame early」という問題がありました。これは、スタックトレースの生成ループが、本来表示すべき最後のスタックフレームを処理する前に終了してしまうことを意味します。結果として、完全なコールスタック情報が得られませんでした。
G
構造体のコピー: 以前のコードでは、G
構造体(goroutineの情報を保持する)をローカルにコピーしていました。これは、スタックアンワインド中にG
構造体の情報が変更される可能性を考慮したものですが、効率的ではない可能性があります。pc == nil
のハンドリング: プログラムカウンタ (PC) がnil
の場合(おそらくnil関数ポインタの呼び出しによるもの)、以前のコードではスタックからPCをポップしていましたが、その後の処理が最適ではありませんでした。retfromnewstack
の処理: スタック切り替えに関連するretfromnewstack
関数がスタックトレースに含まれる場合、これを適切にスキップして真の呼び出し元を特定する必要がありました。以前のコードでは、g.stackbase
やg.stackguard
を直接操作していましたが、新しいコードではStktop
構造体をより直接的に利用しています。
新しい traceback
関数では、これらの問題が以下のように改善されています。
pc
とsp
の初期化:pc0
とsp
を引数として受け取り、pc
が0の場合の処理をより明確にしています。pc = *(uint64*)sp; sp += 8;
という行は、nil関数呼び出しの場合に、呼び出し元のPCをスタックから取得し、スタックポインタを進めることを意味します。Stktop
構造体の利用:Stktop *stk = (Stktop*)g->stackbase;
のように、g
(現在のgoroutine)から直接Stktop
構造体を取得し、スタックの切り替えをより効率的に処理しています。while(pc == (uint64)retfromnewstack)
ループ内で、stk->oldsp
とstk->oldbase
を利用して、以前のスタックブロックに適切に移動しています。- ループ条件の改善:
for(n=0; n<100; n++)
というループが導入され、最大100フレームまでトレースするように変更されています。これにより、無限ループを防ぎつつ、十分な数のフレームをキャプチャできるようになります。以前のcounter
変数とif(counter++ > 100)
のチェックは削除されました。 - スタックフレームの終了条件:
pc = *(uint64*)(sp-8); if(pc <= 0x1000) return;
という新しい終了条件が追加されました。これは、スタックを遡ってPC値が非常に小さい(通常は無効なアドレス)場合、スタックトレースの終端に達したと判断して処理を終了します。これにより、以前の「old loop stopped one frame early」の問題が解決され、すべての関連するスタックフレームが確実に表示されるようになります。 - 冗長な変数の削除:
callpc
,counter
,i
,name
,g
,stktop
など、一部の変数が削除または簡略化され、コードがよりクリーンになっています。
これらの変更により、Goランタイムのスタックトレースはより正確で完全なものとなり、デバッグの効率が向上しました。
コアとなるコードの変更箇所
変更は主に src/runtime/rt2_amd64.c
ファイルの traceback
関数に集中しています。
--- a/src/runtime/rt2_amd64.c
+++ b/src/runtime/rt2_amd64.c
@@ -9,64 +9,49 @@ extern int32 debug;
extern uint8 end;
void
-traceback(uint8 *pc, uint8 *sp, void* r15)
+traceback(byte *pc0, byte *sp, G *g)
{
-- uint8* callpc;
-- int32 counter;
-- int32 i;
-- string name;
-+ Stktop *stk;
-+ uint64 pc;
-+ int32 i, n;
Func *f;
-- G g;
-- Stktop *stktop;
-
-- // store local copy of per-process data block that we can write as we unwind
-- mcpy((byte*)&g, (byte*)r15, sizeof(G));
-+ pc = (uint64)pc0;
-
-- // if the PC is zero, it's probably due to a nil function pointer.
-- // pop the failed frame.
-- if(pc == nil) {
-- pc = ((uint8**)sp)[0];
-+ // If the PC is zero, it's likely a nil function call.
-+ // Start in the caller's frame.
-+ if(pc == 0) {
-+ pc = *(uint64*)sp;
sp += 8;
}
-- counter = 0;
-- for(;;){
-- callpc = pc;
-- if((uint8*)retfromnewstack == pc) {
-- // call site is retfromnewstack(); pop to earlier stack block to get true caller
-- stktop = (Stktop*)g.stackbase;
-- g.stackbase = stktop->oldbase;
-- g.stackguard = stktop->oldguard;
-- sp = stktop->oldsp;
-- pc = ((uint8**)sp)[1];
-- sp += 16; // two irrelevant calls on stack - morestack, plus the call morestack made
-- continue;
-+ stk = (Stktop*)g->stackbase;
-+ for(n=0; n<100; n++) {
-+ while(pc == (uint64)retfromnewstack) {
-+ // pop to earlier stack block
-+ sp = stk->oldsp;
-+ stk = (Stktop*)stk->oldbase;
-+ pc = *(uint64*)(sp+8);
-+ sp += 16; // two irrelevant calls on stack: morestack plus its call
}
-- f = findfunc((uint64)callpc);
-+ f = findfunc(pc);
if(f == nil) {
-- printf("%p unknown pc\n", callpc);
-+ printf("%p unknown pc\n", pc);
return;
}
-- name = f->name;
if(f->frame < 8) // assembly funcs say 0 but lie
sp += 8;
else
sp += f->frame;
-- if(counter++ > 100){
-- prints("stack trace terminated\n");
-- break;
-- }
-- if((pc = ((uint8**)sp)[-1]) <= (uint8*)0x1000)
-- break;
-
// print this frame
// main+0xf /home/rsc/go/src/runtime/x.go:23
// main(0x1, 0x2, 0x3)
-- printf("%S", name);
-- if((uint64)callpc > f->entry)
-- printf("+%X", (uint64)callpc - f->entry);
-- printf(" %S:%d\\n", f->src, funcline(f, (uint64)callpc-1)); // -1 to get to CALL instr.
-- printf("\t%S(", name);
-+ printf("%S", f->name);
-+ if(pc > f->entry)
-+ printf("+%X", pc - f->entry);
-+ printf(" %S:%d\\n", f->src, funcline(f, pc-1)); // -1 to get to CALL instr.
-+ printf("\t%S(", f->name);
for(i = 0; i < f->args; i++) {
if(i != 0)
prints(", ");
@@ -77,5 +62,10 @@ traceback(uint8 *pc, uint8 *sp, void* r15)
}
}
prints(")\\n");
++
++ pc = *(uint64*)(sp-8);
++ if(pc <= 0x1000)
++ return;
}
++ prints("...\\n");
}
コアとなるコードの解説
traceback
関数は、Goランタイムがスタックトレースを生成する際の中心的なロジックを担っています。
-
関数のシグネチャ変更:
- 変更前:
traceback(uint8 *pc, uint8 *sp, void* r15)
- 変更後:
traceback(byte *pc0, byte *sp, G *g)
r15
レジスタからG
構造体(現在のgoroutineの情報)をコピーする代わりに、直接G *g
を引数として受け取るようになりました。これにより、mcpy
によるコピーが不要になり、より直接的にg
の情報にアクセスできるようになります。pc0
は初期のプログラムカウンタです。
- 変更前:
-
PCが0の場合のハンドリング:
// If the PC is zero, it's likely a nil function call. // Start in the caller's frame. if(pc == 0) { pc = *(uint64*)sp; sp += 8; }
pc
が0の場合、これはnil関数ポインタの呼び出しによって発生することが多いです。この場合、スタックポインタsp
が指すアドレスから呼び出し元のPCを取得し、sp
を8バイト(64ビットシステムでのポインタサイズ)進めることで、呼び出し元のフレームからトレースを開始します。 -
スタックトレースループの改善:
Stktop *stk = (Stktop*)g->stackbase; for(n=0; n<100; n++) { while(pc == (uint64)retfromnewstack) { // pop to earlier stack block sp = stk->oldsp; stk = (Stktop*)stk->oldbase; pc = *(uint64*)(sp+8); sp += 16; // two irrelevant calls on stack: morestack plus its call } // ... (フレーム情報の取得と出力) ... pc = *(uint64*)(sp-8); if(pc <= 0x1000) return; } prints("...\\n");
Stktop
の利用:g->stackbase
からStktop
構造体を取得し、スタックの切り替え(retfromnewstack
)を処理する際に、stk->oldsp
とstk->oldbase
を利用して、以前のスタックブロックに移動します。これにより、スタックの拡張やgoroutineの切り替えによって生じるスタックフレームの不連続性を適切に処理し、真の呼び出し元を特定できます。- ループ回数の制限:
for(n=0; n<100; n++)
により、最大100フレームまでトレースするように明示的に制限が設けられました。これにより、無限ループや過剰なトレースを防ぎます。 - 次のPCの取得と終了条件:
pc = *(uint64*)(sp-8);
は、現在のスタックフレームの呼び出し元のPCをスタックから取得します。sp-8
は、現在のフレームの戻りアドレスが格納されている可能性のある位置を指します。if(pc <= 0x1000) return;
は、取得したPCが非常に小さい値(通常は無効なアドレスやスタックの終端を示す)である場合に、スタックトレースを終了する条件です。これにより、以前の「1フレーム早く終了する」問題が解決され、完全なスタックトレースが保証されます。 prints("...\\n");
: 100フレームの制限に達した場合に、スタックトレースが途中で終了したことを示すメッセージが出力されます。
-
findfunc
の引数変更:- 変更前:
f = findfunc((uint64)callpc);
- 変更後:
f = findfunc(pc);
callpc
変数が削除されたため、直接pc
をfindfunc
に渡すようになりました。
- 変更前:
-
出力フォーマットの調整:
printf
の引数からname
変数が削除され、直接f->name
が使用されるようになりました。これにより、コードがより簡潔になります。
これらの変更により、Goランタイムのスタックトレースはより堅牢で正確になり、デバッグ時の情報提供能力が向上しました。
関連リンク
- Go言語のランタイムに関するドキュメント (公式): https://go.dev/doc/
- Go言語のソースコード (GitHub): https://github.com/golang/go
- Go言語のスタックトレースに関する議論 (Go Issues): 関連する問題や改善提案が議論されている可能性があります。
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
src/runtime
ディレクトリ) - スタックトレース、コールスタック、プログラムカウンタ、スタックポインタに関する一般的なコンピュータサイエンスの知識。