[インデックス 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) を経て、最終的にターゲットアーキテクチャの機械語を生成します。
- Go言語のコンパイラは、ターゲットアーキテクチャごとに異なるフロントエンドとバックエンドを持っています。
ggen.c
:ggen.c
ファイルは、Goコンパイラのバックエンドの一部であり、主にコード生成 (code generation) に関連する処理を担当します。ASTやIRから、最終的なアセンブリコードを生成するロジックが含まれています。
cgen_ret
関数:- この関数は、Goプログラム内の
return
ステートメントに対応するアセンブリコードを生成する役割を担っています。関数の戻り値の処理、defer
関数の実行、そして最終的な関数からのリターン命令の生成が含まれます。
- この関数は、Goプログラム内の
defer
ステートメント:- Go言語の
defer
ステートメントは、それが含まれる関数がリターンする直前に、指定された関数呼び出し(defer
関数)を実行することを保証します。これは、リソースの解放(ファイルクローズ、ロック解除など)やエラーハンドリングによく使用されます。コンパイラは、defer
関数が適切なタイミングで実行されるように、特別なコードを生成する必要があります。
- Go言語の
- コンパイラ内部のデータ構造:
Node
: 抽象構文木 (AST) のノードを表すデータ構造です。Goのソースコードの各要素(変数、関数呼び出し、演算子など)はNode
として表現されます。Prog
: 生成されるアセンブリ命令を表すデータ構造です。各Prog
は、オペコード(例:ARET
)とオペランド(例: レジスタ、メモリ位置)を含みます。
- アセンブリ命令/オペコード:
ARET
: Goコンパイラ内部で使われるアセンブリ命令のオペコードの一つで、関数のリターン処理を表します。ORETJMP
:return
ステートメントが、別の関数へのジャンプとして最適化される場合(例: テールコール最適化のような状況)に使用される可能性のある、コンパイラ内部のオペコードです。
技術的詳細
このコミットの技術的な核心は、src/cmd/5g/ggen.c
とsrc/cmd/8g/ggen.c
の両方にあるcgen_ret
関数の修正にあります。この関数は、Goの関数がリターンする際に実行されるべきコードを生成します。
変更前は、cgen_ret
関数は以下のようなロジックを持っていました(簡略化):
genlist(n->list)
: 戻り値の引数をコピーするコードを生成します。if(hasdefer || curfn->exit)
:defer
関数が存在するか、または現在の関数に終了処理(curfn->exit
)がある場合に、retpc
(リターンアドレス)へのジャンプを生成します。p = gins(ARET, N, N)
:ARET
命令を生成します。if(n->op == ORETJMP)
:ORETJMP
の場合の特殊な処理を行います。
このロジックには、CL 83090046によって引き起こされた問題がありました。特に、defer
関数が存在する場合の処理と、curfn->exit
(関数の終了時に実行されるべきコード)の処理順序に問題があったと考えられます。
変更後のロジックは、これらの問題を修正し、より堅牢なコード生成を実現しています。主な変更点は以下の通りです。
if(n != N)
の追加:genlist(n->list)
の呼び出しが、n
がNULL
でない場合にのみ行われるようになりました。これは、戻り値がない関数や、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)
に変更されました。これもn
がNULL
でないことを確認することで、より安全なコード生成を保証します。
これらの変更により、Goコンパイラは、defer
関数と関数の戻り値処理をより正確に、かつ堅牢にコード生成できるようになり、CL 83090046で導入されたビルドの問題が解決されました。
コアとなるコードの変更箇所
src/cmd/5g/ggen.c
および src/cmd/8g/ggen.c
の cgen_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);
:n
がNULL
でない場合にのみ、戻り値の引数コピーのコードを生成するように変更されました。これにより、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 CL 83070046 (このコミットのコードレビューページ): https://golang.org/cl/83070046
- Go CL 83090046 (このコミットが修正した問題の原因となったCL): https://go-review.googlesource.com/c/go/+/83090046 (直接のGitHubリンクではないため、Goのコードレビューサイトへのリンクを記載)
参考にした情報源リンク
- Go言語の
defer
ステートメントに関する公式ドキュメント: - Goコンパイラの内部構造に関する一般的な情報:
- Goのソースコード(特に
src/cmd/compile
ディレクトリ) - Goコンパイラの設計に関する論文やブログ記事(例: "The Design of the Go Assembler" など)
- Goのソースコード(特に
- Web検索: "golang CL 83090046", "Go compiler 5g 8g ggen.c cgen_ret", "Go defer implementation"