[インデックス 16815] ファイルの概要
このコミットは、Goランタイムにおけるスタックトレースの挙動を改善するためのものです。具体的には、morestack
および lessstack
という特殊な関数がスタックトレースに現れた際に、それらが正しく処理され、スタックトレースが途切れないようにするための変更が含まれています。
変更されたファイルは以下の通りです。
src/pkg/runtime/asm_386.s
: 32ビットx86アーキテクチャ向けのアセンブリコードsrc/pkg/runtime/asm_amd64.s
: 64ビットx86アーキテクチャ向けのアセンブリコードsrc/pkg/runtime/asm_arm.s
: ARMアーキテクチャ向けのアセンブリコードsrc/pkg/runtime/proc.c
: Goランタイムのプロセス管理に関するC言語コードsrc/pkg/runtime/traceback_arm.c
: ARMアーキテクチャ向けのスタックトレース処理に関するC言語コードsrc/pkg/runtime/traceback_x86.c
: x86アーキテクチャ向けのスタックトレース処理に関するC言語コード
コミット
commit 58f12ffd79df8ae369afa7ec60ee26d72ce2d843
Author: Russ Cox <rsc@golang.org>
Date: Thu Jul 18 16:53:45 2013 -0400
runtime: handle morestack/lessstack in stack trace
If we start a garbage collection on g0 during a
stack split or unsplit, we'll see morestack or lessstack
at the top of the stack. Record an argument frame size
for those, and record that they terminate the stack.
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/11533043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/58f12ffd79df8ae369afa7ec60ee26d72ce2d843
元コミット内容
runtime: handle morestack/lessstack in stack trace
If we start a garbage collection on g0 during a
stack split or unsplit, we'll see morestack or lessstack
at the top of the stack. Record an argument frame size
for those, and record that they terminate the stack.
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/11533043
変更の背景
Goランタイムでは、ゴルーチン(goroutine)のスタックは必要に応じて動的に伸縮します。スタックが足りなくなった場合は morestack
関数が呼ばれてスタックを拡張し、スタックが大きくなりすぎた場合は lessstack
関数が呼ばれてスタックを縮小します。これらの関数は、通常のGo関数とは異なり、特殊なアセンブリコードで実装されており、スタックフレームの構造が通常の関数呼び出しとは異なります。
問題は、ガベージコレクション(GC)が g0
(スケジューラやGCなどのランタイム内部処理を実行する特別なゴルーチン)上でスタック分割(stack split)またはスタック結合(stack unsplit)の最中に開始された場合、スタックトレースの最上位に morestack
または lessstack
が現れる可能性があったことです。
従来のスタックトレース機構は、これらの特殊な関数を適切に扱えず、スタックトレースが途中で途切れてしまう、あるいは不正なスタックフレームサイズを報告するといった問題が発生していました。これは、デバッグ時やプロファイリング時に正確なスタック情報が得られないという重大な課題でした。このコミットは、この問題を解決し、morestack
や lessstack
がスタックトレースに現れても、正確なスタック情報を取得できるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
- ゴルーチン (Goroutine): Goにおける軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持ちます。
- スタック (Stack): 関数呼び出しの際に、ローカル変数、引数、リターンアドレスなどを格納するメモリ領域です。Goのゴルーチンスタックは、必要に応じて動的にサイズが変更されます(スタックの伸縮)。
g0
ゴルーチン: Goランタイムには、ユーザーゴルーチンとは別に、ランタイム自身の処理(スケジューリング、ガベージコレクション、システムコールなど)を実行するための特別なゴルーチンが存在します。これがg0
ゴルーチンです。g0
は固定サイズのスタックを持ち、ユーザーゴルーチンのスタックとは異なる管理がされます。morestack
関数: ゴルーチンが関数を呼び出す際に、現在のスタックが不足していると判断された場合に、プロローグ(関数の冒頭部分)で自動的に呼び出されるランタイム関数です。この関数は、より大きなスタックを確保し、新しいスタックに実行を移します。lessstack
関数:morestack
とは逆に、スタックが過剰に確保されている場合に、スタックを縮小するために呼び出されるランタイム関数です。- スタックトレース (Stack Trace): プログラムの実行中に、現在実行中の関数から呼び出し元の関数へと遡って、一連の関数呼び出しの履歴を表示するものです。デバッグやエラー解析に不可欠な情報です。
- スタックフレーム (Stack Frame): 各関数呼び出しに対応するスタック上の領域です。これには、関数の引数、ローカル変数、リターンアドレスなどが含まれます。スタックトレースは、これらのスタックフレームを順に辿ることで生成されます。
- 引数フレームサイズ (Argument Frame Size): 関数が呼び出された際に、スタック上に確保される引数領域のサイズです。スタックトレースを正確に生成するためには、各関数の引数フレームサイズを正確に知る必要があります。
TEXT
ディレクティブ (Goアセンブリ): Goのアセンブリ言語で関数を定義する際に使用されるディレクティブです。TEXT symbol(SB), flags, args-framesize
の形式で記述され、args-framesize
の部分で引数とローカル変数の合計サイズを指定します。このサイズはスタックトレースの生成に利用されます。
技術的詳細
このコミットの核心は、morestack
と lessstack
がスタックトレースに現れた際に、それらを「スタックの終端」として扱い、かつ「引数フレームサイズが0である」と明示的にランタイムに伝える点にあります。
Goのスタックトレース機構は、各関数のスタックフレームを解析し、リターンアドレスや引数、ローカル変数の情報を抽出することで機能します。この解析には、各関数の引数フレームサイズが重要な情報となります。しかし、morestack
や lessstack
は通常のGo関数とは異なり、コンパイラによって生成される通常の関数プロローグを持たず、手書きのアセンブリで実装されています。そのため、これらの関数がスタックトレースの対象となった場合、ランタイムは正しい引数フレームサイズを推測できず、スタックトレースが破損する可能性がありました。
このコミットでは、以下の具体的な変更が行われています。
-
アセンブリコードの変更 (
asm_*.s
):runtime·morestack
とruntime·lessstack
のTEXT
ディレクティブにおいて、引数フレームサイズを明示的に0-0
と設定しました。これは、「引数はなく、ローカル変数もスタックフレームを消費しない」ことを意味します。これにより、スタックトレース機構がこれらの関数を正しく認識し、その後のスタックフレームの解析を継続できるようになります。- ARMアーキテクチャの
runtime·morestack
とruntime·lessstack
の呼び出しがB
(Branch) からBL
(Branch with Link) に変更されました。これは、newstack
やoldstack
を呼び出す際にリターンアドレスをLR
レジスタに保存するようにするためです。これにより、スタックトレースがより正確になります。
-
C言語コードの変更 (
proc.c
,traceback_*.c
):runtime·haszeroargs
関数が削除されました。この関数は、特定のランタイム関数が引数を持たないことをハードコードで判断していましたが、TEXT
ディレクティブでの引数フレームサイズの明示的な指定により、この関数は不要になりました。runtime·topofstack
関数にruntime·morestack
とruntime·lessstack
が追加されました。この関数は、特定の関数がゴルーチンスタックの最上位(つまり、その関数より上位にはユーザーコードのスタックフレームがない)を示すかどうかを判断します。これにより、スタックトレースがこれらの関数で適切に終端されるようになります。traceback_arm.c
およびtraceback_x86.c
において、スタックトレース中に不正なPC(プログラムカウンタ)やリターンPC、または不明な引数フレームサイズが検出された場合にruntime·throw
を呼び出す条件が変更されました。具体的には、callback != nil
の場合にのみruntime·throw
を呼び出すようになりました。これは、スタックトレースがデバッグ目的で表示される際に、必ずしも致命的なエラーとして処理する必要がない場合があるためです。例えば、プロファイリングツールがスタックトレースを収集している場合などです。
これらの変更により、GCが g0
上でスタックの伸縮中に発生しても、morestack
や lessstack
がスタックトレースに現れることが許容され、かつそれらがスタックの終端として正しく扱われることで、スタックトレースの正確性と堅牢性が向上しました。
コアとなるコードの変更箇所
src/pkg/runtime/asm_386.s
, src/pkg/runtime/asm_amd64.s
(x86系アセンブリ)
--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -195,7 +195,12 @@ TEXT runtime·mcall(SB), 7, $0-4
*/
// Called during function prolog when more stack is needed.
-TEXT runtime·morestack(SB),7,$0
+//
+// The traceback routines see morestack on a g0 as being
+// the top of a stack (for example, morestack calling newstack
+// calling the scheduler calling newm calling gc), so we must
+// record an argument size. For that purpose, it has no arguments.
+TEXT runtime·morestack(SB),7,$0-0
// Cannot grow scheduler stack (m->g0).
get_tls(CX)
MOVL m(CX), BX
@@ -288,7 +288,10 @@ TEXT reflect·call(SB), 7, $0-12
// Return point when leaving stack.
-TEXT runtime·lessstack(SB), 7, $0
+//
+// Lessstack can appear in stack traces for the same reason
+// as morestack; in that context, it has 0 arguments.
+TEXT runtime·lessstack(SB), 7, $0-0
// Save return value in m->cret
get_tls(CX)
MOVL m(CX), BX
TEXT
ディレクティブの最後の部分が $0
から $0-0
に変更されています。これは、引数サイズが0、フレームサイズも0であることを明示しています。
src/pkg/runtime/asm_arm.s
(ARMアセンブリ)
--- a/src/pkg/runtime/asm_arm.s
+++ b/src/pkg/runtime/asm_arm.s
@@ -170,7 +170,12 @@ TEXT runtime·mcall(SB), 7, $-4-4
// NB. we do not save R0 because we've forced 5c to pass all arguments
// on the stack.
// using frame size $-4 means do not save LR on stack.
-TEXT runtime·morestack(SB),7,$-4
+//
+// The traceback routines see morestack on a g0 as being
+// the top of a stack (for example, morestack calling newstack
+// calling the scheduler calling newm calling gc), so we must
+// record an argument size. For that purpose, it has no arguments.
+TEXT runtime·morestack(SB),7,$-4-0
// Cannot grow scheduler stack (m->g0).
MOVW m_g0(m), R4
CMP g, R4
@@ -197,7 +202,7 @@ TEXT runtime·morestack(SB),7,$-4
// Call newstack on m->g0's stack.
MOVW m_g0(m), g
MOVW (g_sched+gobuf_sp)(g), SP
-\tB runtime·newstack(SB)
+\tBL runtime·newstack(SB)
// Called from reflection library. Mimics morestack,
// reuses stack growth code to create a frame
@@ -241,14 +246,17 @@ TEXT reflect·call(SB), 7, $-4-12
// Return point when leaving stack.
// using frame size $-4 means do not save LR on stack.
-TEXT runtime·lessstack(SB), 7, $-4
+//
+// Lessstack can appear in stack traces for the same reason
+// as morestack; in that context, it has 0 arguments.
+TEXT runtime·lessstack(SB), 7, $-4-0
// Save return value in m->cret
MOVW R0, m_cret(m)
// Call oldstack on m->g0's stack.
MOVW m_g0(m), g
MOVW (g_sched+gobuf_sp)(g), SP
-\tB runtime·oldstack(SB)
+\tBL runtime·oldstack(SB)
// void jmpdefer(fn, sp);
// called from deferreturn.
ARMアセンブリでも同様に TEXT
ディレクティブの引数フレームサイズが $0-0
に変更されています。また、B
(Branch) 命令が BL
(Branch with Link) 命令に変更され、newstack
や oldstack
への呼び出しでリターンアドレスがリンクレジスタ (LR) に保存されるようになっています。
src/pkg/runtime/proc.c
(C言語コード)
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2499,17 +2499,6 @@ runtime·testSchedLocalQueueSteal(void)
extern void runtime·morestack(void);
-bool
-runtime·haszeroargs(uintptr pc)
-{
- return pc == (uintptr)runtime·goexit ||
- pc == (uintptr)runtime·mcall ||
- pc == (uintptr)runtime·mstart ||
- pc == (uintptr)runtime·lessstack ||
- pc == (uintptr)runtime·morestack ||
- pc == (uintptr)_rt0_go;
-}
-
// Does f mark the top of a goroutine stack?
bool
runtime·topofstack(Func *f)
@@ -2517,5 +2506,7 @@ runtime·topofstack(Func *f)
return f->entry == (uintptr)runtime·goexit ||
f->entry == (uintptr)runtime·mstart ||
f->entry == (uintptr)runtime·mcall ||
+\t\tf->entry == (uintptr)runtime·morestack ||
+\t\tf->entry == (uintptr)runtime·lessstack ||
f->entry == (uintptr)_rt0_go;
}
runtime·haszeroargs
関数が完全に削除されています。また、runtime·topofstack
関数に runtime·morestack
と runtime·lessstack
が追加され、これらの関数がスタックの最上位を示すものとして認識されるようになりました。
src/pkg/runtime/traceback_arm.c
, src/pkg/runtime/traceback_x86.c
(C言語コード)
--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -69,7 +69,8 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
\tf = runtime·findfunc(frame.pc);\
\tif(f == nil) {\
\t\truntime·printf("runtime: unknown pc %p after stack split\\n", frame.pc);\
-\t\t\t\truntime·throw("unknown pc");
+\t\t\t\tif(callback != nil)\
+\t\t\t\t\truntime·throw("unknown pc");
\t}\
\tframe.fn = f;\
\tcontinue;\
@@ -89,7 +90,8 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
\tflr = runtime·findfunc(frame.lr);\
\tif(flr == nil) {\
\t\truntime·printf("runtime: unexpected return pc for %s called from %p\\n", runtime·funcname(f), frame.lr);\
-\t\t\t\truntime·throw("unknown caller pc");
+\t\t\t\tif(callback != nil)\
+\t\t\t\t\truntime·throw("unknown caller pc");
\t}\
}\
\t\t
@@ -112,7 +114,7 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
\telse {\
\t\truntime·printf("runtime: unknown argument frame size for %s called from %p [%s]\\n",\
\t\t\truntime·funcname(f), frame.lr, flr ? runtime·funcname(flr) : "?");\
-\t\t\t\tif(!printing)\
+\t\t\t\tif(callback != nil)\
\t\t\truntime·throw("invalid stack");
\t\tframe.arglen = 0;\
\t}\
@@ -131,7 +133,8 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
} else {\
\tif(f->locals > frame.fp - frame.sp) {\
\t\truntime·printf("runtime: inconsistent locals=%p frame=%p fp=%p sp=%p for %s\\n", (uintptr)f->locals, (uintptr)f->frame, frame.fp, frame.sp, runtime·funcname(f));\
-\t\t\t\truntime·throw("invalid stack");
+\t\t\t\tif(callback != nil)\
+\t\t\t\t\truntime·throw("invalid stack");
\t}\
\tframe.varp = (byte*)frame.fp - f->locals;\
\tframe.varlen = f->locals;\
runtime·gentraceback
関数内で、runtime·throw
の呼び出しが if(callback != nil)
という条件で囲まれています。これにより、スタックトレースの収集がコールバック関数を伴う場合(例えば、プロファイリングツールなど)にのみ、不正なスタック情報が検出された際にパニックを引き起こすようになります。通常のスタックトレース表示時には、エラーメッセージは出力されますが、プログラムは継続されます。
コアとなるコードの解説
アセンブリコードの変更 (TEXT
ディレクティブ)
Goのアセンブリでは、TEXT
ディレクティブは関数の定義に使用されます。その書式は TEXT symbol(SB), flags, args-framesize
です。
symbol(SB)
: 関数のシンボル名。SB
はStatic Baseで、グローバルシンボルであることを示します。flags
: 関数の特性を示すフラグ。このコミットでは7
が使われており、これはNOSPLIT
(スタック分割しない) とRODATA
(読み取り専用データ) の組み合わせです。args-framesize
: ここが今回の変更の肝です。args
: 関数が受け取る引数の合計サイズ(バイト単位)。framesize
: 関数が使用するローカル変数とレジスタ退避領域の合計サイズ(バイト単位)。args-framesize
の形式で指定することで、ランタイムはスタックフレームの構造を正確に把握できます。
変更前は TEXT runtime·morestack(SB),7,$0
のように $0
とだけ指定されていました。これは引数サイズが0であることを示しますが、フレームサイズについては明示されていませんでした。変更後は TEXT runtime·morestack(SB),7,$0-0
となり、引数サイズが0、かつフレームサイズも0であることを明確に指定しています。これにより、morestack
や lessstack
がスタックトレースに現れた際に、スタックトレース機構がこれらの関数がスタックフレームを消費しない特殊な関数であることを正しく認識できるようになりました。
proc.c
の変更 (runtime·haszeroargs
の削除と runtime·topofstack
の更新)
runtime·haszeroargs
の削除: この関数は、特定のランタイム関数(goexit
,mcall
,mstart
,lessstack
,morestack
,_rt0_go
)が引数を持たないことをハードコードで判定していました。しかし、アセンブリコードのTEXT
ディレクティブで引数サイズを明示的に0
と指定するようになったため、この関数は冗長となり削除されました。これは、ランタイムのコードベースをより簡潔にし、一貫性のあるスタックフレーム情報の管理方法に移行したことを意味します。runtime·topofstack
の更新: この関数は、与えられた関数がゴルーチンスタックの最上位にあるかどうかを判断するために使用されます。つまり、その関数より上位にはユーザーコードのスタックフレームが存在しないことを示します。morestack
やlessstack
はスタックの伸縮処理を行う特殊な関数であり、これらがスタックトレースの最上位に現れた場合、それ以上ユーザーコードのスタックフレームを遡る必要はありません。このコミットでruntime·morestack
とruntime·lessstack
がこのリストに追加されたことで、スタックトレースがこれらの関数で適切に終端され、不必要なスタックフレームの探索や誤った解析を防ぐことができます。
traceback_*.c
の変更 (条件付き runtime·throw
)
runtime·gentraceback
関数は、スタックトレースを生成する主要な関数です。この関数内で、不正なPCやリターンPC、または不明な引数フレームサイズが検出された場合に runtime·throw
を呼び出す箇所がありました。runtime·throw
はGoのパニックに相当し、プログラムを異常終了させます。
変更前は、これらのエラーが検出されると常に runtime·throw
が呼び出されていました。しかし、スタックトレースの収集はデバッグ目的だけでなく、プロファイリングツールなど、プログラムの実行を中断させたくない状況でも行われます。
このコミットでは、runtime·throw
の呼び出しが if(callback != nil)
という条件で囲まれました。これは、スタックトレースの収集がコールバック関数を伴う場合(例えば、プロファイリングツールがスタックトレースを収集し、その結果をコールバック関数に渡す場合など)にのみ、不正なスタック情報が検出された際にパニックを引き起こすように変更されたことを意味します。通常のスタックトレース表示時など、callback
が nil
の場合は、エラーメッセージは出力されますが、プログラムは継続されます。これにより、スタックトレース機構の堅牢性が向上し、より多様なユースケースに対応できるようになりました。
関連リンク
- Go CL 11533043: https://golang.org/cl/11533043
参考にした情報源リンク
- Goのスタック管理に関する公式ドキュメントやブログ記事 (一般的なGoランタイムのスタック伸縮、
g0
、morestack
/lessstack
の概念理解のため) - Goのアセンブリ言語に関するドキュメント (特に
TEXT
ディレクティブのargs-framesize
の意味について) - Goのガベージコレクションに関する資料 (GCが
g0
上で実行されることの理解のため) - Goのスタックトレースの仕組みに関する技術記事 (スタックフレームの解析方法の理解のため)# [インデックス 16815] ファイルの概要
このコミットは、Goランタイムにおけるスタックトレースの挙動を改善するためのものです。具体的には、morestack
および lessstack
という特殊な関数がスタックトレースに現れた際に、それらが正しく処理され、スタックトレースが途切れないようにするための変更が含まれています。
変更されたファイルは以下の通りです。
src/pkg/runtime/asm_386.s
: 32ビットx86アーキテクチャ向けのアセンブリコードsrc/pkg/runtime/asm_amd64.s
: 64ビットx86アーキテクチャ向けのアセンブリコードsrc/pkg/runtime/asm_arm.s
: ARMアーキテクチャ向けのアセンブリコードsrc/pkg/runtime/proc.c
: Goランタイムのプロセス管理に関するC言語コードsrc/pkg/runtime/traceback_arm.c
: ARMアーキテクチャ向けのスタックトレース処理に関するC言語コードsrc/pkg/runtime/traceback_x86.c
: x86アーキテクチャ向けのスタックトレース処理に関するC言語コード
コミット
commit 58f12ffd79df8ae369afa7ec60ee26d72ce2d843
Author: Russ Cox <rsc@golang.org>
Date: Thu Jul 18 16:53:45 2013 -0400
runtime: handle morestack/lessstack in stack trace
If we start a garbage collection on g0 during a
stack split or unsplit, we'll see morestack or lessstack
at the top of the stack. Record an argument frame size
for those, and record that they terminate the stack.
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/11533043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/58f12ffd79df8ae369afa7ec60ee26d72ce2d843
元コミット内容
runtime: handle morestack/lessstack in stack trace
If we start a garbage collection on g0 during a
stack split or unsplit, we'll see morestack or lessstack
at the top of the stack. Record an argument frame size
for those, and record that they terminate the stack.
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/11533043
変更の背景
Goランタイムでは、ゴルーチン(goroutine)のスタックは必要に応じて動的に伸縮します。スタックが足りなくなった場合は morestack
関数が呼ばれてスタックを拡張し、スタックが大きくなりすぎた場合は lessstack
関数が呼ばれてスタックを縮小します。これらの関数は、通常のGo関数とは異なり、特殊なアセンブリコードで実装されており、スタックフレームの構造が通常の関数呼び出しとは異なります。
問題は、ガベージコレクション(GC)が g0
(スケジューラやGCなどのランタイム内部処理を実行する特別なゴルーチン)上でスタック分割(stack split)またはスタック結合(stack unsplit)の最中に開始された場合、スタックトレースの最上位に morestack
または lessstack
が現れる可能性があったことです。
従来のスタックトレース機構は、これらの特殊な関数を適切に扱えず、スタックトレースが途中で途切れてしまう、あるいは不正なスタックフレームサイズを報告するといった問題が発生していました。これは、デバッグ時やプロファイリング時に正確なスタック情報が得られないという重大な課題でした。このコミットは、この問題を解決し、morestack
や lessstack
がスタックトレースに現れても、正確なスタック情報を取得できるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
- ゴルーチン (Goroutine): Goにおける軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持ちます。
- スタック (Stack): 関数呼び出しの際に、ローカル変数、引数、リターンアドレスなどを格納するメモリ領域です。Goのゴルーチンスタックは、必要に応じて動的にサイズが変更されます(スタックの伸縮)。
g0
ゴルーチン: Goランタイムには、ユーザーゴルーチンとは別に、ランタイム自身の処理(スケジューリング、ガベージコレクション、システムコールなど)を実行するための特別なゴルーチンが存在します。これがg0
ゴルーチンです。g0
は固定サイズのスタックを持ち、ユーザーゴルーチンのスタックとは異なる管理がされます。morestack
関数: ゴルーチンが関数を呼び出す際に、現在のスタックが不足していると判断された場合に、プロローグ(関数の冒頭部分)で自動的に呼び出されるランタイム関数です。この関数は、より大きなスタックを確保し、新しいスタックに実行を移します。lessstack
関数:morestack
とは逆に、スタックが過剰に確保されている場合に、スタックを縮小するために呼び出されるランタイム関数です。- スタックトレース (Stack Trace): プログラムの実行中に、現在実行中の関数から呼び出し元の関数へと遡って、一連の関数呼び出しの履歴を表示するものです。デバッグやエラー解析に不可欠な情報です。
- スタックフレーム (Stack Frame): 各関数呼び出しに対応するスタック上の領域です。これには、関数の引数、ローカル変数、リターンアドレスなどが含まれます。スタックトレースは、これらのスタックフレームを順に辿ることで生成されます。
- 引数フレームサイズ (Argument Frame Size): 関数が呼び出された際に、スタック上に確保される引数領域のサイズです。スタックトレースを正確に生成するためには、各関数の引数フレームサイズを正確に知る必要があります。
TEXT
ディレクティブ (Goアセンブリ): Goのアセンブリ言語で関数を定義する際に使用されるディレクティブです。TEXT symbol(SB), flags, args-framesize
の形式で記述され、args-framesize
の部分で引数とローカル変数の合計サイズを指定します。このサイズはスタックトレースの生成に利用されます。
技術的詳細
このコミットの核心は、morestack
と lessstack
がスタックトレースに現れた際に、それらを「スタックの終端」として扱い、かつ「引数フレームサイズが0である」と明示的にランタイムに伝える点にあります。
Goのスタックトレース機構は、各関数のスタックフレームを解析し、リターンアドレスや引数、ローカル変数の情報を抽出することで機能します。この解析には、各関数の引数フレームサイズが重要な情報となります。しかし、morestack
や lessstack
は通常のGo関数とは異なり、コンパイラによって生成される通常の関数プロローグを持たず、手書きのアセンブリで実装されています。そのため、これらの関数がスタックトレースの対象となった場合、ランタイムは正しい引数フレームサイズを推測できず、スタックトレースが破損する可能性がありました。
このコミットでは、以下の具体的な変更が行われています。
-
アセンブリコードの変更 (
asm_*.s
):runtime·morestack
とruntime·lessstack
のTEXT
ディレクティブにおいて、引数フレームサイズを明示的に0-0
と設定しました。これは、「引数はなく、ローカル変数もスタックフレームを消費しない」ことを意味します。これにより、スタックトレース機構がこれらの関数を正しく認識し、その後のスタックフレームの解析を継続できるようになります。- ARMアーキテクチャの
runtime·morestack
とruntime·lessstack
の呼び出しがB
(Branch) からBL
(Branch with Link) に変更されました。これは、newstack
やoldstack
を呼び出す際にリターンアドレスをLR
レジスタに保存するようにするためです。これにより、スタックトレースがより正確になります。
-
C言語コードの変更 (
proc.c
,traceback_*.c
):runtime·haszeroargs
関数が削除されました。この関数は、特定のランタイム関数が引数を持たないことをハードコードで判断していましたが、TEXT
ディレクティブでの引数フレームサイズの明示的な指定により、この関数は不要になりました。runtime·topofstack
関数にruntime·morestack
とruntime·lessstack
が追加されました。この関数は、特定の関数がゴルーチンスタックの最上位(つまり、その関数より上位にはユーザーコードのスタックフレームがない)を示すかどうかを判断します。これにより、スタックトレースがこれらの関数で適切に終端されるようになります。traceback_arm.c
およびtraceback_x86.c
において、スタックトレース中に不正なPC(プログラムカウンタ)やリターンPC、または不明な引数フレームサイズが検出された場合にruntime·throw
を呼び出す条件が変更されました。具体的には、callback != nil
の場合にのみruntime·throw
を呼び出すようになりました。これは、スタックトレースがデバッグ目的で表示される際に、必ずしも致命的なエラーとして処理する必要がない場合があるためです。例えば、プロファイリングツールがスタックトレースを収集している場合などです。
これらの変更により、GCが g0
上でスタックの伸縮中に発生しても、morestack
や lessstack
がスタックトレースに現れることが許容され、かつそれらがスタックの終端として正しく扱われることで、スタックトレースの正確性と堅牢性が向上しました。
コアとなるコードの変更箇所
src/pkg/runtime/asm_386.s
, src/pkg/runtime/asm_amd64.s
(x86系アセンブリ)
--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -195,7 +195,12 @@ TEXT runtime·mcall(SB), 7, $0-4
*/
// Called during function prolog when more stack is needed.
-TEXT runtime·morestack(SB),7,$0
+//
+// The traceback routines see morestack on a g0 as being
+// the top of a stack (for example, morestack calling newstack
+// calling the scheduler calling newm calling gc), so we must
+// record an argument size. For that purpose, it has no arguments.
+TEXT runtime·morestack(SB),7,$0-0
// Cannot grow scheduler stack (m->g0).
get_tls(CX)
MOVL m(CX), BX
@@ -288,7 +288,10 @@ TEXT reflect·call(SB), 7, $0-12
// Return point when leaving stack.
-TEXT runtime·lessstack(SB), 7, $0
+//
+// Lessstack can appear in stack traces for the same reason
+// as morestack; in that context, it has 0 arguments.
+TEXT runtime·lessstack(SB), 7, $0-0
// Save return value in m->cret
get_tls(CX)
MOVL m(CX), BX
TEXT
ディレクティブの最後の部分が $0
から $0-0
に変更されています。これは、引数サイズが0、フレームサイズも0であることを明示しています。
src/pkg/runtime/asm_arm.s
(ARMアセンブリ)
--- a/src/pkg/runtime/asm_arm.s
+++ b/src/pkg/runtime/asm_arm.s
@@ -170,7 +170,12 @@ TEXT runtime·mcall(SB), 7, $-4-4
// NB. we do not save R0 because we've forced 5c to pass all arguments
// on the stack.
// using frame size $-4 means do not save LR on stack.
-TEXT runtime·morestack(SB),7,$-4
+//
+// The traceback routines see morestack on a g0 as being
+// the top of a stack (for example, morestack calling newstack
+// calling the scheduler calling newm calling gc), so we must
+// record an argument size. For that purpose, it has no arguments.
+TEXT runtime·morestack(SB),7,$-4-0
// Cannot grow scheduler stack (m->g0).
MOVW m_g0(m), R4
CMP g, R4
@@ -197,7 +202,7 @@ TEXT runtime·morestack(SB),7,$-4
// Call newstack on m->g0's stack.
MOVW m_g0(m), g
MOVW (g_sched+gobuf_sp)(g), SP
-\tB runtime·newstack(SB)
+\tBL runtime·newstack(SB)
// Called from reflection library. Mimics morestack,
// reuses stack growth code to create a frame
@@ -241,14 +246,17 @@ TEXT reflect·call(SB), 7, $-4-12
// Return point when leaving stack.
// using frame size $-4 means do not save LR on stack.
-TEXT runtime·lessstack(SB), 7, $-4
+//
+// Lessstack can appear in stack traces for the same reason
+// as morestack; in that context, it has 0 arguments.
+TEXT runtime·lessstack(SB), 7, $-4-0
// Save return value in m->cret
MOVW R0, m_cret(m)
// Call oldstack on m->g0's stack.
MOVW m_g0(m), g
MOVW (g_sched+gobuf_sp)(g), SP
-\tB runtime·oldstack(SB)
+\tBL runtime·oldstack(SB)
// void jmpdefer(fn, sp);\n // called from deferreturn.
ARMアセンブリでも同様に TEXT
ディレクティブの引数フレームサイズが $0-0
に変更されています。また、B
(Branch) 命令が BL
(Branch with Link) 命令に変更され、newstack
や oldstack
への呼び出しでリターンアドレスがリンクレジスタ (LR) に保存されるようになっています。
src/pkg/runtime/proc.c
(C言語コード)
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2499,17 +2499,6 @@ runtime·testSchedLocalQueueSteal(void)
extern void runtime·morestack(void);
-bool
-runtime·haszeroargs(uintptr pc)
-{
- return pc == (uintptr)runtime·goexit ||
- pc == (uintptr)runtime·mcall ||
- pc == (uintptr)runtime·mstart ||
- pc == (uintptr)runtime·lessstack ||
- pc == (uintptr)runtime·morestack ||
- pc == (uintptr)_rt0_go;
-}
-
// Does f mark the top of a goroutine stack?
bool
runtime·topofstack(Func *f)
@@ -2517,5 +2506,7 @@ runtime·topofstack(Func *f)
return f->entry == (uintptr)runtime·goexit ||
f->entry == (uintptr)runtime·mstart ||
f->entry == (uintptr)runtime·mcall ||
+\t\tf->entry == (uintptr)runtime·morestack ||
+\t\tf->entry == (uintptr)runtime·lessstack ||
f->entry == (uintptr)_rt0_go;
}
runtime·haszeroargs
関数が完全に削除されています。また、runtime·topofstack
関数に runtime·morestack
と runtime·lessstack
が追加され、これらの関数がスタックの最上位を示すものとして認識されるようになりました。
src/pkg/runtime/traceback_arm.c
, src/pkg/runtime/traceback_x86.c
(C言語コード)
--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -69,7 +69,8 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
\tf = runtime·findfunc(frame.pc);\
\tif(f == nil) {\
\t\truntime·printf("runtime: unknown pc %p after stack split\\n", frame.pc);\
-\t\t\t\truntime·throw("unknown pc");
+\t\t\t\tif(callback != nil)\
+\t\t\t\t\truntime·throw("unknown pc");
\t}\
\tframe.fn = f;\
\tcontinue;\
@@ -89,7 +90,8 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
\tflr = runtime·findfunc(frame.lr);\
\tif(flr == nil) {\
\t\truntime·printf("runtime: unexpected return pc for %s called from %p\\n", runtime·funcname(f), frame.lr);\
-\t\t\t\truntime·throw("unknown caller pc");
+\t\t\t\tif(callback != nil)\
+\t\t\t\t\truntime·throw("unknown caller pc");
\t}\
}\
\t\t
@@ -112,7 +114,7 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
\telse {\
\t\truntime·printf("runtime: unknown argument frame size for %s called from %p [%s]\\n",\
\t\t\truntime·funcname(f), frame.lr, flr ? runtime·funcname(flr) : "?");\
-\t\t\t\tif(!printing)\
+\t\t\t\tif(callback != nil)\
\t\t\truntime·throw("invalid stack");
\t\tframe.arglen = 0;\
\t}\
@@ -131,7 +133,8 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
} else {\
\tif(f->locals > frame.fp - frame.sp) {\
\t\truntime·printf("runtime: inconsistent locals=%p frame=%p fp=%p sp=%p for %s\\n", (uintptr)f->locals, (uintptr)f->frame, frame.fp, frame.sp, runtime·funcname(f));\
-\t\t\t\truntime·throw("invalid stack");
+\t\t\t\tif(callback != nil)\
+\t\t\t\t\truntime·throw("invalid stack");
\t}\
\tframe.varp = (byte*)frame.fp - f->locals;\
\tframe.varlen = f->locals;\
runtime·gentraceback
関数内で、runtime·throw
の呼び出しが if(callback != nil)
という条件で囲まれています。これにより、スタックトレースの収集がコールバック関数を伴う場合(例えば、プロファイリングツールなど)にのみ、不正なスタック情報が検出された際にパニックを引き起こすようになります。通常のスタックトレース表示時には、エラーメッセージは出力されますが、プログラムは継続されます。
コアとなるコードの解説
アセンブリコードの変更 (TEXT
ディレクティブ)
Goのアセンブリでは、TEXT
ディレクティブは関数の定義に使用されます。その書式は TEXT symbol(SB), flags, args-framesize
です。
symbol(SB)
: 関数のシンボル名。SB
はStatic Baseで、グローバルシンボルであることを示します。flags
: 関数の特性を示すフラグ。このコミットでは7
が使われており、これはNOSPLIT
(スタック分割しない) とRODATA
(読み取り専用データ) の組み合わせです。args-framesize
: ここが今回の変更の肝です。args
: 関数が受け取る引数の合計サイズ(バイト単位)。framesize
: 関数が使用するローカル変数とレジスタ退避領域の合計サイズ(バイト単位)。args-framesize
の形式で指定することで、ランタイムはスタックフレームの構造を正確に把握できます。
変更前は TEXT runtime·morestack(SB),7,$0
のように $0
とだけ指定されていました。これは引数サイズが0であることを示しますが、フレームサイズについては明示されていませんでした。変更後は TEXT runtime·morestack(SB),7,$0-0
となり、引数サイズが0、かつフレームサイズも0であることを明確に指定しています。これにより、morestack
や lessstack
がスタックトレースに現れた際に、スタックトレース機構がこれらの関数がスタックフレームを消費しない特殊な関数であることを正しく認識できるようになりました。
proc.c
の変更 (runtime·haszeroargs
の削除と runtime·topofstack
の更新)
runtime·haszeroargs
の削除: この関数は、特定のランタイム関数(goexit
,mcall
,mstart
,lessstack
,morestack
,_rt0_go
)が引数を持たないことをハードコードで判定していました。しかし、アセンブリコードのTEXT
ディレクティブで引数サイズを明示的に0
と指定するようになったため、この関数は冗長となり削除されました。これは、ランタイムのコードベースをより簡潔にし、一貫性のあるスタックフレーム情報の管理方法に移行したことを意味します。runtime·topofstack
の更新: この関数は、与えられた関数がゴルーチンスタックの最上位にあるかどうかを判断するために使用されます。つまり、その関数より上位にはユーザーコードのスタックフレームが存在しないことを示します。morestack
やlessstack
はスタックの伸縮処理を行う特殊な関数であり、これらがスタックトレースの最上位に現れた場合、それ以上ユーザーコードのスタックフレームを遡る必要はありません。このコミットでruntime·morestack
とruntime·lessstack
がこのリストに追加されたことで、スタックトレースがこれらの関数で適切に終端され、不必要なスタックフレームの探索や誤った解析を防ぐことができます。
traceback_*.c
の変更 (条件付き runtime·throw
)
runtime·gentraceback
関数は、スタックトレースを生成する主要な関数です。この関数内で、不正なPCやリターンPC、または不明な引数フレームサイズが検出された場合に runtime·throw
を呼び出す箇所がありました。runtime·throw
はGoのパニックに相当し、プログラムを異常終了させます。
変更前は、これらのエラーが検出されると常に runtime·throw
が呼び出されていました。しかし、スタックトレースの収集はデバッグ目的だけでなく、プロファイリングツールなど、プログラムの実行を中断させたくない状況でも行われます。
このコミットでは、runtime·throw
の呼び出しが if(callback != nil)
という条件で囲まれました。これは、スタックトレースの収集がコールバック関数を伴う場合(例えば、プロファイリングツールがスタックトレースを収集し、その結果をコールバック関数に渡す場合など)にのみ、不正なスタック情報が検出された際にパニックを引き起こすように変更されたことを意味します。通常のスタックトレース表示時など、callback
が nil
の場合は、エラーメッセージは出力されますが、プログラムは継続されます。これにより、スタックトレース機構の堅牢性が向上し、より多様なユースケースに対応できるようになりました。
関連リンク
- Go CL 11533043: https://golang.org/cl/11533043
参考にした情報源リンク
- Goのスタック管理に関する公式ドキュメントやブログ記事 (一般的なGoランタイムのスタック伸縮、
g0
、morestack
/lessstack
の概念理解のため) - Goのアセンブリ言語に関するドキュメント (特に
TEXT
ディレクティブのargs-framesize
の意味について) - Goのガベージコレクションに関する資料 (GCが
g0
上で実行されることの理解のため) - Goのスタックトレースの仕組みに関する技術記事 (スタックフレームの解析方法の理解のため)