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

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

このコミットは、Goコンパイラのcmd/5g (ARMアーキテクチャ向け) と cmd/8g (x86アーキテクチャ向け) におけるビルドの問題を修正するものです。具体的には、以前の変更セット (CL 83090046) で導入された不具合を修正し、関数の戻り値処理とdefer呼び出しのコード生成を改善しています。

コミット

commit 9c8f11ff96dbef5ad6020f1c47d9e55b3284ec21
Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 1 20:24:53 2014 -0400

    cmd/5g, cmd/8g: fix build
    
    Botched during CL 83090046.
    
    TBR=khr
    CC=golang-codereviews
    https://golang.org/cl/83070046

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

https://github.com/golang/go/commit/9c8f11ff96dbef5ad6020f1c47d9e55b3284ec21

元コミット内容

cmd/5g, cmd/8g: fix build Botched during CL 83090046.

変更の背景

このコミットは、Goコンパイラのcmd/5gおよびcmd/8gにおけるビルドの問題を修正するために行われました。コミットメッセージに「Botched during CL 83090046」とあるように、この変更は、以前の変更セットであるCL 83090046(「runtime: fix stack overflow in deep recursion」というタイトルで、深い再帰におけるスタックオーバーフローの修正を目的としたもの)の際に誤って導入された不具合を修正するものです。

CL 83090046は、Goランタイムが深い再帰関数によって引き起こされるスタックオーバーフローをより堅牢に処理し、クラッシュを防ぎ、深いコールスタックを持つアプリケーションの安定性を向上させることを目的としていました。しかし、その変更の過程で、コンパイラのコード生成ロジック、特にreturnステートメントとdefer呼び出しの処理において、意図しない副作用が生じたと考えられます。本コミットは、この副作用によって引き起こされたビルドの問題、すなわちコンパイラが正しくコードを生成できない状況を解消することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの内部構造とGo言語の機能に関する知識が必要です。

  • Goコンパイラ (cmd/5g, cmd/8g):
    • Go言語のコンパイラは、ターゲットアーキテクチャごとに異なるフロントエンドとバックエンドを持っています。5gはARMアーキテクチャ(例: ARMv5, ARMv6, ARMv7)向けのコンパイラ、8gはx86アーキテクチャ(例: x86-32, x86-64)向けのコンパイラです。これらはGoのソースコードを各アーキテクチャの機械語に変換する役割を担います。
    • Goコンパイラは、ソースコードを抽象構文木 (AST) に変換し、その後、中間表現 (IR) を経て、最終的にターゲットアーキテクチャの機械語を生成します。
  • ggen.c:
    • ggen.cファイルは、Goコンパイラのバックエンドの一部であり、主にコード生成 (code generation) に関連する処理を担当します。ASTやIRから、最終的なアセンブリコードを生成するロジックが含まれています。
  • cgen_ret 関数:
    • この関数は、Goプログラム内のreturnステートメントに対応するアセンブリコードを生成する役割を担っています。関数の戻り値の処理、defer関数の実行、そして最終的な関数からのリターン命令の生成が含まれます。
  • defer ステートメント:
    • Go言語のdeferステートメントは、それが含まれる関数がリターンする直前に、指定された関数呼び出し(defer関数)を実行することを保証します。これは、リソースの解放(ファイルクローズ、ロック解除など)やエラーハンドリングによく使用されます。コンパイラは、defer関数が適切なタイミングで実行されるように、特別なコードを生成する必要があります。
  • コンパイラ内部のデータ構造:
    • Node: 抽象構文木 (AST) のノードを表すデータ構造です。Goのソースコードの各要素(変数、関数呼び出し、演算子など)はNodeとして表現されます。
    • Prog: 生成されるアセンブリ命令を表すデータ構造です。各Progは、オペコード(例: ARET)とオペランド(例: レジスタ、メモリ位置)を含みます。
  • アセンブリ命令/オペコード:
    • ARET: Goコンパイラ内部で使われるアセンブリ命令のオペコードの一つで、関数のリターン処理を表します。
    • ORETJMP: returnステートメントが、別の関数へのジャンプとして最適化される場合(例: テールコール最適化のような状況)に使用される可能性のある、コンパイラ内部のオペコードです。

技術的詳細

このコミットの技術的な核心は、src/cmd/5g/ggen.csrc/cmd/8g/ggen.cの両方にあるcgen_ret関数の修正にあります。この関数は、Goの関数がリターンする際に実行されるべきコードを生成します。

変更前は、cgen_ret関数は以下のようなロジックを持っていました(簡略化):

  1. genlist(n->list): 戻り値の引数をコピーするコードを生成します。
  2. if(hasdefer || curfn->exit): defer関数が存在するか、または現在の関数に終了処理(curfn->exit)がある場合に、retpc(リターンアドレス)へのジャンプを生成します。
  3. p = gins(ARET, N, N): ARET命令を生成します。
  4. if(n->op == ORETJMP): ORETJMPの場合の特殊な処理を行います。

このロジックには、CL 83090046によって引き起こされた問題がありました。特に、defer関数が存在する場合の処理と、curfn->exit(関数の終了時に実行されるべきコード)の処理順序に問題があったと考えられます。

変更後のロジックは、これらの問題を修正し、より堅牢なコード生成を実現しています。主な変更点は以下の通りです。

  • if(n != N) の追加: genlist(n->list)の呼び出しが、nNULLでない場合にのみ行われるようになりました。これは、戻り値がない関数や、returnステートメントが特定のNodeに関連付けられていない場合でも安全に処理できるようにするためです。
  • defer呼び出しの明示的な挿入: if(hasdefer)の条件が独立し、ginscall(deferreturn, 0)が明示的に呼び出されるようになりました。これにより、defer関数が常に適切なタイミングで(ARET命令の前に)実行されることが保証されます。以前はretpcへのジャンプの中にdeferの処理が含まれていた可能性があり、これが問題を引き起こしていたと考えられます。
  • curfn->exitの独立した処理: genlist(curfn->exit)defer処理とは独立して呼び出されるようになりました。これにより、関数の終了時に必要なクリーンアップコードが、defer関数とは別のロジックとして確実に実行されます。
  • ORETJMPの条件の修正: if(n->op == ORETJMP)の条件がif(n != N && n->op == ORETJMP)に変更されました。これもnNULLでないことを確認することで、より安全なコード生成を保証します。

これらの変更により、Goコンパイラは、defer関数と関数の戻り値処理をより正確に、かつ堅牢にコード生成できるようになり、CL 83090046で導入されたビルドの問題が解決されました。

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

src/cmd/5g/ggen.c および src/cmd/8g/ggen.ccgen_ret 関数における変更点:

--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -472,13 +472,13 @@ cgen_ret(Node *n)
 {
 	Prog *p;
 
-	genlist(n->list);		// copy out args
-	if(hasdefer || curfn->exit) {
-		gjmp(retpc);
-		return;
-	}
+	if(n != N)
+		genlist(n->list);		// copy out args
+	if(hasdefer)
+		ginscall(deferreturn, 0);
+	genlist(curfn->exit);
 	p = gins(ARET, N, N);
-	if(n->op == ORETJMP) {
+	if(n != N && n->op == ORETJMP) {
 		p->to.name = D_EXTERN;
 		p->to.type = D_CONST;
 		p->to.sym = linksym(n->left->sym);
diff --git a/src/cmd/8g/ggen.c b/src/cmd/8g/ggen.c
index 2ece188128..8388e64bd5 100644
--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -462,13 +462,13 @@ cgen_ret(Node *n)
 {
 	Prog *p;
 
-	genlist(n->list);		// copy out args
-	if(retpc) {
-		gjmp(retpc);
-		return;
-	}
+	if(n != N)
+		genlist(n->list);		// copy out args
+	if(hasdefer)
+		ginscall(deferreturn, 0);
+	genlist(curfn->exit);
 	p = gins(ARET, N, N);
-	if(n->op == ORETJMP) {
+	if(n != N && n->op == ORETJMP) {
 		p->to.type = D_EXTERN;
 		p->to.sym = linksym(n->left->sym);
 	}

コアとなるコードの解説

cgen_ret関数は、Goの関数がリターンする際に実行されるアセンブリコードを生成します。この関数は、Node *nという引数を受け取りますが、これはリターンステートメントに関連するASTノードを指します。

変更前と変更後のコードを比較しながら解説します。

変更前:

	genlist(n->list);		// copy out args
	if(hasdefer || curfn->exit) {
		gjmp(retpc);
		return;
	}
	p = gins(ARET, N, N);
	if(n->op == ORETJMP) {
		// ...
	}
  • genlist(n->list);: これは、関数の戻り値(n->listに格納されている)をコピーするためのコードを生成します。
  • if(hasdefer || curfn->exit): ここが問題の核心でした。defer関数が存在するか(hasdefer)、または現在の関数に終了処理(curfn->exit)がある場合、retpc(リターンアドレス)への無条件ジャンプgjmp(retpc)を行っていました。このロジックでは、defer関数の呼び出しやcurfn->exitの処理がARET命令の前に適切に行われない可能性がありました。特に、deferの実行はARET命令の直前に行われるべきですが、この構造ではgjmpによってスキップされるか、不適切な順序で実行されるリスクがありました。

変更後:

	if(n != N)
		genlist(n->list);		// copy out args
	if(hasdefer)
		ginscall(deferreturn, 0);
	genlist(curfn->exit);
	p = gins(ARET, N, N);
	if(n != N && n->op == ORETJMP) {
		// ...
	}
  • if(n != N) genlist(n->list);: nNULLでない場合にのみ、戻り値の引数コピーのコードを生成するように変更されました。これにより、nが有効なノードでない場合の潜在的なクラッシュや不正なコード生成を防ぎます。
  • if(hasdefer) ginscall(deferreturn, 0);: defer関数が存在する場合(hasdeferが真の場合)に、deferreturn関数を呼び出すためのコード(ginscall)を明示的に生成するようになりました。これにより、defer関数がARET命令の直前に確実に実行されることが保証されます。これは、Goのdeferのセマンティクスに厳密に従うための重要な修正です。
  • genlist(curfn->exit);: curfn->exit(関数の終了時に実行されるべきクリーンアップコードなど)のコード生成が、defer処理とは独立して、かつARET命令の前に実行されるように配置されました。これにより、関数の終了処理がdeferとは別のロジックとして確実に実行されます。
  • p = gins(ARET, N, N);: 変更前と同様に、最終的なリターン命令ARETを生成します。
  • if(n != N && n->op == ORETJMP): ORETJMPの条件にn != Nが追加されました。これにより、nが有効なノードである場合にのみORETJMPの特殊な処理が行われるようになり、堅牢性が向上しました。

これらの変更により、cgen_ret関数は、defer関数の実行、関数の終了処理、および戻り値の処理を、Go言語のセマンティクスに沿って、より正確かつ安全にコード生成できるようになりました。これにより、CL 83090046で導入されたコンパイラのビルド問題が解決されました。

関連リンク

参考にした情報源リンク

  • Go言語のdeferステートメントに関する公式ドキュメント:
  • Goコンパイラの内部構造に関する一般的な情報:
    • Goのソースコード(特にsrc/cmd/compileディレクトリ)
    • Goコンパイラの設計に関する論文やブログ記事(例: "The Design of the Go Assembler" など)
  • Web検索: "golang CL 83090046", "Go compiler 5g 8g ggen.c cgen_ret", "Go defer implementation"