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

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

このコミットは、Goランタイムにおけるスタックオーバーフローチェックの精度向上と、reflect.call時のスタックトレースの改善、さらにはプリエンプション(横取り)の可能性を広げるための変更です。具体的には、スタックオーバーフローチェックに使用するスタックポインタの参照元をm->morebuf.spからgp->sched.spに変更し、それに伴いreflect.call内でgp->schedの情報を適切に記録するように修正しています。

コミット

commit f0d73fbc7c24ea9d81f24732896a99778f623f80
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 27 16:51:06 2013 -0400

    runtime: use gp->sched.sp for stack overflow check

    On x86 it is a few words lower on the stack than m->morebuf.sp
    so it is a more precise check. Enabling the check requires recording
    a valid gp->sched in reflect.call too. This is a good thing in general,
    since it will make stack traces during reflect.call work better, and it
    may be useful for preemption too.

    R=dvyukov
    CC=golang-dev
    https://golang.org/cl/10709043

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

https://github.com/golang/go/commit/f0d73fbc7c24ea9d81f24732896a99778f623f80

元コミット内容

runtime: use gp->sched.sp for stack overflow check

On x86 it is a few words lower on the stack than m->morebuf.sp
so it is a more precise check. Enabling the check requires recording
a valid gp->sched in reflect.call too. This is a good thing in general,
since it will make stack traces during reflect.call work better, and it
may be useful for preemption too.

変更の背景

この変更の主な背景は、Goランタイムにおけるスタックオーバーフローチェックの精度を向上させることです。従来のm->morebuf.spを使用する方法では、特にx86アーキテクチャにおいて、実際のスタックの限界よりも少し高い位置を参照していたため、スタックオーバーフローを検出するタイミングが遅れる可能性がありました。

gp->sched.spを使用することで、より正確なスタックポインタの位置を把握できるようになり、スタックオーバーフローをより早期に、かつ正確に検出することが可能になります。

また、この変更はreflect.call(リフレクションによる関数呼び出し)の挙動にも影響を与えます。reflect.callは、Goのランタイムが通常の関数呼び出しとは異なる方法でスタックを管理するため、スタックトレースが不完全になるなどの問題がありました。gp->sched情報を適切に記録することで、reflect.call中のスタックトレースがより正確になり、デバッグが容易になります。

さらに、この変更は将来的なプリエンプション(協調的ではないゴルーチンの横取り)の実装にも寄与する可能性があります。正確なスタックポインタ情報を持つことは、ランタイムがゴルーチンの実行を中断し、別のゴルーチンに切り替える際に不可欠な要素となります。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とアセンブリ言語の知識が必要です。

Goランタイム (Go Runtime)

Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、システムコールなど、Goプログラムの実行に必要な低レベルな処理を担っています。

ゴルーチン (Goroutine)

ゴルーチンはGoにおける軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。各ゴルーチンは独自のスタックを持ち、ランタイムによってスケジューリングされます。

スタック (Stack)

スタックは、関数呼び出しの際にローカル変数、引数、戻りアドレスなどを格納するために使用されるメモリ領域です。関数が呼び出されるたびにスタックフレームが積まれ、関数から戻る際にスタックフレームが解放されます。スタックは通常、メモリ上の高いアドレスから低いアドレスに向かって成長します。

スタックオーバーフロー (Stack Overflow)

スタックオーバーフローは、関数呼び出しが深くなりすぎたり、大きなローカル変数を確保しすぎたりして、スタック領域が割り当てられたメモリ範囲を超えてしまうエラーです。これにより、プログラムがクラッシュしたり、予期せぬ動作を引き起こしたりします。Goランタイムは、スタックオーバーフローを検出してスタックを拡張するメカニズム(morestack)を持っています。

reflect.call (リフレクションによる関数呼び出し)

Goのリフレクション機能は、実行時に型情報や値情報を操作することを可能にします。reflect.Call関数は、reflect.Valueとして表現された関数を動的に呼び出すために使用されます。通常の関数呼び出しとは異なり、reflect.callはランタイムが特別な処理を行うため、スタックの管理方法が異なります。

m->morebuf.spgp->sched.sp

Goランタイムは、M(Machine)、P(Processor)、G(Goroutine)という3つの主要な抽象化を用いてゴルーチンをスケジューリングします。

  • M (Machine): OSのスレッドに対応します。
  • P (Processor): Mがゴルーチンを実行するための論理的なプロセッサです。
  • G (Goroutine): ゴルーチンそのものです。

これらの構造体には、スタックポインタやレジスタの状態を保存するためのフィールドが含まれています。

  • m->morebuf.sp: M(OSスレッド)に関連付けられたmorebuf構造体内のスタックポインタです。これは、スタック拡張(morestack)の際に使用される一時的なバッファに関連するスタックポインタを指すことがあります。
  • gp->sched.sp: G(ゴルーチン)に関連付けられたsched構造体内のスタックポインタです。これは、ゴルーチンのスケジューリング情報の一部として、そのゴルーチンの現在のスタックポインタを保持します。gobuf構造体の一部として定義されており、ゴルーチンの実行状態を保存・復元するために使われます。

このコミットのポイントは、m->morebuf.spが必ずしもゴルーチンの正確なスタックポインタを指しているわけではないのに対し、gp->sched.spはゴルーチン自身のスケジューリングコンテキストの一部として、より正確なスタックポインタ情報を提供することです。

gobuf 構造体

gobufは、ゴルーチンの実行コンテキスト(プログラムカウンタ、スタックポインタ、フレームポインタなど)を保存するための構造体です。ゴルーチンの切り替えやスタックの拡張など、コンテキストスイッチが必要な場面で利用されます。

プリエンプション (Preemption)

プリエンプションとは、実行中のゴルーチンをランタイムが強制的に中断し、別のゴルーチンにCPUを明け渡させるメカニズムです。Goの初期バージョンでは協調的プリエンプション(ゴルーチンが自発的に実行を中断する)が主でしたが、より公平なスケジューリングやレイテンシの改善のために、非協調的(強制的な)プリエンプションが導入されていきました。正確なスタックポインタ情報は、プリエンプションの実現に不可欠です。

アセンブリ言語 (Assembly Language)

Goランタイムの一部、特にコンテキストスイッチやスタック操作などの低レベルな処理は、アセンブリ言語で記述されています。このコミットでは、x86 (386, amd64) および ARM アーキテクチャ向けのアセンブリコードが変更されています。

技術的詳細

このコミットの核心は、Goランタイムがスタックオーバーフローを検出する際の基準となるスタックポインタの参照を、より正確なものに切り替える点にあります。

  1. スタックオーバーフローチェックの精度向上:

    • 従来のGoランタイムでは、スタックオーバーフローのチェックにm->morebuf.spが使用されていました。しかし、このポインタはOSスレッド(M)のコンテキストに関連しており、特にreflect.callのような特殊な呼び出しパスでは、ゴルーチン(G)の実際のスタックポインタとわずかにずれることがありました。コミットメッセージにあるように、x86アーキテクチャでは「数ワード低い」位置が実際のスタックの限界に近いとされています。
    • このコミットでは、スタックオーバーフローチェックの基準をgp->sched.spに変更します。gp->sched.spは、現在実行中のゴルーチン(gp)のスケジューリングコンテキスト(sched)に保存されているスタックポインタです。これは、ゴルーチン自身の状態を正確に反映するため、より精密なスタックオーバーフロー検出が可能になります。これにより、スタックオーバーフローが実際に発生する直前でmorestackルーチンが呼び出され、スタックが拡張されるようになります。
  2. reflect.callにおけるgp->schedの記録:

    • reflect.callは、Goのリフレクション機能を使って関数を動的に呼び出すためのメカニズムです。この呼び出しパスは、通常のGo関数呼び出しとは異なり、ランタイムがスタックを特別に管理します。
    • このコミット以前は、reflect.callが実行されている間、gp->sched(ゴルーチンのスケジューリング情報)が常に有効な状態を保っているわけではありませんでした。これは、reflect.call中にスタックトレースを取得しようとすると、不完全な情報しか得られない原因となっていました。
    • 今回の変更では、reflect.callのアセンブリコード内で、現在のプログラムカウンタ(reflect.callのアドレス)と現在のスタックポインタ(SPレジスタの値)を、それぞれgp->sched.pcgp->sched.spに明示的に保存するようにしました。これにより、reflect.callが実行されている間も、ゴルーチンのスケジューリング情報が常に最新かつ正確な状態に保たれます。
  3. スタックトレースの改善:

    • gp->sched情報がreflect.call中も正確に記録されるようになったことで、reflect.callを介して呼び出された関数からのスタックトレースがより完全で意味のあるものになります。デバッグ時やエラー発生時の原因究明に役立ちます。
  4. プリエンプションへの寄与:

    • Goランタイムがゴルーチンをプリエンプト(横取り)するためには、そのゴルーチンの現在の実行状態(特にスタックポインタとプログラムカウンタ)を正確に保存し、後で復元できる必要があります。
    • gp->sched.spが常に正確なスタックポインタを指すようにすることで、将来的にGoランタイムがより洗練されたプリエンプションメカニズムを実装する際の基盤となります。これにより、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げることなく、より公平なスケジューリングが可能になります。

この変更は、Goランタイムの堅牢性とデバッグ可能性を向上させるとともに、将来的なパフォーマンス最適化やスケジューリング改善のための重要なステップと言えます。

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

このコミットでは、主に以下の4つのファイルが変更されています。

  1. src/pkg/runtime/asm_386.s (x86 32-bit アセンブリ)
  2. src/pkg/runtime/asm_amd64.s (x86 64-bit アセンブリ)
  3. src/pkg/runtime/asm_arm.s (ARM 32-bit アセンブリ)
  4. src/pkg/runtime/stack.c (C言語)

src/pkg/runtime/asm_386.s および src/pkg/runtime/asm_amd64.s の変更

これらのファイルでは、TEXT reflect·call(SB)セクションに以下の行が追加されています。

--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -254,6 +254,11 @@ TEXT reflect·call(SB), 7, $0
  	MOVL	g(CX), AX
  	MOVL	AX, (m_morebuf+gobuf_g)(BX)
 
+	// Save our own state as the PC and SP to restore
+	// if this goroutine needs to be restarted.
+	MOVL	$reflect·call(SB), (g_sched+gobuf_pc)(AX)
+	MOVL	SP, (g_sched+gobuf_sp)(AX)
+
  	// Set up morestack arguments to call f on a new stack.
  	// We set f's frame size to 1, as a hint to newstack
  	// that this is a call from reflect·call.

asm_amd64.sも同様の変更です(レジスタ名がMOVLからMOVQに変わるなど、64-bitアーキテクチャに合わせた違いはあります)。

  • MOVL $reflect·call(SB), (g_sched+gobuf_pc)(AX):
    • $reflect·call(SB)は、reflect.call関数の開始アドレス(プログラムカウンタ)を指します。
    • AXレジスタには現在のゴルーチン(g)のアドレスが格納されています。
    • (g_sched+gobuf_pc)(AX)は、ゴルーチン構造体g内のschedフィールド(gobuf型)のpc(プログラムカウンタ)フィールドへのオフセットアドレスを示します。
    • この命令は、reflect.callの開始アドレスを現在のゴルーチンのgobufpcフィールドに保存します。
  • MOVL SP, (g_sched+gobuf_sp)(AX):
    • SPは現在のスタックポインタレジスタです。
    • (g_sched+gobuf_sp)(AX)は、ゴルーチン構造体g内のschedフィールドのsp(スタックポインタ)フィールドへのオフセットアドレスを示します。
    • この命令は、現在のスタックポインタの値をゴルーチンのgobufspフィールドに保存します。

src/pkg/runtime/asm_arm.s の変更

ARMアーキテクチャ向けのアセンブリファイルでも同様に、TEXT reflect·call(SB)セクションに以下の行が追加されています。

--- a/src/pkg/runtime/asm_arm.s
+++ b/src/pkg/runtime/asm_arm.s
@@ -207,6 +207,13 @@ TEXT reflect·call(SB), 7, $-4
  	MOVW	SP, (m_morebuf+gobuf_sp)(m)	// our caller's SP
  	MOVW	g,  (m_morebuf+gobuf_g)(m)
 
+	// Save our own state as the PC and SP to restore
+	// if this goroutine needs to be restarted.
+	MOVW	$reflect·call(SB), R11
+	MOVW	R11, (g_sched+gobuf_pc)(g)
+	MOVW	LR, (g_sched+gobuf_lr)(g)
+	MOVW	SP, (g_sched+gobuf_sp)(g)
+
  	// Set up morestack arguments to call f on a new stack.
  	// We set f's frame size to 1, as a hint to newstack
  	// that this is a call from reflect·call.

ARMでは、LR(リンクレジスタ)もg_sched+gobuf_lrに保存されています。これはARMの関数呼び出し規約に関連しており、戻りアドレスがLRに格納されるためです。

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

C言語で記述されたruntime·newstack関数内で、スタックポインタの参照元が変更されています。

--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -215,7 +215,7 @@ runtime·newstack(void)\n 	if(!reflectcall)\n 	\truntime·rewindmorestack(&gp->sched);\n 
-\tsp = m->morebuf.sp;\n+\tsp = gp->sched.sp;\n 	if(thechar == '6' || thechar == '8') {
 	// The call to morestack cost a word.
 	// sp -= sizeof(uintptr);
  • runtime·newstackは、スタックオーバーフローが発生した際に新しいスタックを割り当てるためのランタイム関数です。
  • 変更前は、sp = m->morebuf.sp;として、M(OSスレッド)のmorebuf内のスタックポインタをspに代入していました。
  • 変更後は、sp = gp->sched.sp;として、現在のゴルーチン(gp)のsched内のスタックポインタをspに代入しています。

コアとなるコードの解説

アセンブリコードの変更 (asm_386.s, asm_amd64.s, asm_arm.s)

reflect.callは、Goのリフレクション機能を使って関数を動的に呼び出すための特殊なパスです。このパスでは、通常のGo関数呼び出しとは異なるスタック管理が行われるため、ランタイムがゴルーチンの状態を正確に把握することが難しい場合がありました。

追加されたアセンブリ命令は、reflect.callが実行される直前に、現在のゴルーチン(g)のschedフィールド(gobuf構造体)に、reflect.callの開始アドレス(pc)と現在のスタックポインタ(sp)を明示的に保存しています。

  • g_sched+gobuf_pcへのreflect.callアドレスの保存:
    • これは、ゴルーチンがreflect.callのどの位置で実行を中断しても、そのゴルーチンがreflect.callのコンテキストで実行されていたことを正確に識別できるようにするためです。スタックトレースを生成する際に、この情報が利用され、より完全なトレースが可能になります。
  • g_sched+gobuf_spへの現在のスタックポインタの保存:
    • これは、reflect.callが実行されている間のゴルーチンの正確なスタックポインタを記録するためです。これにより、スタックオーバーフローチェックがより正確に行われるようになります。また、将来的にプリエンプションが導入された場合、ゴルーチンを中断して後で再開する際に、この正確なスタックポインタ情報が必要になります。

これらの変更により、reflect.callがGoランタイムの他の部分(特にスタック管理とスケジューリング)とより密接に連携できるようになり、リフレクションを使用する際の堅牢性とデバッグ可能性が向上します。

Cコードの変更 (stack.c)

runtime·newstack関数は、Goランタイムがスタックオーバーフローを検出した際に呼び出され、ゴルーチンのスタックを拡張する役割を担っています。

  • sp = m->morebuf.sp; から sp = gp->sched.sp; への変更:
    • この変更は、スタックオーバーフローチェックの基準を、OSスレッド(M)に関連する一時的なスタックポインタ(m->morebuf.sp)から、現在実行中のゴルーチン(gp)自身のスケジューリングコンテキストに保存されているスタックポインタ(gp->sched.sp)に切り替えることを意味します。
    • gp->sched.spは、ゴルーチンの実際のスタックポインタをより正確に反映しています。特に、reflect.callのような特殊な呼び出しパスでは、m->morebuf.spが実際のスタックの限界よりも高い位置を指すことがあり、スタックオーバーフローの検出が遅れる可能性がありました。
    • gp->sched.spを使用することで、スタックの限界により近い位置でスタックオーバーフローを検出し、morestackルーチンを呼び出すことができるようになります。これにより、スタックオーバーフローの検出がより正確になり、プログラムの安定性が向上します。

このCコードの変更は、アセンブリコードの変更と連携して機能します。アセンブリコードでreflect.call中にgp->sched.spが正確に更新されるようになったため、runtime·newstackがその正確な値を利用してスタックオーバーフローチェックを行うことができるようになったのです。

関連リンク

参考にした情報源リンク

  • Goのソースコード(src/pkg/runtime/以下のファイル)
  • Goのドキュメント(特にランタイム、リフレクション、並行処理に関するセクション)
  • Goのスタック管理に関する技術記事やブログポスト
  • Goのスケジューラに関する技術記事やブログポスト
  • アセンブリ言語(x86, ARM)の基本知識
  • Goのgobuf構造体に関する情報
  • Goのプリエンプションに関する情報

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

このコミットは、Goランタイムにおけるスタックオーバーフローチェックの精度向上と、reflect.call時のスタックトレースの改善、さらにはプリエンプション(横取り)の可能性を広げるための変更です。具体的には、スタックオーバーフローチェックに使用するスタックポインタの参照元をm->morebuf.spからgp->sched.spに変更し、それに伴いreflect.call内でgp->schedの情報を適切に記録するように修正しています。

コミット

commit f0d73fbc7c24ea9d81f24732896a99778f623f80
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 27 16:51:06 2013 -0400

    runtime: use gp->sched.sp for stack overflow check

    On x86 it is a few words lower on the stack than m->morebuf.sp
    so it is a more precise check. Enabling the check requires recording
    a valid gp->sched in reflect.call too. This is a good thing in general,
    since it will make stack traces during reflect.call work better, and it
    may be useful for preemption too.

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

https://github.com/golang/go/commit/f0d73fbc7c24ea9d81f24732896a99778f623f80

元コミット内容

runtime: use gp->sched.sp for stack overflow check

On x86 it is a few words lower on the stack than m->morebuf.sp
so it is a more precise check. Enabling the check requires recording
a valid gp->sched in reflect.call too. This is a good thing in general,
since it will make stack traces during reflect.call work better, and it
may be useful for preemption too.

変更の背景

この変更の主な背景は、Goランタイムにおけるスタックオーバーフローチェックの精度を向上させることです。従来のm->morebuf.spを使用する方法では、特にx86アーキテクチャにおいて、実際のスタックの限界よりも少し高い位置を参照していたため、スタックオーバーフローを検出するタイミングが遅れる可能性がありました。

gp->sched.spを使用することで、より正確なスタックポインタの位置を把握できるようになり、スタックオーバーフローをより早期に、かつ正確に検出することが可能になります。

また、この変更はreflect.call(リフレクションによる関数呼び出し)の挙動にも影響を与えます。reflect.callは、Goのランタイムが通常の関数呼び出しとは異なる方法でスタックを管理するため、スタックトレースが不完全になるなどの問題がありました。gp->sched情報を適切に記録することで、reflect.call中のスタックトレースがより正確になり、デバッグが容易になります。

さらに、この変更は将来的なプリエンプション(協調的ではないゴルーチンの横取り)の実装にも寄与する可能性があります。正確なスタックポインタ情報を持つことは、ランタイムがゴルーチンの実行を中断し、別のゴルーチンに切り替える際に不可欠な要素となります。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とアセンブリ言語の知識が必要です。

Goランタイム (Go Runtime)

Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、システムコールなど、Goプログラムの実行に必要な低レベルな処理を担っています。

ゴルーチン (Goroutine)

ゴルーチンはGoにおける軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。各ゴルーチンは独自のスタックを持ち、ランタイムによってスケジューリングされます。

スタック (Stack)

スタックは、関数呼び出しの際にローカル変数、引数、戻りアドレスなどを格納するために使用されるメモリ領域です。関数が呼び出されるたびにスタックフレームが積まれ、関数から戻る際にスタックフレームが解放されます。スタックは通常、メモリ上の高いアドレスから低いアドレスに向かって成長します。

スタックオーバーフロー (Stack Overflow)

スタックオーバーフローは、関数呼び出しが深くなりすぎたり、大きなローカル変数を確保しすぎたりして、スタック領域が割り当てられたメモリ範囲を超えてしまうエラーです。これにより、プログラムがクラッシュしたり、予期せぬ動作を引き起こしたりします。Goランタイムは、スタックオーバーフローを検出してスタックを拡張するメカニズム(morestack)を持っています。

reflect.call (リフレクションによる関数呼び出し)

Goのリフレクション機能は、実行時に型情報や値情報を操作することを可能にします。reflect.Call関数は、reflect.Valueとして表現された関数を動的に呼び出すために使用されます。通常の関数呼び出しとは異なり、reflect.callはランタイムが特別な処理を行うため、スタックの管理方法が異なります。

m->morebuf.spgp->sched.sp

Goランタイムは、M(Machine)、P(Processor)、G(Goroutine)という3つの主要な抽象化を用いてゴルーチンをスケジューリングします。

  • M (Machine): OSのスレッドに対応します。
  • P (Processor): Mがゴルーチンを実行するための論理的なプロセッサです。
  • G (Goroutine): ゴルーチンそのものです。

これらの構造体には、スタックポインタやレジスタの状態を保存するためのフィールドが含まれています。

  • m->morebuf.sp: M(OSスレッド)に関連付けられたmorebuf構造体内のスタックポインタです。これは、スタック拡張(morestack)の際に使用される一時的なバッファに関連するスタックポインタを指すことがあります。
  • gp->sched.sp: G(ゴルーチン)に関連付けられたsched構造体内のスタックポインタです。これは、ゴルーチンのスケジューリング情報の一部として、そのゴルーチンの現在のスタックポインタを保持します。gobuf構造体の一部として定義されており、ゴルーチンの実行状態を保存・復元するために使われます。

このコミットのポイントは、m->morebuf.spが必ずしもゴルーチンの正確なスタックポインタを指しているわけではないのに対し、gp->sched.spはゴルーチン自身のスケジューリングコンテキストの一部として、より正確なスタックポインタ情報を提供することです。

gobuf 構造体

gobufは、ゴルーチンの実行コンテキスト(プログラムカウンタ、スタックポインタ、フレームポインタなど)を保存するための構造体です。ゴルーチンの切り替えやスタックの拡張など、コンテキストスイッチが必要な場面で利用されます。

プリエンプション (Preemption)

プリエンプションとは、実行中のゴルーチンをランタイムが強制的に中断し、別のゴルーチンにCPUを明け渡させるメカニズムです。Goの初期バージョンでは協調的プリエンプション(ゴルーチンが自発的に実行を中断する)が主でしたが、より公平なスケジューリングやレイテンシの改善のために、非協調的(強制的な)プリエンプションが導入されていきました。正確なスタックポインタ情報は、プリエンプションの実現に不可欠です。

アセンブリ言語 (Assembly Language)

Goランタイムの一部、特にコンテキストスイッチやスタック操作などの低レベルな処理は、アセンブリ言語で記述されています。このコミットでは、x86 (386, amd64) および ARM アーキテクチャ向けのアセンブリコードが変更されています。

技術的詳細

このコミットの核心は、Goランタイムがスタックオーバーフローを検出する際の基準となるスタックポインタの参照を、より正確なものに切り替える点にあります。

  1. スタックオーバーフローチェックの精度向上:

    • 従来のGoランタイムでは、スタックオーバーフローのチェックにm->morebuf.spが使用されていました。しかし、このポインタはOSスレッド(M)のコンテキストに関連しており、特にreflect.callのような特殊な呼び出しパスでは、ゴルーチン(G)の実際のスタックポインタとわずかにずれることがありました。コミットメッセージにあるように、x86アーキテクチャでは「数ワード低い」位置が実際のスタックの限界に近いとされています。
    • このコミットでは、スタックオーバーフローチェックの基準をgp->sched.spに変更します。gp->sched.spは、現在実行中のゴルーチン(gp)のスケジューリングコンテキスト(sched)に保存されているスタックポインタです。これは、ゴルーチン自身の状態を正確に反映するため、より精密なスタックオーバーフロー検出が可能になります。これにより、スタックオーバーフローが実際に発生する直前でmorestackルーチンが呼び出され、スタックが拡張されるようになります。
  2. reflect.callにおけるgp->schedの記録:

    • reflect.callは、Goのリフレクション機能を使って関数を動的に呼び出すためのメカニズムです。この呼び出しパスは、通常のGo関数呼び出しとは異なり、ランタイムがスタックを特別に管理します。
    • このコミット以前は、reflect.callが実行されている間、gp->sched(ゴルーチンのスケジューリング情報)が常に有効な状態を保っているわけではありませんでした。これは、reflect.call中にスタックトレースを取得しようとすると、不完全な情報しか得られない原因となっていました。
    • 今回の変更では、reflect.callのアセンブリコード内で、現在のプログラムカウンタ(reflect.callのアドレス)と現在のスタックポインタ(SPレジスタの値)を、それぞれgp->sched.pcgp->sched.spに明示的に保存するようにしました。これにより、reflect.callが実行されている間も、ゴルーチンのスケジューリング情報が常に最新かつ正確な状態に保たれます。
  3. スタックトレースの改善:

    • gp->sched情報がreflect.call中も正確に記録されるようになったことで、reflect.callを介して呼び出された関数からのスタックトレースがより完全で意味のあるものになります。デバッグ時やエラー発生時の原因究明に役立ちます。
  4. プリエンプションへの寄与:

    • Goランタイムがゴルーチンをプリエンプト(横取り)するためには、そのゴルーチンの現在の実行状態(特にスタックポインタとプログラムカウンタ)を正確に保存し、後で復元できる必要があります。
    • gp->sched.spが常に正確なスタックポインタを指すようにすることで、将来的にGoランタイムがより洗練されたプリエンプションメカニズムを実装する際の基盤となります。これにより、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げることなく、より公平なスケジューリングが可能になります。

この変更は、Goランタイムの堅牢性とデバッグ可能性を向上させるとともに、将来的なパフォーマンス最適化やスケジューリング改善のための重要なステップと言えます。

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

このコミットでは、主に以下の4つのファイルが変更されています。

  1. src/pkg/runtime/asm_386.s (x86 32-bit アセンブリ)
  2. src/pkg/runtime/asm_amd64.s (x86 64-bit アセンブリ)
  3. src/pkg/runtime/asm_arm.s (ARM 32-bit アセンブリ)
  4. src/pkg/runtime/stack.c (C言語)

src/pkg/runtime/asm_386.s および src/pkg/runtime/asm_amd64.s の変更

これらのファイルでは、TEXT reflect·call(SB)セクションに以下の行が追加されています。

--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -254,6 +254,11 @@ TEXT reflect·call(SB), 7, $0
  	MOVL	g(CX), AX
  	MOVL	AX, (m_morebuf+gobuf_g)(BX)
 
+	// Save our own state as the PC and SP to restore
+	// if this goroutine needs to be restarted.
+	MOVL	$reflect·call(SB), (g_sched+gobuf_pc)(AX)
+	MOVL	SP, (g_sched+gobuf_sp)(AX)
+
  	// Set up morestack arguments to call f on a new stack.
  	// We set f's frame size to 1, as a hint to newstack
  	// that this is a call from reflect·call.

asm_amd64.sも同様の変更です(レジスタ名がMOVLからMOVQに変わるなど、64-bitアーキテクチャに合わせた違いはあります)。

  • MOVL $reflect·call(SB), (g_sched+gobuf_pc)(AX):
    • $reflect·call(SB)は、reflect.call関数の開始アドレス(プログラムカウンタ)を指します。
    • AXレジスタには現在のゴルーチン(g)のアドレスが格納されています。
    • (g_sched+gobuf_pc)(AX)は、ゴルーチン構造体g内のschedフィールド(gobuf型)のpc(プログラムカウンタ)フィールドへのオフセットアドレスを示します。
    • この命令は、reflect.callの開始アドレスを現在のゴルーチンのgobufpcフィールドに保存します。
  • MOVL SP, (g_sched+gobuf_sp)(AX):
    • SPは現在のスタックポインタレジスタです。
    • (g_sched+gobuf_sp)(AX)は、ゴルーチン構造体g内のschedフィールドのsp(スタックポインタ)フィールドへのオフセットアドレスを示します。
    • この命令は、現在のスタックポインタの値をゴルーチンのgobufspフィールドに保存します。

src/pkg/runtime/asm_arm.s の変更

ARMアーキテクチャ向けのアセンブリファイルでも同様に、TEXT reflect·call(SB)セクションに以下の行が追加されています。

--- a/src/pkg/runtime/asm_arm.s
+++ b/src/pkg/runtime/asm_arm.s
@@ -207,6 +207,13 @@ TEXT reflect·call(SB), 7, $-4
  	MOVW	SP, (m_morebuf+gobuf_sp)(m)	// our caller's SP
  	MOVW	g,  (m_morebuf+gobuf_g)(m)
 
+	// Save our own state as the PC and SP to restore
+	// if this goroutine needs to be restarted.
+	MOVW	$reflect·call(SB), R11
+	MOVW	R11, (g_sched+gobuf_pc)(g)
+	MOVW	LR, (g_sched+gobuf_lr)(g)
+	MOVW	SP, (g_sched+gobuf_sp)(g)
+
  	// Set up morestack arguments to call f on a new stack.
  	// We set f's frame size to 1, as a hint to newstack
  	// that this is a call from reflect·call.

ARMでは、LR(リンクレジスタ)もg_sched+gobuf_lrに保存されています。これはARMの関数呼び出し規約に関連しており、戻りアドレスがLRに格納されるためです。

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

C言語で記述されたruntime·newstack関数内で、スタックポインタの参照元が変更されています。

--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -215,7 +215,7 @@ runtime·newstack(void)\n 	if(!reflectcall)\n 	\truntime·rewindmorestack(&gp->sched);\n 
-\tsp = m->morebuf.sp;\n+\tsp = gp->sched.sp;\n 	if(thechar == '6' || thechar == '8') {
 	// The call to morestack cost a word.
 	// sp -= sizeof(uintptr);
  • runtime·newstackは、スタックオーバーフローが発生した際に新しいスタックを割り当てるためのランタイム関数です。
  • 変更前は、sp = m->morebuf.sp;として、M(OSスレッド)のmorebuf内のスタックポインタをspに代入していました。
  • 変更後は、sp = gp->sched.sp;として、現在のゴルーチン(gp)のsched内のスタックポインタをspに代入しています。

コアとなるコードの解説

アセンブリコードの変更 (asm_386.s, asm_amd64.s, asm_arm.s)

reflect.callは、Goのリフレクション機能を使って関数を動的に呼び出すための特殊なパスです。このパスでは、通常のGo関数呼び出しとは異なるスタック管理が行われるため、ランタイムがゴルーチンの状態を正確に把握することが難しい場合がありました。

追加されたアセンブリ命令は、reflect.callが実行される直前に、現在のゴルーチン(g)のschedフィールド(gobuf構造体)に、reflect.callの開始アドレス(pc)と現在のスタックポインタ(sp)を明示的に保存しています。

  • g_sched+gobuf_pcへのreflect.callアドレスの保存:
    • これは、ゴルーチンがreflect.callのどの位置で実行を中断しても、そのゴルーチンがreflect.callのコンテキストで実行されていたことを正確に識別できるようにするためです。スタックトレースを生成する際に、この情報が利用され、より完全なトレースが可能になります。
  • g_sched+gobuf_spへの現在のスタックポインタの保存:
    • これは、reflect.callが実行されている間のゴルーチンの正確なスタックポインタを記録するためです。これにより、スタックオーバーフローチェックがより正確に行われるようになります。また、将来的にプリエンプションが導入された場合、ゴルーチンを中断して後で再開する際に、この正確なスタックポインタ情報が必要になります。

これらの変更により、reflect.callがGoランタイムの他の部分(特にスタック管理とスケジューリング)とより密接に連携できるようになり、リフレクションを使用する際の堅牢性とデバッグ可能性が向上します。

Cコードの変更 (stack.c)

runtime·newstack関数は、Goランタイムがスタックオーバーフローを検出した際に呼び出され、ゴルーチンのスタックを拡張する役割を担っています。

  • sp = m->morebuf.sp; から sp = gp->sched.sp; への変更:
    • この変更は、スタックオーバーフローチェックの基準を、OSスレッド(M)に関連する一時的なスタックポインタ(m->morebuf.sp)から、現在実行中のゴルーチン(gp)自身のスケジューリングコンテキストに保存されているスタックポインタ(gp->sched.sp)に切り替えることを意味します。
    • gp->sched.spは、ゴルーチンの実際のスタックポインタをより正確に反映しています。特に、reflect.callのような特殊な呼び出しパスでは、m->morebuf.spが実際のスタックの限界よりも高い位置を指すことがあり、スタックオーバーフローの検出が遅れる可能性がありました。
    • gp->sched.spを使用することで、スタックの限界により近い位置でスタックオーバーフローを検出し、morestackルーチンを呼び出すことができるようになります。これにより、スタックオーバーフローの検出がより正確になり、プログラムの安定性が向上します。

このCコードの変更は、アセンブリコードの変更と連携して機能します。アセンブリコードでreflect.call中にgp->sched.spが正確に更新されるようになったため、runtime·newstackがその正確な値を利用してスタックオーバーフローチェックを行うことができるようになったのです。

関連リンク

参考にした情報源リンク

  • Goのソースコード(src/pkg/runtime/以下のファイル)
  • Goのドキュメント(特にランタイム、リフレクション、並行処理に関するセクション)
  • Goのスタック管理に関する技術記事やブログポスト
  • Goのスケジューラに関する技術記事やブログポスト
  • アセンブリ言語(x86, ARM)の基本知識
  • Goのgobuf構造体に関する情報
  • Goのプリエンプションに関する情報