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

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

このコミットは、Goコンパイラのcmd/8cにおける64ビットレジスタの破壊(register smash)に関する修正を改善するものです。具体的には、src/cmd/8c/cgen.csrc/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.ccopy関数内で、64ビット値(w == 8)の処理ロジックが変更されています。

  1. v = w == 8; の移動: 以前はx = 0;の後にv = w == 8;がありましたが、これがif(n->complex >= FNX && nn != nil && nn->complex >= FNX)ブロックの前に移動されました。これにより、64ビット値の処理フラグvがより早期に設定され、その後のロジックで適切に利用されるようになります。
  2. cgen64.cからのロジックの削除: src/cmd/8c/cgen64.cから、nnが複雑な式である場合の特別な処理ブロックが完全に削除されました。これは、このロジックがcgen.ccopy関数に統合され、より一般的な方法で処理されるようになったことを意味します。
  3. 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(長整数型)に設定します。これは、lcgennnを評価する際に、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へのポインタ型(間接参照型)に設定します。これは、nod2nnの値そのものではなく、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.ccopy関数に統合された新しい、より一般的な処理に置き換えられました。これにより、cgen64.cのコードが簡素化され、特定のケースに特化した複雑な処理が解消されました。

全体として、このコミットは、Goコンパイラのコード生成ロジックをより堅牢で理解しやすいものにすることで、64ビットレジスタ破壊の問題に対する「より良い」修正を提供しています。

関連リンク

参考にした情報源リンク

  • github.com (golang/go issue 3501)
    • このリンクは、Goのgolang/goリポジトリにおけるIssue #3501が「cmd/8c: code generation bug」として記述されていることを示しています。