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

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

このコミットは、Go言語の初期のコンパイラ(8g、386アーキテクチャ向け)におけるコード生成機能の拡張に焦点を当てています。特に、関数呼び出しのメカニズム、スタックフレームの管理、および戻り値の処理に関する重要な進展が含まれています。また、将来的な機能(例えば、スレッドの開始や特定の算術演算)のための基盤も準備されています。

コミット

commit 6b07021a2b474bac0c93ddac64b395bc03c20bc9
Author: Russ Cox <rsc@golang.org>
Date:   Thu Apr 2 16:48:06 2009 -0700

    implement some more 8g
    
            package main
            func main() {
                    println("hello,", 123);
            }
    
    R=ken
    OCL=27043
    CL=27043

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

https://github.com/golang/go/commit/6b07021a2b474bac0c93ddac64b395bc03c20bc9

元コミット内容

このコミットは、Goコンパイラ(8g)のさらなる実装を進めるものです。具体的には、println("hello,", 123); のような基本的な関数呼び出しをコンパイルできるようにするための変更が含まれています。

変更の背景

Go言語は2009年11月に一般公開されましたが、このコミットはそれ以前の2009年4月に行われています。この時期は、Goコンパイラとランタイムが活発に開発されていた初期段階にあたります。Goの設計目標の一つに、効率的なコンパイルと実行がありました。そのためには、関数呼び出し、スタック管理、および基本的な演算のコード生成を正確かつ効率的に行うことが不可欠でした。

このコミットの背景には、Goプログラムが実際に実行可能なバイナリを生成できるようにするための、低レベルなコード生成ロジックの構築という目的があります。特に、printlnのような標準ライブラリ関数を呼び出す能力は、言語の基本的な実用性を示す上で重要でした。

前提知識の解説

このコミットの変更内容を理解するためには、以下の知識が役立ちます。

  • Goコンパイラアーキテクチャ(初期):
    • 8g: 386(x86 32-bit)アーキテクチャ向けのGoコンパイラ。Goの初期には、ターゲットアーキテクチャごとに異なるコンパイラ(例: 6g for amd64, 5g for ARM)が存在しました。これらはPlan 9のツールチェインの影響を強く受けていました。
    • 8l: 386アーキテクチャ向けのGoリンカ。コンパイラが生成したオブジェクトファイルを結合して実行可能ファイルを生成します。
    • gc: Goコンパイラの共通部分。型システム、AST(抽象構文木)の処理など、アーキテクチャに依存しない部分を扱います。
  • コンパイラのコード生成:
    • 中間表現 (IR): ソースコードを機械語に変換する途中で使われる抽象的な表現。Goコンパイラでは、ASTからさらに低レベルな中間表現に変換され、最終的にアセンブリコードが生成されます。
    • スタックフレーム: 関数が呼び出される際に、ローカル変数、引数、戻りアドレスなどを格納するためにスタック上に確保されるメモリ領域。
    • 呼び出し規約 (Calling Convention): 関数が呼び出される際に、引数をどのように渡し、戻り値をどのように受け取るか、レジスタをどのように保存・復元するかといった、関数呼び出しに関する取り決め。
  • アセンブリ言語 (x86/386):
    • レジスタ: CPU内部の高速な記憶領域(例: AX, SP (Stack Pointer))。
    • 命令: MOVL (Move Long), CALL (Call function), RET (Return from function), LEAL (Load Effective Address) など。
    • スタック操作: PUSH (スタックにプッシュ), POP (スタックからポップ)。
  • オペレーティングシステムとスレッド:
    • システムコール: アプリケーションがOSの機能(ファイルI/O、メモリ管理、スレッド作成など)を利用するためのインターフェース。
    • スレッド: プロセス内で独立して実行される実行単位。GoのGoroutineは、OSスレッド上で多重化されて実行されます。

技術的詳細

このコミットは、Goコンパイラのコード生成バックエンドにおける複数の重要な側面を改善しています。

  1. 関数呼び出しのコード生成 (cgen_call):

    • 以前は未実装でfatalエラーを発生させていたcgen_call関数が実装されました。この関数は、Goソースコード内の関数呼び出しに対応するアセンブリコードを生成します。
    • 関数ポインタを介した間接呼び出しと、直接的な関数呼び出しの両方に対応しています。
    • 引数の準備と、実際のCALL命令の生成が含まれます。
    • setmaxarg(t)を呼び出すことで、関数呼び出しに必要な最大の引数領域を追跡し、スタックフレームのサイズ計算に反映させます。
  2. 戻り値の処理 (cgen_callret, cgen_aret):

    • cgen_callretは、関数呼び出しの戻り値をスタックから取得し、指定されたノードに格納するコードを生成します。Goの呼び出し規約では、戻り値は通常スタック上に配置されます。
    • cgen_aretは、戻り値そのものではなく、戻り値が格納されているスタック上のアドレスを取得するコードを生成します。これは、戻り値へのポインタが必要な場合(例えば、構造体をポインタで返す場合など)に利用されます。LEAL (Load Effective Address) 命令が使用され、メモリ上のアドレスをレジスタにロードします。
  3. スタックフレームサイズの正確な計算:

    • src/cmd/8g/gen.ccompile関数において、関数の最終的なスタックサイズを計算する方法が変更されました。maxstksizeという新しいグローバル変数が導入され、関数内で発生する可能性のある最大のスタック使用量(例えば、ネストされた関数呼び出しによる一時的なスタック消費)を追跡します。これにより、関数全体のスタックフレームが適切に確保され、スタックオーバーフローのリスクが低減されます。
  4. 一時変数の管理:

    • src/cmd/8g/gg.htempalloctempfreeの宣言が追加されました。これらは、コード生成中に一時的に必要となる変数を割り当てたり解放したりするための関数です。コンパイラが複雑な式を評価する際に、中間結果を保持するための一時的なストレージが必要になります。
  5. 将来の機能拡張のための準備:

    • src/cmd/8g/gen.cには、cgen_div (除算), cgen_shift (シフト演算), cgen_bmul (バイト乗算) のためのプレースホルダー関数が追加されています。これらはまだfatalエラーを発生させる段階ですが、これらの演算のコード生成を実装するための構造が用意されたことを示しています。
    • src/cmd/8l/8.out.hD_F7が追加されたことは、浮動小数点演算のサポートに向けた準備である可能性があります。
    • src/runtime/darwin/386/sys.sbsdthread_startという新しいアセンブリ関数が追加されました。これは、Darwin(macOS)上で新しいスレッドを開始するための低レベルなエントリポイントであり、GoのGoroutine実装の初期段階を示唆しています。GoroutineはOSスレッド上で動作するため、OSスレッドの作成と管理はランタイムの重要な部分です。
  6. ASTノードへのスタックオフセット情報の追加:

    • src/cmd/gc/go.hNode構造体にostk (offset on stack) フィールドが追加されました。これは、ASTノードが表す変数や式がスタック上のどこに配置されるかを示すオフセット情報を保持するために使用されます。これにより、コンパイラはスタック上のデータに正確にアクセスできるようになります。

これらの変更は、Goコンパイラがより複雑なGoプログラムをコンパイルし、実行するための基本的な能力を獲得する上で不可欠なステップでした。

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

src/cmd/8g/gen.c

--- a/src/cmd/8g/gen.c
+++ b/src/cmd/8g/gen.c
@@ -84,7 +84,10 @@ compile(Node *fn)
 	ptxt->to.offset2 = rnd(curfn->type->argwid, maxround);
 
 	// fill in final stack size
-	ptxt->to.offset = rnd(stksize+maxarg, maxround);
+	if(stksize > maxstksize)
+		maxstksize = stksize;
+	ptxt->to.offset = rnd(maxstksize+maxarg, maxround);
+	maxstksize = 0;
 
 	if(debug['f'])
 		frame(0);
@@ -157,7 +160,115 @@ cgen_callinter(Node *n, Node *res, int proc)
 void
 cgen_call(Node *n, int proc)
 {
-	fatal("cgen_call");
+	Type *t;
+	Node nod, afun;
+
+	if(n == N)
+		return;
+
+	if(n->left->ullman >= UINF) {
+		// if name involves a fn call
+		// precompute the address of the fn
+		tempalloc(&afun, types[tptr]);
+		cgen(n->left, &afun);
+	}
+
+	gen(n->right);		// assign the args
+	t = n->left->type;
+
+	setmaxarg(t);
+
+	// call tempname pointer
+	if(n->left->ullman >= UINF) {
+		regalloc(&nod, types[tptr], N);
+		cgen_as(&nod, &afun);
+		tempfree(&afun);
+		nod.type = t;
+		ginscall(&nod, proc);
+		regfree(&nod);
+		return;
+	}
+
+	// call pointer
+	if(n->left->op != ONAME || n->left->class != PFUNC) {
+		regalloc(&nod, types[tptr], N);
+		cgen_as(&nod, n->left);
+		nod.type = t;
+		ginscall(&nod, proc);
+		regfree(&nod);
+		return;
+	}
+
+	// call direct
+	n->left->method = 1;
+	ginscall(n->left, proc);
+}
+
+/*
+ * call to n has already been generated.
+ * generate:
+ *	res = return value from call.
+ */
+void
+cgen_callret(Node *n, Node *res)
+{
+	Node nod;
+	Type *fp, *t;
+	Iter flist;
+
+	t = n->left->type;
+	if(t->etype == TPTR32 || t->etype == TPTR64)
+		t = t->type;
+
+	fp = structfirst(&flist, getoutarg(t));
+	if(fp == T)
+		fatal("cgen_callret: nil");
+
+	memset(&nod, 0, sizeof(nod));
+	nod.op = OINDREG;
+	nod.val.u.reg = D_SP;
+	nod.addable = 1;
+
+	nod.xoffset = fp->width;
+	nod.type = fp->type;
+	cgen_as(res, &nod);
+}
+
+/*
+ * call to n has already been generated.
+ * generate:
+ *	res = &return value from call.
+ */
+void
+cgen_aret(Node *n, Node *res)
+{
+	Node nod1, nod2;
+	Type *fp, *t;
+	Iter flist;
+
+	t = n->left->type;
+	if(isptr[t->etype])
+		t = t->type;
+
+	fp = structfirst(&flist, getoutarg(t));
+	if(fp == T)
+		fatal("cgen_aret: nil");
+
+	memset(&nod1, 0, sizeof(nod1));
+	nod1.op = OINDREG;
+	nod1.val.u.reg = D_SP;
+	nod1.addable = 1;
+
+	nod1.xoffset = fp->width;
+	nod1.type = fp->type;
+
+	if(res->op != OREGISTER) {
+		regalloc(&nod2, types[tptr], res);
+		gins(ALEAL, &nod1, &nod2);
+		gins(AMOVL, &nod2, res);
+		regfree(&nod2);
+	} else
+		gins(ALEAL, &nod1, res);
 }
 
 /*
@@ -182,3 +293,37 @@ cgen_asop(Node *n)
 	fatal("cgen_asop");
 }
 
+/*
+ * generate division according to op, one of:
+ *	res = nl / nr
+ *	res = nl % nr
+ */
+void
+cgen_div(int op, Node *nl, Node *nr, Node *res)
+{
+	fatal("cgen_div");
+}
+
+/*
+ * generate shift according to op, one of:
+ *	res = nl << nr
+ *	res = nl >> nr
+ */
+void
+cgen_shift(int op, Node *nl, Node *nr, Node *res)
+{
+	fatal("cgen_shift");
+}
+
+/*
+ * generate byte multiply:
+ *	res = nl * nr
+ * no byte multiply instruction so have to do
+ * 16-bit multiply and take bottom half.
+ */
+void
+cgen_bmul(int op, Node *nl, Node *nr, Node *res)
+{
+	fatal("cgen_bmul");
+}
+

src/cmd/8g/gg.h

--- a/src/cmd/8g/gg.h
+++ b/src/cmd/8g/gg.h
@@ -57,6 +57,7 @@ EXTERN	Node*	deferproc;
 EXTERN	Node*	deferreturn;
 EXTERN	Node*	throwindex;
 EXTERN	Node*	throwreturn;
+EXTERN	int	maxstksize;
 
 /*
  * gen.c
@@ -93,6 +94,8 @@ Prog*	gins(int, Node*, Node*);
 int	samaddr(Node*, Node*);
 void	naddr(Node*, Addr*);
 void	cgen_aret(Node*, Node*);
+int	cgen64(Node*, Node*);
+int	is64(Type*);
 
 /*
  * gsubr.c
@@ -114,6 +119,8 @@ void	ginit(void);
 void	gclean(void);
 void	regalloc(Node*, Type*, Node*);
 void	regfree(Node*);
+void	tempalloc(Node*, Type*);
+void	tempfree(Node*);
 Node*	nodarg(Type*, int);
 void	nodreg(Node*, Type*, int);
 void	nodindreg(Node*, Type*, int);

src/cmd/8l/8.out.h

--- a/src/cmd/8l/8.out.h
+++ b/src/cmd/8l/8.out.h
@@ -413,6 +413,7 @@ enum
 	D_DI,
 
 	D_F0		= 16,
+	D_F7		= D_F0 + 7,
 
 	D_CS		= 24,
 	D_SS,

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -232,6 +232,7 @@ struct	Node
 	int32	vargen;		// unique name for OTYPE/ONAME
 	int32	lineno;
 	vlong	xoffset;
+	int32	ostk;
 };
 #define	N	((Node*)0)
 

src/runtime/darwin/386/sys.s

--- a/src/runtime/darwin/386/sys.s
+++ b/src/runtime/darwin/386/sys.s
@@ -95,6 +95,10 @@ TEXT bsdthread_create(SB),7,$0
 	CALL	notok(SB)
 	RET
 
+TEXT bsdthread_start(SB),7,$0
+	CALL	notok(SB)
+	RET
+
 TEXT bsdthread_register(SB),7,$40
 	MOVL	$366, AX
 	MOVL	$bsdthread_start(SB), 0(SP)	// threadstart

コアとなるコードの解説

src/cmd/8g/gen.c

  • compile 関数におけるスタックサイズ計算の修正:

    • ptxt->to.offset = rnd(stksize+maxarg, maxround); の行が変更され、maxstksizeという新しい変数が導入されました。
    • if(stksize > maxstksize) maxstksize = stksize; は、現在の関数のスタック使用量(stksize)が、これまでに記録された最大スタック使用量(maxstksize)よりも大きい場合に、maxstksizeを更新します。
    • ptxt->to.offset = rnd(maxstksize+maxarg, maxround); は、最終的なスタックフレームサイズを、関数内で必要となる最大のスタック使用量に基づいて計算するように変更されました。これにより、関数内のあらゆる時点でのスタック要求に対応できるようになります。
    • maxstksize = 0; は、関数コンパイルの最後にmaxstksizeをリセットし、次の関数のコンパイルに影響を与えないようにします。
  • cgen_call 関数の実装:

    • この関数は、Goの関数呼び出しをアセンブリコードに変換する主要なロジックを含んでいます。
    • if(n->left->ullman >= UINF) のブロックは、関数名が別の関数呼び出しの結果である場合(つまり、関数ポインタを介した呼び出しの場合)に、まずその関数アドレスを一時変数(afun)に計算して格納します。
    • gen(n->right); は、関数に渡される引数を評価し、スタックに配置するコードを生成します。
    • setmaxarg(t); は、引数の最大サイズを追跡し、スタックフレームの計算に役立てます。
    • 関数ポインタを介した呼び出しの場合、regalloccgen_asを使って関数アドレスをレジスタにロードし、そのレジスタを介してginscallで呼び出しを行います。
    • 直接的な関数呼び出しの場合(n->left->op == ONAME || n->left->class == PFUNC)、n->left->method = 1; を設定し、ginscall(n->left, proc); で直接呼び出しを行います。
  • cgen_callret 関数の実装:

    • この関数は、関数呼び出しの後に戻り値を処理するコードを生成します。
    • getoutarg(t) を使って、関数の戻り値の型情報を取得します。
    • nod.op = OINDREG; nod.val.u.reg = D_SP; は、スタックポインタ(D_SP)を基準とした間接参照(スタック上のメモリ)を表すノードを作成します。
    • nod.xoffset = fp->width; は、戻り値がスタック上のどこにあるかを示すオフセットを設定します。
    • cgen_as(res, &nod); は、スタック上の戻り値を結果ノード(res)にコピーするコードを生成します。
  • cgen_aret 関数の実装:

    • この関数は、戻り値そのものではなく、戻り値が格納されているメモリのアドレスを取得するコードを生成します。
    • nod1.op = OINDREG; nod1.val.u.reg = D_SP; は、cgen_callretと同様にスタック上のメモリ位置を表すノードを作成します。
    • gins(ALEAL, &nod1, &nod2); は、ALEAL (Load Effective Address) 命令を生成します。これは、nod1が指すメモリ位置のアドレスをnod2レジスタにロードします。
    • gins(AMOVL, &nod2, res); は、そのアドレスを最終的な結果ノード(res)に移動します。もしresがレジスタであれば、直接ALEAL命令でアドレスをロードします。
  • cgen_div, cgen_shift, cgen_bmul のプレースホルダー:

    • これらの関数は、それぞれ除算、シフト演算、バイト乗算のコード生成を担当しますが、このコミット時点ではまだ実装されておらず、呼び出されるとfatalエラーを発生させます。これは、コンパイラの機能が段階的に追加されていることを示しています。

src/cmd/8g/gg.h

  • EXTERN int maxstksize;: maxstksize変数の外部宣言。これにより、gen.c以外のファイルからもこの変数にアクセスできるようになります。
  • void cgen_aret(Node*, Node*);: cgen_aret関数の宣言。
  • int cgen64(Node*, Node*);int is64(Type*);: 64ビット関連のコード生成および型チェック関数の宣言。これは、32ビット環境でも64ビット整数を扱うための準備、または将来的な64ビットアーキテクチャサポートへの布石と考えられます。
  • void tempalloc(Node*, Type*);void tempfree(Node*);: 一時変数の割り当てと解放を行う関数の宣言。

src/cmd/8l/8.out.h

  • D_F7 = D_F0 + 7;: 浮動小数点レジスタの定義に追加された定数。D_F0からD_F7までの8つのFPUレジスタが利用可能であることを示唆しており、浮動小数点演算のサポートに向けた準備です。

src/cmd/gc/go.h

  • struct Nodeint32 ostk; が追加されました。ostkは "offset on stack" の略で、このノードが表すデータがスタックフレーム内のどこに配置されるかを示すオフセットを格納するために使用されます。これは、コンパイラがスタック上の変数や引数にアクセスする際に不可欠な情報です。

src/runtime/darwin/386/sys.s

  • TEXT bsdthread_start(SB),7,$0 が追加されました。これは、Darwin(macOS)システムにおける新しいスレッドの開始点となるアセンブリ関数です。現時点ではCALL notok(SB)RETのみで、実質的には何もしないスタブですが、GoのGoroutineがOSスレッド上で動作するための低レベルな基盤が構築され始めていることを示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • x86アセンブリ言語の一般的な知識
  • コンパイラ設計の原則に関する一般的な書籍や資料
  • Go言語のソースコード(特にsrc/cmd/ディレクトリ以下の初期のコミット)