[インデックス 17574] ファイルの概要
このコミットは、Go言語のリンカ(cmd/5l
, cmd/6l
, cmd/8l
)におけるスタック分割(stack split)コードのリファクタリングを目的としています。具体的には、各アーキテクチャ(ARM, AMD64, 386)のリンカに散在していたスタック分割チェックのコードを、stacksplit
という独立した関数に切り出すことで、コードの重複を排除し、可読性と保守性を向上させています。
コミット
commit 1a6576db3468566ec89671c2c191e0b975833a7f
Author: Russ Cox <rsc@golang.org>
Date: Wed Sep 11 20:29:45 2013 -0400
cmd/5l, cmd/6l, cmd/8l: refactor stack split code
Pull the stack split generation into its own function.
This will make an upcoming change to fix recover
easier to digest.
R=ken2
CC=golang-dev
https://golang.org/cl/13611044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1a6576db3468566ec89671c2c191e0b975833a7f
元コミット内容
cmd/5l, cmd/6l, cmd/8l: refactor stack split code
スタック分割の生成を独自の関数に引き出す。
これにより、recover
を修正するための今後の変更が理解しやすくなる。
変更の背景
Go言語のランタイムは、goroutineごとに動的にスタックサイズを調整する「スタック分割(stack splitting)」というメカニズムを採用しています。これは、固定サイズのスタックを持つ他の多くの言語とは異なり、小さなgoroutineが大量に存在する場合でもメモリ効率を高く保つための重要な機能です。
関数が呼び出される際、Goランタイムは現在のスタックが関数実行に必要なサイズを持っているかを確認します。もし不足している場合、runtime.morestack
という特別な関数が呼び出され、新しい(より大きな)スタックフレームが割り当てられ、既存のスタックの内容がコピーされます。このスタックチェックとmorestack
の呼び出しを生成するコードは、Goのリンカ(cmd/5l
for ARM, cmd/6l
for AMD64, cmd/8l
for 386)内に組み込まれていました。
このコミット以前は、スタックチェックとmorestack
呼び出しを生成するロジックが、各アーキテクチャのリンカのコード(noop.c
やpass.c
など)内に直接記述されており、コードの重複が多く、複雑な構造になっていました。
コミットメッセージにある「This will make an upcoming change to fix recover easier to digest.」という記述は、このリファクタリングの直接的な動機を示しています。recover
はGoのパニックからの回復メカニズムであり、スタックの状態と密接に関連しています。スタック管理コードの変更は非常にデリケートであり、そのロジックが複雑に絡み合っていると、新しい機能の追加やバグ修正が困難になります。スタック分割コードを独立した関数に切り出すことで、recover
の修正に関連する将来の変更が、より局所的で理解しやすいものになるという意図がありました。
前提知識の解説
Goのスタック管理とスタック分割 (Stack Splitting)
Goのgoroutineは、非常に軽量なスレッドであり、そのスタックは必要に応じて動的に伸縮します。これは、プログラムの実行中にスタックが不足しそうになったときに、より大きなスタックを割り当てて切り替えることで実現されます。このプロセスを「スタック分割」と呼びます。
- スタックガード (Stack Guard): 各goroutineのスタックには「スタックガード」と呼ばれる境界値が設定されています。これは、現在のスタックポインタがこの値を超えそうになったときに、スタックオーバーフローを防ぐためのトリガーとなります。
runtime.morestack
: 関数が呼び出される際、プロローグ(関数の冒頭部分)でスタックガードとの比較が行われます。もしスタックが不足していると判断された場合、runtime.morestack
という特別なランタイム関数が呼び出されます。この関数は、新しい(通常はより大きな)スタックフレームを割り当て、現在のスタックの内容を新しいスタックにコピーし、実行を新しいスタックに切り替えます。- リンカの役割: Goのコンパイラは、各関数のプロローグにスタックチェックのコードを挿入します。このコードは、リンカによって最終的な実行可能バイナリに組み込まれます。リンカは、アーキテクチャ固有の命令セット(ARM, AMD64, 386など)に合わせて、スタックチェックと
morestack
呼び出しの具体的なアセンブリ命令を生成します。
Goのリンカ (cmd/5l
, cmd/6l
, cmd/8l
)
Goのツールチェインには、各ターゲットアーキテクチャに対応するリンカが含まれています。
cmd/5l
: ARMアーキテクチャ (Plan 9 from User Spaceの命名規則でARMは5番目のCPUとされていたため)cmd/6l
: AMD64アーキテクチャ (同上、AMD64は6番目)cmd/8l
: 386アーキテクチャ (同上、386は8番目)
これらのリンカは、コンパイラが生成したオブジェクトファイルを受け取り、それらを結合して実行可能バイナリを生成します。この過程で、ランタイムのサポートコード(スタックチェックなど)が挿入されます。
Prog
構造体
Goのリンカ内部では、アセンブリ命令を抽象化したProg
という構造体が使われています。これは、命令の種類(as
フィールド)、オペランド(from
, to
フィールド)、条件コード(scond
フィールド)、リンク情報(link
, pcond
フィールド)などを表現します。リンカはこれらのProg
構造体のリストを操作して、最終的な機械語コードを生成します。
appendp
関数
appendp
は、リンカの内部でProg
構造体のリストに新しい命令を追加するために使われるヘルパー関数です。
技術的詳細
このコミットの主要な変更は、各リンカ(5l
, 6l
, 8l
)のdostkoff
関数(またはnoop.c
のnoops
関数)内に直接記述されていたスタック分割チェックのロジックを、stacksplit
という新しい静的関数に抽出したことです。
変更前は、dostkoff
関数内で、関数のスタックフレームサイズ(autoffset
またはautosize
)に基づいて、3種類のスタックチェックロジック(StackSmall
, StackBig
, それ以上の大きなスタック)が条件分岐によって直接生成されていました。これらのロジックは、g
(現在のgoroutine構造体)のスタックガード値を取得し、現在のスタックポインタと比較し、必要に応じてruntime.morestack
を呼び出す一連のアセンブリ命令を生成するものでした。
変更後は、これらの複雑なアセンブリ命令の生成ロジックがstacksplit
関数内にカプセル化されました。dostkoff
関数からは、単にstacksplit
関数を呼び出すだけでよくなりました。
stacksplit
関数の引数と戻り値
Prog *p
: 現在の命令リストの末尾のProg
ポインタ。stacksplit
はこのポインタに命令を追加していきます。int32 framesize
: 現在の関数のスタックフレームサイズ。Prog **jmpok
(6l, 8lのみ): スタックチェックが成功し、スタック分割が不要な場合にジャンプすべき命令のポインタを格納するためのポインタ。
stacksplit
関数は、追加された最後の命令のProg
ポインタを返します。
load_g_cx
関数の導入 (6l, 8lのみ)
6l
(AMD64) と 8l
(386) では、g
(現在のgoroutine構造体へのポインタ)をCX
レジスタにロードする処理もload_g_cx
という別の関数に切り出されました。これは、スタックチェックの前にg
ポインタが必要となるため、stacksplit
関数がg
がCX
レジスタにロードされていることを前提とできるようにするためです。
各ファイルの変更点
-
src/cmd/5l/noop.c
:noops
関数からスタック分割ロジックが削除され、stacksplit(p, autosize)
の呼び出しに置き換えられました。stacksplit
静的関数が新しく追加され、元のnoops
関数内にあったスタックチェックのアセンブリ命令生成ロジックが移動されました。symmorestack
とpmorestack
がグローバル変数として宣言されました。
-
src/cmd/6l/pass.c
:dostkoff
関数からスタック分割ロジックが削除され、load_g_cx(p)
とstacksplit(p, autoffset, &q)
の呼び出しに置き換えられました。load_g_cx
静的関数が新しく追加され、g
をCX
レジスタにロードするロジックが移動されました。stacksplit
静的関数が新しく追加され、元のdostkoff
関数内にあったスタックチェックのアセンブリ命令生成ロジックが移動されました。gmsym
がグローバル変数として宣言されました。
-
src/cmd/8l/pass.c
:dostkoff
関数からスタック分割ロジックが削除され、load_g_cx(p)
とstacksplit(p, autoffset, &q)
の呼び出しに置き換えられました。load_g_cx
静的関数が新しく追加され、g
をCX
レジスタにロードするロジックが移動されました。stacksplit
静的関数が新しく追加され、元のdostkoff
関数内にあったスタックチェックのアセンブリ命令生成ロジックが移動されました。plan9_tos
,pmorestack
,symmorestack
がグローバル変数として宣言されました。
このリファクタリングにより、各リンカのコードベースにおけるスタック分割ロジックの重複が大幅に削減され、コードの構造がよりモジュール化されました。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、各リンカの主要な処理関数(5l
のnoops
、6l
と8l
のdostkoff
)から、スタック分割チェックのアセンブリ命令生成ロジックが削除され、それがstacksplit
という新しい関数に移動された点です。
src/cmd/5l/noop.c
の変更例
変更前 (noops
関数内の一部):
if(!(p->reg & NOSPLIT)) {
// MOVW g_stackguard(g), R1
p = appendp(p);
p->as = AMOVW;
p->from.type = D_OREG;
p->from.reg = REGG;
p->to.type = D_REG;
p->to.reg = 1;
if(autosize <= StackSmall) {
// small stack: SP < stackguard
// CMP stackguard, SP
p = appendp(p);
p->as = ACMP;
p->from.type = D_REG;
p->from.reg = 1;
p->reg = REGSP;
} else if(autosize <= StackBig) {
// large stack: SP-framesize < stackguard-StackSmall
// MOVW $-autosize(SP), R2
// CMP stackguard, R2
p = appendp(p);
p->as = AMOVW;
p->from.type = D_CONST;
p->from.reg = REGSP;
p->from.offset = -autosize;
p->to.type = D_REG;
p->to.reg = 2;
p = appendp(p);
p->as = ACMP;
p->from.type = D_REG;
p->from.reg = 1;
p->reg = 2;
} else {
// Such a large stack we need to protect against wraparound
// if SP is close to zero.
// SP-stackguard+StackGuard < framesize + (StackGuard-StackSmall)
// The +StackGuard on both sides is required to keep the left side positive:
// SP is allowed to be slightly below stackguard. See stack.h.
// CMP $StackPreempt, R1
// MOVW.NE $StackGuard(SP), R2
// SUB.NE R1, R2
// MOVW.NE $(autosize+(StackGuard-StackSmall)), R3
// CMP.NE R3, R2
p = appendp(p);
p->as = ACMP;
p->from.type = D_CONST;
p->from.offset = (uint32)StackPreempt;
p->reg = 1;
p = appendp(p);
p->as = AMOVW;
p->from.type = D_CONST;
p->from.reg = REGSP;
p->from.offset = StackGuard;
p->to.type = D_REG;
p->to.reg = 2;
p->scond = C_SCOND_NE;
p = appendp(p);
p->as = ASUB;
p->from.type = D_REG;
p->from.reg = 1;
p->to.type = D_REG;
p->to.reg = 2;
p->scond = C_SCOND_NE;
p = appendp(p);
p->as = AMOVW;
p->from.type = D_CONST;
p->from.offset = autosize + (StackGuard - StackSmall);
p->to.type = D_REG;
p->to.reg = 3;
p->scond = C_SCOND_NE;
p = appendp(p);
p->as = ACMP;
p->from.type = D_REG;
p->from.reg = 3;
p->reg = 2;
p->scond = C_SCOND_NE;
}
// MOVW.LS $autosize, R1
p = appendp(p);
p->as = AMOVW;
p->scond = C_SCOND_LS;
p->from.type = D_CONST;
p->from.offset = autosize;
p->to.type = D_REG;
p->to.reg = 1;
// MOVW.LS $args, R2
p = appendp(p);
p->as = AMOVW;
p->scond = C_SCOND_LS;
p->from.type = D_CONST;
arg = cursym->text->to.offset2;
if(arg == 1) // special marker for known 0
arg = 0;
if(arg&3)
diag("misaligned argument size in stack split");
p->from.offset = arg;
p->to.type = D_REG;
p->to.reg = 2;
// MOVW.LS R14, R3
p = appendp(p);
p->as = AMOVW;
p->scond = C_SCOND_LS;
p->from.type = D_REG;
p->from.reg = REGLINK;
p->to.type = D_REG;
p->to.reg = 3;
// BL.LS runtime.morestack(SB) // modifies LR, returns with LO still asserted
p = appendp(p);
p->as = ABL;
p->scond = C_SCOND_LS;
p->to.type = D_BRANCH;
p->to.sym = symmorestack;
p->cond = pmorestack;
// BLS start
p = appendp(p);
p->as = ABLS;
p->to.type = D_BRANCH;
p->cond = cursym->text->link;
}
変更後 (noops
関数内の一部):
if(!(p->reg & NOSPLIT))
p = stacksplit(p, autosize); // emit split check
新しく追加された stacksplit
関数 (src/cmd/5l/noop.c):
static Prog*
stacksplit(Prog *p, int32 framesize)
{
int32 arg;
// MOVW g_stackguard(g), R1
p = appendp(p);
p->as = AMOVW;
p->from.type = D_OREG;
p->from.reg = REGG;
p->to.type = D_REG;
p->to.reg = 1;
if(framesize <= StackSmall) {
// small stack: SP < stackguard
// CMP stackguard, SP
p = appendp(p);
p->as = ACMP;
p->from.type = D_REG;
p->from.reg = 1;
p->reg = REGSP;
} else if(framesize <= StackBig) {
// large stack: SP-framesize < stackguard-StackSmall
// MOVW $-framesize(SP), R2
// CMP stackguard, R2
p = appendp(p);
p->as = AMOVW;
p->from.type = D_CONST;
p->from.reg = REGSP;
p->from.offset = -framesize;
p->to.type = D_REG;
p->to.reg = 2;
p = appendp(p);
p->as = ACMP;
p->from.type = D_REG;
p->from.reg = 1;
p->reg = 2;
} else {
// Such a large stack we need to protect against wraparound
// if SP is close to zero.
// SP-stackguard+StackGuard < framesize + (StackGuard-StackSmall)
// The +StackGuard on both sides is required to keep the left side positive:
// SP is allowed to be slightly below stackguard. See stack.h.
// CMP $StackPreempt, R1
// MOVW.NE $StackGuard(SP), R2
// SUB.NE R1, R2
// MOVW.NE $(framesize+(StackGuard-StackSmall)), R3
// CMP.NE R3, R2
p = appendp(p);
p->as = ACMP;
p->from.type = D_CONST;
p->from.offset = (uint32)StackPreempt;
p->reg = 1;
p = appendp(p);
p->as = AMOVW;
p->from.type = D_CONST;
p->from.reg = REGSP;
p->from.offset = StackGuard;
p->to.type = D_REG;
p->to.reg = 2;
p->scond = C_SCOND_NE;
p = appendp(p);
p->as = ASUB;
p->from.type = D_REG;
p->from.reg = 1;
p->to.type = D_REG;
p->to.reg = 2;
p->scond = C_SCOND_NE;
p = appendp(p);
p->as = AMOVW;
p->from.type = D_CONST;
p->from.offset = framesize + (StackGuard - StackSmall);
p->to.type = D_REG;
p->to.reg = 3;
p->scond = C_SCOND_NE;
p = appendp(p);
p->as = ACMP;
p->from.type = D_REG;
p->from.reg = 3;
p->reg = 2;
p->scond = C_SCOND_NE;
}
// MOVW.LS $framesize, R1
p = appendp(p);
p->as = AMOVW;
p->scond = C_SCOND_LS;
p->from.type = D_CONST;
p->from.offset = framesize;
p->to.type = D_REG;
p->to.reg = 1;
// MOVW.LS $args, R2
p = appendp(p);
p->as = AMOVW;
p->scond = C_SCOND_LS;
p->from.type = D_CONST;
arg = cursym->text->to.offset2;
if(arg == 1) // special marker for known 0
arg = 0;
if(arg&3)
diag("misaligned argument size in stack split");
p->from.offset = arg;
p->to.type = D_REG;
p->to.reg = 2;
// MOVW.LS R14, R3
p = appendp(p);
p->as = AMOVW;
p->scond = C_SCOND_LS;
p->from.type = D_REG;
p->from.reg = REGLINK;
p->to.type = D_REG;
p->to.reg = 3;
// BL.LS runtime.morestack(SB) // modifies LR, returns with LO still asserted
p = appendp(p);
p->as = ABL;
p->scond = C_SCOND_LS;
p->to.type = D_BRANCH;
p->to.sym = symmorestack;
p->cond = pmorestack;
// BLS start
p = appendp(p);
p->as = ABLS;
p->to.type = D_BRANCH;
p->cond = cursym->text->link;
return p;
}
コアとなるコードの解説
上記のコード変更は、Goのリンカが関数プロローグに挿入するスタックチェックのロジックを、より構造化された形に整理したものです。
-
if(!(p->reg & NOSPLIT))
:- これは、現在の関数がスタック分割を無効にする
//go:nosplit
ディレクティブでマークされていないかを確認する条件です。NOSPLIT
が設定されている関数は、スタックチェックをスキップします。
- これは、現在の関数がスタック分割を無効にする
-
p = stacksplit(p, autosize);
:- この行がリファクタリングの核心です。以前はここに直接記述されていた大量のアセンブリ命令生成ロジックが、
stacksplit
関数への単一の呼び出しに置き換えられました。 p
は現在の命令リストの末尾を指すポインタで、stacksplit
関数はこのポインタから命令を追加していきます。autosize
(またはframesize
)は、現在の関数のスタックフレームに必要なバイト数を示します。
- この行がリファクタリングの核心です。以前はここに直接記述されていた大量のアセンブリ命令生成ロジックが、
-
stacksplit
関数の内部:MOVW g_stackguard(g), R1
: 最初に、現在のgoroutine (g
) のスタックガード値を取得し、レジスタR1
(ARMの場合)にロードします。g_stackguard(g)
は、g
構造体内のスタックガードフィールドへのオフセットを示します。- スタックサイズに応じた比較ロジック:
framesize <= StackSmall
: 必要なスタックサイズが小さい場合。現在のスタックポインタ(SP
)とスタックガードを直接比較します。SP < stackguard
であればスタック不足です。framesize <= StackBig
: 必要なスタックサイズが中程度の場合。SP - framesize
とstackguard - StackSmall
を比較します。これは、新しいフレームを割り当てた後のSP
がスタックガードを下回るかどうかをチェックします。else
(大きなスタック): 非常に大きなスタックが必要な場合。スタックポインタがゼロに近い場合のラップアラウンド(桁あふれ)を防ぐためのより複雑なチェックを行います。これには、プリエンプション(横取り)によってスタックガードが非常に大きな値(StackPreempt
)に設定される可能性も考慮されます。
BL.LS runtime.morestack(SB)
: 上記の比較の結果、スタックが不足していると判断された場合(条件コードLS
- Less than or Same)、runtime.morestack
関数が呼び出されます。この関数は、新しいスタックの割り当てと切り替えを行います。BLS start
:runtime.morestack
から戻った後、元の関数の冒頭(start
ラベル)にジャンプし、関数の実行を再開します。
このリファクタリングにより、スタック分割のロジックがstacksplit
関数内に凝縮され、各リンカのメインパスが簡潔になりました。これにより、将来的にスタック管理に関連する変更(特にrecover
の修正)を行う際に、影響範囲が限定され、コードの理解とデバッグが容易になります。
関連リンク
- Go言語のスタック管理に関する公式ドキュメントやブログ記事(当時の情報源は現在と異なる可能性がありますが、概念は共通です)
- Go's Execution Tracer (スタックトレースの概念に関連)
- Go Slices: usage and internals (メモリ管理の基礎に関連)
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/5l/noop.c
,src/cmd/6l/pass.c
,src/cmd/8l/pass.c
のコミット履歴) - Go言語のリンカの内部構造に関する一般的な知識
- アセンブリ言語(ARM, AMD64, 386)の基本的な命令セット
- Goのスタック分割に関する技術記事や解説(当時の情報に基づく)
- Goの
recover
メカニズムに関する情報