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

[インデックス 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 が現れる可能性があったことです。

従来のスタックトレース機構は、これらの特殊な関数を適切に扱えず、スタックトレースが途中で途切れてしまう、あるいは不正なスタックフレームサイズを報告するといった問題が発生していました。これは、デバッグ時やプロファイリング時に正確なスタック情報が得られないという重大な課題でした。このコミットは、この問題を解決し、morestacklessstack がスタックトレースに現れても、正確なスタック情報を取得できるようにすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。

  1. ゴルーチン (Goroutine): Goにおける軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持ちます。
  2. スタック (Stack): 関数呼び出しの際に、ローカル変数、引数、リターンアドレスなどを格納するメモリ領域です。Goのゴルーチンスタックは、必要に応じて動的にサイズが変更されます(スタックの伸縮)。
  3. g0 ゴルーチン: Goランタイムには、ユーザーゴルーチンとは別に、ランタイム自身の処理(スケジューリング、ガベージコレクション、システムコールなど)を実行するための特別なゴルーチンが存在します。これが g0 ゴルーチンです。g0 は固定サイズのスタックを持ち、ユーザーゴルーチンのスタックとは異なる管理がされます。
  4. morestack 関数: ゴルーチンが関数を呼び出す際に、現在のスタックが不足していると判断された場合に、プロローグ(関数の冒頭部分)で自動的に呼び出されるランタイム関数です。この関数は、より大きなスタックを確保し、新しいスタックに実行を移します。
  5. lessstack 関数: morestack とは逆に、スタックが過剰に確保されている場合に、スタックを縮小するために呼び出されるランタイム関数です。
  6. スタックトレース (Stack Trace): プログラムの実行中に、現在実行中の関数から呼び出し元の関数へと遡って、一連の関数呼び出しの履歴を表示するものです。デバッグやエラー解析に不可欠な情報です。
  7. スタックフレーム (Stack Frame): 各関数呼び出しに対応するスタック上の領域です。これには、関数の引数、ローカル変数、リターンアドレスなどが含まれます。スタックトレースは、これらのスタックフレームを順に辿ることで生成されます。
  8. 引数フレームサイズ (Argument Frame Size): 関数が呼び出された際に、スタック上に確保される引数領域のサイズです。スタックトレースを正確に生成するためには、各関数の引数フレームサイズを正確に知る必要があります。
  9. TEXT ディレクティブ (Goアセンブリ): Goのアセンブリ言語で関数を定義する際に使用されるディレクティブです。TEXT symbol(SB), flags, args-framesize の形式で記述され、args-framesize の部分で引数とローカル変数の合計サイズを指定します。このサイズはスタックトレースの生成に利用されます。

技術的詳細

このコミットの核心は、morestacklessstack がスタックトレースに現れた際に、それらを「スタックの終端」として扱い、かつ「引数フレームサイズが0である」と明示的にランタイムに伝える点にあります。

Goのスタックトレース機構は、各関数のスタックフレームを解析し、リターンアドレスや引数、ローカル変数の情報を抽出することで機能します。この解析には、各関数の引数フレームサイズが重要な情報となります。しかし、morestacklessstack は通常のGo関数とは異なり、コンパイラによって生成される通常の関数プロローグを持たず、手書きのアセンブリで実装されています。そのため、これらの関数がスタックトレースの対象となった場合、ランタイムは正しい引数フレームサイズを推測できず、スタックトレースが破損する可能性がありました。

このコミットでは、以下の具体的な変更が行われています。

  1. アセンブリコードの変更 (asm_*.s):

    • runtime·morestackruntime·lessstackTEXT ディレクティブにおいて、引数フレームサイズを明示的に 0-0 と設定しました。これは、「引数はなく、ローカル変数もスタックフレームを消費しない」ことを意味します。これにより、スタックトレース機構がこれらの関数を正しく認識し、その後のスタックフレームの解析を継続できるようになります。
    • ARMアーキテクチャの runtime·morestackruntime·lessstack の呼び出しが B (Branch) から BL (Branch with Link) に変更されました。これは、newstackoldstack を呼び出す際にリターンアドレスを LR レジスタに保存するようにするためです。これにより、スタックトレースがより正確になります。
  2. C言語コードの変更 (proc.c, traceback_*.c):

    • runtime·haszeroargs 関数が削除されました。この関数は、特定のランタイム関数が引数を持たないことをハードコードで判断していましたが、TEXT ディレクティブでの引数フレームサイズの明示的な指定により、この関数は不要になりました。
    • runtime·topofstack 関数に runtime·morestackruntime·lessstack が追加されました。この関数は、特定の関数がゴルーチンスタックの最上位(つまり、その関数より上位にはユーザーコードのスタックフレームがない)を示すかどうかを判断します。これにより、スタックトレースがこれらの関数で適切に終端されるようになります。
    • traceback_arm.c および traceback_x86.c において、スタックトレース中に不正なPC(プログラムカウンタ)やリターンPC、または不明な引数フレームサイズが検出された場合に runtime·throw を呼び出す条件が変更されました。具体的には、callback != nil の場合にのみ runtime·throw を呼び出すようになりました。これは、スタックトレースがデバッグ目的で表示される際に、必ずしも致命的なエラーとして処理する必要がない場合があるためです。例えば、プロファイリングツールがスタックトレースを収集している場合などです。

これらの変更により、GCが g0 上でスタックの伸縮中に発生しても、morestacklessstack がスタックトレースに現れることが許容され、かつそれらがスタックの終端として正しく扱われることで、スタックトレースの正確性と堅牢性が向上しました。

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

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) 命令に変更され、newstackoldstack への呼び出しでリターンアドレスがリンクレジスタ (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·morestackruntime·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であることを明確に指定しています。これにより、morestacklessstack がスタックトレースに現れた際に、スタックトレース機構がこれらの関数がスタックフレームを消費しない特殊な関数であることを正しく認識できるようになりました。

proc.c の変更 (runtime·haszeroargs の削除と runtime·topofstack の更新)

  • runtime·haszeroargs の削除: この関数は、特定のランタイム関数(goexit, mcall, mstart, lessstack, morestack, _rt0_go)が引数を持たないことをハードコードで判定していました。しかし、アセンブリコードの TEXT ディレクティブで引数サイズを明示的に 0 と指定するようになったため、この関数は冗長となり削除されました。これは、ランタイムのコードベースをより簡潔にし、一貫性のあるスタックフレーム情報の管理方法に移行したことを意味します。
  • runtime·topofstack の更新: この関数は、与えられた関数がゴルーチンスタックの最上位にあるかどうかを判断するために使用されます。つまり、その関数より上位にはユーザーコードのスタックフレームが存在しないことを示します。morestacklessstack はスタックの伸縮処理を行う特殊な関数であり、これらがスタックトレースの最上位に現れた場合、それ以上ユーザーコードのスタックフレームを遡る必要はありません。このコミットで runtime·morestackruntime·lessstack がこのリストに追加されたことで、スタックトレースがこれらの関数で適切に終端され、不必要なスタックフレームの探索や誤った解析を防ぐことができます。

traceback_*.c の変更 (条件付き runtime·throw)

runtime·gentraceback 関数は、スタックトレースを生成する主要な関数です。この関数内で、不正なPCやリターンPC、または不明な引数フレームサイズが検出された場合に runtime·throw を呼び出す箇所がありました。runtime·throw はGoのパニックに相当し、プログラムを異常終了させます。

変更前は、これらのエラーが検出されると常に runtime·throw が呼び出されていました。しかし、スタックトレースの収集はデバッグ目的だけでなく、プロファイリングツールなど、プログラムの実行を中断させたくない状況でも行われます。

このコミットでは、runtime·throw の呼び出しが if(callback != nil) という条件で囲まれました。これは、スタックトレースの収集がコールバック関数を伴う場合(例えば、プロファイリングツールがスタックトレースを収集し、その結果をコールバック関数に渡す場合など)にのみ、不正なスタック情報が検出された際にパニックを引き起こすように変更されたことを意味します。通常のスタックトレース表示時など、callbacknil の場合は、エラーメッセージは出力されますが、プログラムは継続されます。これにより、スタックトレース機構の堅牢性が向上し、より多様なユースケースに対応できるようになりました。

関連リンク

参考にした情報源リンク

  • Goのスタック管理に関する公式ドキュメントやブログ記事 (一般的なGoランタイムのスタック伸縮、g0morestack/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 が現れる可能性があったことです。

従来のスタックトレース機構は、これらの特殊な関数を適切に扱えず、スタックトレースが途中で途切れてしまう、あるいは不正なスタックフレームサイズを報告するといった問題が発生していました。これは、デバッグ時やプロファイリング時に正確なスタック情報が得られないという重大な課題でした。このコミットは、この問題を解決し、morestacklessstack がスタックトレースに現れても、正確なスタック情報を取得できるようにすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。

  1. ゴルーチン (Goroutine): Goにおける軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持ちます。
  2. スタック (Stack): 関数呼び出しの際に、ローカル変数、引数、リターンアドレスなどを格納するメモリ領域です。Goのゴルーチンスタックは、必要に応じて動的にサイズが変更されます(スタックの伸縮)。
  3. g0 ゴルーチン: Goランタイムには、ユーザーゴルーチンとは別に、ランタイム自身の処理(スケジューリング、ガベージコレクション、システムコールなど)を実行するための特別なゴルーチンが存在します。これが g0 ゴルーチンです。g0 は固定サイズのスタックを持ち、ユーザーゴルーチンのスタックとは異なる管理がされます。
  4. morestack 関数: ゴルーチンが関数を呼び出す際に、現在のスタックが不足していると判断された場合に、プロローグ(関数の冒頭部分)で自動的に呼び出されるランタイム関数です。この関数は、より大きなスタックを確保し、新しいスタックに実行を移します。
  5. lessstack 関数: morestack とは逆に、スタックが過剰に確保されている場合に、スタックを縮小するために呼び出されるランタイム関数です。
  6. スタックトレース (Stack Trace): プログラムの実行中に、現在実行中の関数から呼び出し元の関数へと遡って、一連の関数呼び出しの履歴を表示するものです。デバッグやエラー解析に不可欠な情報です。
  7. スタックフレーム (Stack Frame): 各関数呼び出しに対応するスタック上の領域です。これには、関数の引数、ローカル変数、リターンアドレスなどが含まれます。スタックトレースは、これらのスタックフレームを順に辿ることで生成されます。
  8. 引数フレームサイズ (Argument Frame Size): 関数が呼び出された際に、スタック上に確保される引数領域のサイズです。スタックトレースを正確に生成するためには、各関数の引数フレームサイズを正確に知る必要があります。
  9. TEXT ディレクティブ (Goアセンブリ): Goのアセンブリ言語で関数を定義する際に使用されるディレクティブです。TEXT symbol(SB), flags, args-framesize の形式で記述され、args-framesize の部分で引数とローカル変数の合計サイズを指定します。このサイズはスタックトレースの生成に利用されます。

技術的詳細

このコミットの核心は、morestacklessstack がスタックトレースに現れた際に、それらを「スタックの終端」として扱い、かつ「引数フレームサイズが0である」と明示的にランタイムに伝える点にあります。

Goのスタックトレース機構は、各関数のスタックフレームを解析し、リターンアドレスや引数、ローカル変数の情報を抽出することで機能します。この解析には、各関数の引数フレームサイズが重要な情報となります。しかし、morestacklessstack は通常のGo関数とは異なり、コンパイラによって生成される通常の関数プロローグを持たず、手書きのアセンブリで実装されています。そのため、これらの関数がスタックトレースの対象となった場合、ランタイムは正しい引数フレームサイズを推測できず、スタックトレースが破損する可能性がありました。

このコミットでは、以下の具体的な変更が行われています。

  1. アセンブリコードの変更 (asm_*.s):

    • runtime·morestackruntime·lessstackTEXT ディレクティブにおいて、引数フレームサイズを明示的に 0-0 と設定しました。これは、「引数はなく、ローカル変数もスタックフレームを消費しない」ことを意味します。これにより、スタックトレース機構がこれらの関数を正しく認識し、その後のスタックフレームの解析を継続できるようになります。
    • ARMアーキテクチャの runtime·morestackruntime·lessstack の呼び出しが B (Branch) から BL (Branch with Link) に変更されました。これは、newstackoldstack を呼び出す際にリターンアドレスを LR レジスタに保存するようにするためです。これにより、スタックトレースがより正確になります。
  2. C言語コードの変更 (proc.c, traceback_*.c):

    • runtime·haszeroargs 関数が削除されました。この関数は、特定のランタイム関数が引数を持たないことをハードコードで判断していましたが、TEXT ディレクティブでの引数フレームサイズの明示的な指定により、この関数は不要になりました。
    • runtime·topofstack 関数に runtime·morestackruntime·lessstack が追加されました。この関数は、特定の関数がゴルーチンスタックの最上位(つまり、その関数より上位にはユーザーコードのスタックフレームがない)を示すかどうかを判断します。これにより、スタックトレースがこれらの関数で適切に終端されるようになります。
    • traceback_arm.c および traceback_x86.c において、スタックトレース中に不正なPC(プログラムカウンタ)やリターンPC、または不明な引数フレームサイズが検出された場合に runtime·throw を呼び出す条件が変更されました。具体的には、callback != nil の場合にのみ runtime·throw を呼び出すようになりました。これは、スタックトレースがデバッグ目的で表示される際に、必ずしも致命的なエラーとして処理する必要がない場合があるためです。例えば、プロファイリングツールがスタックトレースを収集している場合などです。

これらの変更により、GCが g0 上でスタックの伸縮中に発生しても、morestacklessstack がスタックトレースに現れることが許容され、かつそれらがスタックの終端として正しく扱われることで、スタックトレースの正確性と堅牢性が向上しました。

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

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) 命令に変更され、newstackoldstack への呼び出しでリターンアドレスがリンクレジスタ (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·morestackruntime·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であることを明確に指定しています。これにより、morestacklessstack がスタックトレースに現れた際に、スタックトレース機構がこれらの関数がスタックフレームを消費しない特殊な関数であることを正しく認識できるようになりました。

proc.c の変更 (runtime·haszeroargs の削除と runtime·topofstack の更新)

  • runtime·haszeroargs の削除: この関数は、特定のランタイム関数(goexit, mcall, mstart, lessstack, morestack, _rt0_go)が引数を持たないことをハードコードで判定していました。しかし、アセンブリコードの TEXT ディレクティブで引数サイズを明示的に 0 と指定するようになったため、この関数は冗長となり削除されました。これは、ランタイムのコードベースをより簡潔にし、一貫性のあるスタックフレーム情報の管理方法に移行したことを意味します。
  • runtime·topofstack の更新: この関数は、与えられた関数がゴルーチンスタックの最上位にあるかどうかを判断するために使用されます。つまり、その関数より上位にはユーザーコードのスタックフレームが存在しないことを示します。morestacklessstack はスタックの伸縮処理を行う特殊な関数であり、これらがスタックトレースの最上位に現れた場合、それ以上ユーザーコードのスタックフレームを遡る必要はありません。このコミットで runtime·morestackruntime·lessstack がこのリストに追加されたことで、スタックトレースがこれらの関数で適切に終端され、不必要なスタックフレームの探索や誤った解析を防ぐことができます。

traceback_*.c の変更 (条件付き runtime·throw)

runtime·gentraceback 関数は、スタックトレースを生成する主要な関数です。この関数内で、不正なPCやリターンPC、または不明な引数フレームサイズが検出された場合に runtime·throw を呼び出す箇所がありました。runtime·throw はGoのパニックに相当し、プログラムを異常終了させます。

変更前は、これらのエラーが検出されると常に runtime·throw が呼び出されていました。しかし、スタックトレースの収集はデバッグ目的だけでなく、プロファイリングツールなど、プログラムの実行を中断させたくない状況でも行われます。

このコミットでは、runtime·throw の呼び出しが if(callback != nil) という条件で囲まれました。これは、スタックトレースの収集がコールバック関数を伴う場合(例えば、プロファイリングツールがスタックトレースを収集し、その結果をコールバック関数に渡す場合など)にのみ、不正なスタック情報が検出された際にパニックを引き起こすように変更されたことを意味します。通常のスタックトレース表示時など、callbacknil の場合は、エラーメッセージは出力されますが、プログラムは継続されます。これにより、スタックトレース機構の堅牢性が向上し、より多様なユースケースに対応できるようになりました。

関連リンク

参考にした情報源リンク

  • Goのスタック管理に関する公式ドキュメントやブログ記事 (一般的なGoランタイムのスタック伸縮、g0morestack/lessstack の概念理解のため)
  • Goのアセンブリ言語に関するドキュメント (特に TEXT ディレクティブの args-framesize の意味について)
  • Goのガベージコレクションに関する資料 (GCが g0 上で実行されることの理解のため)
  • Goのスタックトレースの仕組みに関する技術記事 (スタックフレームの解析方法の理解のため)