[インデックス 13171] ファイルの概要
このコミットは、Goコンパイラのcmd/8c
における64ビットレジスタの破壊(register smash)に関する修正を改善するものです。具体的には、src/cmd/8c/cgen.c
とsrc/cmd/8c/cgen64.c
の2つのファイルが変更されています。
コミット
commit 97cbf47c78abf6f776640902804fb0006567a2ec
Author: Russ Cox <rsc@golang.org>
Date: Thu May 24 23:36:26 2012 -0400
cmd/8c: better fix for 64-bit register smash
Ken pointed out that CL 5998043 was ugly code.
This should be better.
Fixes #3501.
R=ken2
CC=golang-dev
https://golang.org/cl/6258049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/97cbf47c78abf6f776640902804fb0006567a2ec
元コミット内容
cmd/8c
: 64ビットレジスタ破壊に対するより良い修正
KenがCL 5998043が醜いコードだと指摘した。 これはより良いはずだ。
Issue #3501を修正。
変更の背景
このコミットは、Goコンパイラのcmd/8c
(x86アーキテクチャ向けのコンパイラ)における、64ビット値の処理中に発生するレジスタ破壊の問題に対する修正の改善を目的としています。
以前の修正(Change List: CL 5998043)は、このレジスタ破壊の問題を解決しようとしましたが、コードの品質が低い("ugly code")と指摘されました。具体的には、cgen64.c
内で、関数呼び出しを伴う複雑な式のアドレスをレジスタに評価する際に、その評価が完了する前にレジスタの内容が破壊される可能性がありました。これは、Goコンパイラがコードを生成する過程で、特定の最適化やレジスタ割り当てのロジックが、予期せぬ副作用を引き起こすことに起因します。
このコミットは、その「醜い」コードをよりクリーンで堅牢な方法に置き換えることで、同じ問題を解決しつつ、コンパイラのコードベースの品質を向上させています。関連するIssue #3501は、cmd/8c
におけるコード生成バグ、特にLinux/386環境での問題として記述されています。
前提知識の解説
cmd/8c
: Go言語のコンパイラツールチェーンの一部で、x86(32ビットおよび64ビット)アーキテクチャ向けのコードを生成するコンパイラです。Goの初期のコンパイラは、各アーキテクチャに対して独立したコンパイラ(例:8c
for x86,6c
for amd64,5c
for ARMなど)を持っていました。- レジスタ破壊 (Register Smash): コンピュータのCPUには、データを一時的に保持するための高速な記憶領域であるレジスタがあります。関数呼び出しや特定の操作中に、本来保持しておくべきレジスタの値が、意図せず別の値で上書きされてしまう現象を「レジスタ破壊」と呼びます。これは、コンパイラがレジスタの利用状況を正確に追跡できていない場合に発生し、プログラムの誤動作やクラッシュにつながることがあります。特に、関数呼び出しはレジスタの状態を大きく変更する可能性があり、注意が必要です。
- 64ビットアーキテクチャ: 64ビットのレジスタやメモリアドレスを使用するCPUアーキテクチャです。32ビットアーキテクチャと比較して、より大きなデータを一度に処理でき、より広いメモリアドレス空間を扱えます。Goコンパイラは、32ビットと64ビットの両方のターゲットに対してコードを生成する必要があり、それぞれの特性に応じたレジスタ管理が必要です。
CL
(Change List): Goプロジェクトでは、Gerritというコードレビューシステムが使われており、各変更は「Change List (CL)」として管理されます。CL 5998043
は、このコミットの前に存在した、レジスタ破壊問題に対する以前の修正を指します。FNX
: Goコンパイラの内部で使われる定数で、ノードの複雑度(complex
フィールド)が関数呼び出しを含むことを示すために使われます。n->complex >= FNX
のような条件は、そのノードが関数呼び出しを伴う複雑な式であることを意味します。このような式を評価する際には、レジスタの状態が変化する可能性が高いため、特別な注意が必要です。regialloc
/regfree
: Goコンパイラのレジスタ割り当て(register allocation)に関連する関数です。regialloc
はレジスタを割り当て、regfree
は割り当てられたレジスタを解放します。これらの関数は、コード生成中にどのレジスタをどの値に割り当てるかを管理し、レジスタの競合や破壊を防ぐ上で重要です。sugen
: Goコンパイラのコード生成フェーズで使用される関数の一つで、特定のノード(式)の値をレジスタやメモリに格納するためのコードを生成します。
技術的詳細
このコミットの核心は、64ビット値のコピー(copy
関数内)において、関数呼び出しを伴う複雑な式(nn->complex >= FNX
)がソース(nn
)として使用される場合のレジスタ管理の改善です。
以前のCL 5998043
では、src/cmd/8c/cgen64.c
内のcgen64
関数で、nn
が複雑な式である場合に、そのアドレスをレジスタに評価し、そのレジスタを使ってn
を処理するというアプローチが取られていました。しかし、このアプローチは、nn
の評価中に発生する可能性のある関数呼び出しが、n
の処理に必要なレジスタを破壊する可能性を完全に排除できていませんでした。また、コードの構造が複雑で理解しにくいという問題がありました。
新しいアプローチでは、src/cmd/8c/cgen.c
のcopy
関数内で、64ビット値(w == 8
)の処理ロジックが変更されています。
v = w == 8;
の移動: 以前はx = 0;
の後にv = w == 8;
がありましたが、これがif(n->complex >= FNX && nn != nil && nn->complex >= FNX)
ブロックの前に移動されました。これにより、64ビット値の処理フラグv
がより早期に設定され、その後のロジックで適切に利用されるようになります。cgen64.c
からのロジックの削除:src/cmd/8c/cgen64.c
から、nn
が複雑な式である場合の特別な処理ブロックが完全に削除されました。これは、このロジックがcgen.c
のcopy
関数に統合され、より一般的な方法で処理されるようになったことを意味します。cgen.c
での新しい処理ロジック:if(v)
(つまり、64ビット値のコピーの場合)のブロック内に、nn
が複雑な式である場合の新しい処理が追加されました。- この新しいロジックでは、まず
nn
の型を一時的にTLONG
(Goコンパイラにおける長整数型)に設定し、regialloc
で一時的なレジスタを割り当ててnn
を評価します(lcgen(nn, &nod2)
)。これにより、nn
の評価結果がレジスタnod2
に格納されます。 - その後、
nn
の型を元に戻し、nod2
を基に間接参照ノードnod1
を作成します。このnod1
は、nod2
が指すメモリ位置の値を表します。 - 最後に、
sugen(n, &nod1, w)
を呼び出して、n
の値をnod1
が指すメモリ位置にコピーするコードを生成します。 - 処理が完了したら、
regfree(&nod2)
で一時的に割り当てたレジスタを解放します。
この変更により、nn
が関数呼び出しを伴う複雑な式であっても、その評価結果が一時的なレジスタに安全に格納され、その後のコピー操作でレジスタが破壊されることなく利用されるようになります。また、cgen64.c
から特定のロジックを削除し、cgen.c
に集約することで、コードの重複が減り、全体的な構造が改善されています。
コアとなるコードの変更箇所
src/cmd/8c/cgen.c
--- a/src/cmd/8c/cgen.c
+++ b/src/cmd/8c/cgen.c
@@ -1703,6 +1703,7 @@ copy:
}
}
+ v = w == 8;
if(n->complex >= FNX && nn != nil && nn->complex >= FNX) {
t = nn->type;
nn->type = types[TLONG];
@@ -1728,8 +1729,28 @@ copy:
}
x = 0;
- v = w == 8;
if(v) {
+ if(nn != nil && nn->complex >= FNX) {
+ t = nn->type;
+ nn->type = types[TLONG];
+ regialloc(&nod2, nn, Z);
+ lcgen(nn, &nod2);
+ nn->type = t;
+
+ nod2.type = typ(TIND, t);
+
+ nod1 = nod2;
+ nod1.op = OIND;
+ nod1.left = &nod2;
+ nod1.right = Z;
+ nod1.complex = 1;
+ nod1.type = t;
+
+ sugen(n, &nod1, w);
+ regfree(&nod2);
+ return;
+ }
+
c = cursafe;
if(n->left != Z && n->left->complex >= FNX
&& n->right != Z && n->right->complex >= FNX) {
src/cmd/8c/cgen64.c
--- a/src/cmd/8c/cgen64.c
+++ b/src/cmd/8c/cgen64.c
@@ -1601,33 +1601,6 @@ cgen64(Node *n, Node *nn)
prtree(n, "cgen64");
print("AX = %d\n", reg[D_AX]);
}
-
- if(nn != Z && nn->complex >= FNX) {
- // Evaluate nn address to register
- // before we use registers for n.
- // Otherwise the call during computation of nn
- // will smash the registers. See
- // http://golang.org/issue/3501.
-
- // If both n and nn want calls, refuse to compile.
- if(n != Z && n->complex >= FNX)
- diag(n, "cgen64 miscompile");
-
- reglcgen(&nod1, nn, Z);
- m = cgen64(n, &nod1);
- regfree(&nod1);
-
- if(m == 0) {
- // Now what? We computed &nn, which involved a
- // function call, and didn't use it. The caller will recompute nn,
- // calling the function a second time.
- // We can figure out what to do later, if this actually happens.
- diag(n, "cgen64 miscompile");
- }
-
- return m;
- }
-
cmp = 0;
sh = 0;
コアとなるコードの解説
src/cmd/8c/cgen.c
の変更点
v = w == 8;
の移動: この行がif(n->complex >= FNX && nn != nil && nn->complex >= FNX)
ブロックの前に移動されました。これにより、v
(64ビット値のコピーを示すフラグ)が、複雑な式の処理に入る前に確実に設定されるようになります。これは、コードの意図をより明確にし、潜在的なタイミングの問題を回避します。- 新しい
if(v)
ブロック内のロジック:if(nn != nil && nn->complex >= FNX)
: これは、コピー元nn
が関数呼び出しを伴う複雑な式である場合にのみ実行される新しいブロックです。t = nn->type; nn->type = types[TLONG];
:nn
の元の型を保存し、一時的にTLONG
(長整数型)に設定します。これは、lcgen
がnn
を評価する際に、64ビット値として適切に扱われるようにするためです。regialloc(&nod2, nn, Z); lcgen(nn, &nod2);
:nn
の評価結果を格納するための一時的なレジスタnod2
を割り当て、lcgen
関数を使ってnn
を評価し、その結果をnod2
に格納します。このステップが重要で、nn
の評価中に発生する可能性のある関数呼び出しが、他の重要なレジスタを破壊する前に、nn
の値が安全にレジスタに退避されます。nn->type = t;
:nn
の型を元の型に戻します。nod2.type = typ(TIND, t);
:nod2
の型を、元の型t
へのポインタ型(間接参照型)に設定します。これは、nod2
がnn
の値そのものではなく、nn
の値が格納されているメモリ位置を指すようにするためです。nod1 = nod2; nod1.op = OIND; nod1.left = &nod2; nod1.right = Z; nod1.complex = 1; nod1.type = t;
:nod2
を基に、間接参照ノードnod1
を構築します。nod1
は、nod2
が指すメモリ位置から値を読み取る操作を表します。これにより、sugen
関数に渡されるnod1
は、nn
の評価結果が格納されたメモリ位置を正確に参照できるようになります。sugen(n, &nod1, w);
:n
の値をnod1
が指すメモリ位置(つまり、nn
の評価結果が格納されている場所)にコピーするコードを生成します。regfree(&nod2);
: 一時的に割り当てたレジスタnod2
を解放します。return;
: このブロックで処理が完了するため、関数を終了します。
この新しいロジックは、複雑な式の評価とレジスタの利用をより厳密に制御することで、レジスタ破壊の問題を根本的に解決しています。
src/cmd/8c/cgen64.c
の変更点
- 複雑な式のアドレス評価ロジックの削除:
cgen64
関数から、nn
が複雑な式である場合にそのアドレスをレジスタに評価し、cgen64
を再帰的に呼び出すという以前のロジックが完全に削除されました。このロジックは、cgen.c
のcopy
関数に統合された新しい、より一般的な処理に置き換えられました。これにより、cgen64.c
のコードが簡素化され、特定のケースに特化した複雑な処理が解消されました。
全体として、このコミットは、Goコンパイラのコード生成ロジックをより堅牢で理解しやすいものにすることで、64ビットレジスタ破壊の問題に対する「より良い」修正を提供しています。
関連リンク
- Go Gerrit Change List: https://golang.org/cl/6258049
- Go Issue #3501: https://github.com/golang/go/issues/3501
参考にした情報源リンク
- github.com (golang/go issue 3501)
- このリンクは、Goの
golang/go
リポジトリにおけるIssue #3501が「cmd/8c: code generation bug」として記述されていることを示しています。
- このリンクは、Goの