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

[インデックス 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.cpass.cなど)内に直接記述されており、コードの重複が多く、複雑な構造になっていました。

コミットメッセージにある「This will make an upcoming change to fix recover easier to digest.」という記述は、このリファクタリングの直接的な動機を示しています。recoverはGoのパニックからの回復メカニズムであり、スタックの状態と密接に関連しています。スタック管理コードの変更は非常にデリケートであり、そのロジックが複雑に絡み合っていると、新しい機能の追加やバグ修正が困難になります。スタック分割コードを独立した関数に切り出すことで、recoverの修正に関連する将来の変更が、より局所的で理解しやすいものになるという意図がありました。

前提知識の解説

Goのスタック管理とスタック分割 (Stack Splitting)

Goのgoroutineは、非常に軽量なスレッドであり、そのスタックは必要に応じて動的に伸縮します。これは、プログラムの実行中にスタックが不足しそうになったときに、より大きなスタックを割り当てて切り替えることで実現されます。このプロセスを「スタック分割」と呼びます。

  1. スタックガード (Stack Guard): 各goroutineのスタックには「スタックガード」と呼ばれる境界値が設定されています。これは、現在のスタックポインタがこの値を超えそうになったときに、スタックオーバーフローを防ぐためのトリガーとなります。
  2. runtime.morestack: 関数が呼び出される際、プロローグ(関数の冒頭部分)でスタックガードとの比較が行われます。もしスタックが不足していると判断された場合、runtime.morestackという特別なランタイム関数が呼び出されます。この関数は、新しい(通常はより大きな)スタックフレームを割り当て、現在のスタックの内容を新しいスタックにコピーし、実行を新しいスタックに切り替えます。
  3. リンカの役割: 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.cnoops関数)内に直接記述されていたスタック分割チェックのロジックを、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関数がgCXレジスタにロードされていることを前提とできるようにするためです。

各ファイルの変更点

  • src/cmd/5l/noop.c:

    • noops関数からスタック分割ロジックが削除され、stacksplit(p, autosize)の呼び出しに置き換えられました。
    • stacksplit静的関数が新しく追加され、元のnoops関数内にあったスタックチェックのアセンブリ命令生成ロジックが移動されました。
    • symmorestackpmorestackがグローバル変数として宣言されました。
  • src/cmd/6l/pass.c:

    • dostkoff関数からスタック分割ロジックが削除され、load_g_cx(p)stacksplit(p, autoffset, &q)の呼び出しに置き換えられました。
    • load_g_cx静的関数が新しく追加され、gCXレジスタにロードするロジックが移動されました。
    • stacksplit静的関数が新しく追加され、元のdostkoff関数内にあったスタックチェックのアセンブリ命令生成ロジックが移動されました。
    • gmsymがグローバル変数として宣言されました。
  • src/cmd/8l/pass.c:

    • dostkoff関数からスタック分割ロジックが削除され、load_g_cx(p)stacksplit(p, autoffset, &q)の呼び出しに置き換えられました。
    • load_g_cx静的関数が新しく追加され、gCXレジスタにロードするロジックが移動されました。
    • stacksplit静的関数が新しく追加され、元のdostkoff関数内にあったスタックチェックのアセンブリ命令生成ロジックが移動されました。
    • plan9_tos, pmorestack, symmorestackがグローバル変数として宣言されました。

このリファクタリングにより、各リンカのコードベースにおけるスタック分割ロジックの重複が大幅に削減され、コードの構造がよりモジュール化されました。

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

このコミットのコアとなる変更は、各リンカの主要な処理関数(5lnoops6l8ldostkoff)から、スタック分割チェックのアセンブリ命令生成ロジックが削除され、それが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のリンカが関数プロローグに挿入するスタックチェックのロジックを、より構造化された形に整理したものです。

  1. if(!(p->reg & NOSPLIT)):

    • これは、現在の関数がスタック分割を無効にする//go:nosplitディレクティブでマークされていないかを確認する条件です。NOSPLITが設定されている関数は、スタックチェックをスキップします。
  2. p = stacksplit(p, autosize);:

    • この行がリファクタリングの核心です。以前はここに直接記述されていた大量のアセンブリ命令生成ロジックが、stacksplit関数への単一の呼び出しに置き換えられました。
    • pは現在の命令リストの末尾を指すポインタで、stacksplit関数はこのポインタから命令を追加していきます。
    • autosize(またはframesize)は、現在の関数のスタックフレームに必要なバイト数を示します。
  3. stacksplit関数の内部:

    • MOVW g_stackguard(g), R1: 最初に、現在のgoroutine (g) のスタックガード値を取得し、レジスタR1(ARMの場合)にロードします。g_stackguard(g)は、g構造体内のスタックガードフィールドへのオフセットを示します。
    • スタックサイズに応じた比較ロジック:
      • framesize <= StackSmall: 必要なスタックサイズが小さい場合。現在のスタックポインタ(SP)とスタックガードを直接比較します。SP < stackguardであればスタック不足です。
      • framesize <= StackBig: 必要なスタックサイズが中程度の場合。SP - framesizestackguard - 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言語のソースコード (特にsrc/cmd/5l/noop.c, src/cmd/6l/pass.c, src/cmd/8l/pass.cのコミット履歴)
  • Go言語のリンカの内部構造に関する一般的な知識
  • アセンブリ言語(ARM, AMD64, 386)の基本的な命令セット
  • Goのスタック分割に関する技術記事や解説(当時の情報に基づく)
  • Goのrecoverメカニズムに関する情報