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

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

このコミットは、Goコンパイラの一部であるcmd/8c(x86アーキテクチャ向けのCコンパイラ)におけるコード生成のバグ修正に関するものです。具体的には、uint64型の値を複雑なポインタ(関数呼び出しによってアドレスが決定されるポインタ)にストアする際に発生する問題に対処しています。

コミット

commit 30bc5d7bbd8644e044c8c3ecfceca9455326b7a5
Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 10 10:45:58 2012 -0400

    cmd/8c: fix store to complex uint64 ptr
    
    Assignment of a computed uint64 value to an
    address derived with a function call was executing
    the call after computing the value, which trashed
    the value (held in registers).
    
    long long *f(void) { return 0; }
    void g(int x, int y) {
            *f() = (long long)x | (long long)y<<32;
    }
    
    Before:
    
    (x.c:3) TEXT    g+0(SB),(gok(71))
    ...
    (x.c:4) ORL     AX,DX
    (x.c:4) ORL     CX,BX
    (x.c:4) CALL    ,f+0(SB)
    (x.c:4) MOVL    DX,(AX)
    (x.c:4) MOVL    BX,4(AX)
    
    After:
    (x.c:3) TEXT    g+0(SB),(gok(71))
    (x.c:4) CALL    ,f+0(SB)
    ...
    (x.c:4) ORL     CX,BX
    (x.c:4) ORL     DX,BP
    (x.c:4) MOVL    BX,(AX)
    (x.c:4) MOVL    BP,4(AX)
    
    Fixes #3501.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5998043

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

https://github.com/golang/go/commit/30bc5d7bbd8644e044c8c3ecfceca9455326b7a5

元コミット内容

このコミットは、cmd/8c(Goコンパイラのx86バックエンド)におけるバグ修正です。具体的には、計算されたuint64型の値を、関数呼び出しによってアドレスが決定されるポインタ(例: *f())に代入する際に発生する問題に対処しています。

問題は、uint64値の計算が完了した後に関数呼び出し(ポインタのアドレスを決定するため)が実行されると、その関数呼び出しがレジスタに保持されていた計算済みのuint64値を破壊してしまうというものでした。これにより、誤った値がメモリにストアされていました。

コミットメッセージには、この問題を再現するC言語のコードスニペットと、修正前後のアセンブリコードの比較が示されています。

変更の背景

この変更は、Go Issue 3501「cmd/8c: code generation bug」を修正するために行われました。このバグは、特定の条件下でGoコンパイラが誤ったアセンブリコードを生成し、結果としてプログラムが期待通りに動作しないというものでした。

具体的には、uint64のような64ビット値を計算し、その結果を関数呼び出しによって得られるポインタ(例: *f())に書き込む際に、コンパイラが値の計算とポインタのアドレス計算(関数呼び出し)の順序を誤っていました。値の計算結果がレジスタに一時的に保持されている間にポインタのアドレスを計算するための関数が呼び出されると、その関数がレジスタを上書きしてしまい、計算済みの値が失われるという問題が発生していました。

この種のバグは、コンパイラのコード生成におけるレジスタ割り当てと命令スケジューリングの複雑さに起因します。特に、関数呼び出しはレジスタの状態を大きく変更する可能性があるため、その前後で重要な値が適切に保存・復元されるように注意深くコードを生成する必要があります。

前提知識の解説

  • Goコンパイラ (cmd/8c): Go言語のソースコードを機械語に変換するコンパイラの一部です。8cはx86(32ビットおよび64ビット)アーキテクチャ向けのCコンパイラであり、Goコンパイラのバックエンドとして利用されていました(現在はより新しいコンパイラインフラストラクチャに置き換えられています)。このコンパイラは、Goのコードを中間表現に変換した後、最終的なアセンブリコードを生成する役割を担っていました。
  • レジスタ (Registers): CPU内部にある高速な記憶領域です。プログラムの実行中に頻繁にアクセスされるデータ(変数、計算結果、ポインタなど)が一時的に格納されます。レジスタの数は限られているため、コンパイラはどの値をどのレジスタに割り当てるか(レジスタ割り当て)を効率的に決定する必要があります。
  • 関数呼び出し (Function Call): プログラムの実行フローを別の関数に一時的に移す操作です。関数が呼び出されると、引数が渡され、関数のローカル変数がスタックに割り当てられ、そして関数内のコードが実行されます。関数は通常、レジスタを使用して引数を受け取ったり、戻り値を返したりします。この際、呼び出し元が使用していたレジスタの内容が、呼び出された関数によって上書きされる(「スマッシュされる」または「破壊される」)可能性があります。そのため、コンパイラは呼び出し規約(calling convention)に従って、レジスタの保存・復元を行うコードを生成します。
  • uint64: 符号なし64ビット整数型です。x86アーキテクチャでは、64ビット値を扱うために2つの32ビットレジスタ(例: AXDX、またはCXBX)を組み合わせて使用することが一般的です。
  • 複雑なポインタ (Complex Pointer): ポインタのアドレスが単純な変数や定数ではなく、関数呼び出しの結果や複雑な計算によって決定される場合を指します。例: *f()f() の部分。
  • アセンブリコード (Assembly Code): 機械語と1対1に対応する低レベルのプログラミング言語です。コンパイラは最終的にアセンブリコードを生成し、それがアセンブラによって機械語に変換されます。アセンブリコードを読むことで、コンパイラがどのようにレジスタを使用し、命令をスケジューリングしているかを詳細に理解できます。
  • FNX (Function Call Expression): コンパイラの内部表現において、関数呼び出しを含む複雑な式を示すフラグまたは状態。nn->complex >= FNX は、nnというノード(式)が関数呼び出しを含む複雑なものであることを示唆しています。

技術的詳細

このバグは、コンパイラのコード生成フェーズ、特にcgen64.cファイル内のcgen64関数で発生していました。この関数は、Goの抽象構文木(AST)のノードを受け取り、それに対応するx86アセンブリコードを生成する役割を担っています。

問題の核心は、*f() = (long long)x | (long long)y<<32; のような代入文の処理順序にありました。

  1. 右辺の (long long)x | (long long)y<<32 の計算が行われ、その結果がレジスタ(例: DX:AXBX:CX のようなレジスタペア)に格納されます。
  2. 次に、左辺の*f() のアドレスを計算するために、関数f()が呼び出されます。

ここで問題が発生します。f()の呼び出しは、呼び出し規約に従ってレジスタを使用します。もしf()が、右辺の計算結果が格納されているレジスタを上書きしてしまうと、その値は失われ、誤った値がメモリにストアされてしまいます。

修正前のアセンブリコードでは、ORL命令(右辺の計算)の後にCALL命令(f()の呼び出し)が来ており、その後にMOVL命令(ストア)が続いていました。これは、値の計算が関数呼び出しの前に完了していることを示しています。

Before:
(x.c:4) ORL     AX,DX   // 右辺の計算の一部
(x.c:4) ORL     CX,BX   // 右辺の計算の残り
(x.c:4) CALL    ,f+0(SB) // f() の呼び出し
(x.c:4) MOVL    DX,(AX)  // ストア(DXの内容が破壊されている可能性)
(x.c:4) MOVL    BX,4(AX) // ストア(BXの内容が破壊されている可能性)

修正後のアセンブリコードでは、CALL命令がORL命令の前に移動しています。これにより、f()が呼び出されてポインタのアドレスがレジスタ(AX)に格納された後で、右辺の計算が行われます。この順序であれば、右辺の計算結果がf()によって破壊されることはありません。

After:
(x.c:4) CALL    ,f+0(SB) // f() の呼び出し(まずポインタのアドレスを計算)
...
(x.c:4) ORL     CX,BX   // 右辺の計算の一部
(x.c:4) ORL     DX,BP   // 右辺の計算の残り(BPは新しいレジスタ)
(x.c:4) MOVL    BX,(AX)  // ストア
(x.c:4) MOVL    BP,4(AX) // ストア

この修正は、cgen64関数内で、代入の右辺(n)と左辺(nn、ポインタのアドレス)の両方が関数呼び出しを含む可能性がある場合に、nn(アドレス)の計算をn(値)の計算よりも先に行うようにロジックを追加することで実現されています。

具体的には、nnが関数呼び出しを含む複雑な式である場合(nn->complex >= FNX)、まずnnのアドレスをレジスタに評価(reglcgen(&nod1, nn, Z))し、その後にnの値を計算してストアするように変更されています。これにより、値の計算中にアドレス計算のための関数呼び出しが行われ、レジスタが破壊されることを防ぎます。

また、nnnの両方が関数呼び出しを含む場合は、コンパイルを拒否する(diag(n, "cgen64 miscompile"))という防御的なチェックも追加されています。これは、そのような複雑なケースを適切に処理するためのロジックがまだ存在しないため、コンパイラが誤ったコードを生成するのを防ぐための措置です。

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

変更はsrc/cmd/8c/cgen64.cファイルに集中しています。

--- a/src/cmd/8c/cgen64.c
+++ b/src/cmd/8c/cgen64.c
@@ -1601,6 +1601,33 @@ 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;

コアとなるコードの解説

追加されたコードブロックは、cgen64関数の冒頭近くに挿入されています。

  1. if(nn != Z && nn->complex >= FNX):

    • nn != Z: nnがNULLでないことを確認します。nnは代入の左辺、つまり値をストアするアドレスを表すノードです。
    • nn->complex >= FNX: nnが関数呼び出しを含む複雑な式であることをチェックします。FNXは、コンパイラの内部で関数呼び出しを伴う式を示す定数です。この条件が真の場合、nnのアドレスを計算するために関数呼び出しが必要であることを意味します。
  2. コメントブロック:

    • 「Evaluate nn address to register before we use registers for n.」: n(代入される値)のためにレジスタを使用する前に、nn(アドレス)をレジスタに評価する必要があることを説明しています。
    • 「Otherwise the call during computation of nn will smash the registers.」: そうしないと、nnの計算中に行われる関数呼び出しが、nの計算結果が格納されているレジスタを破壊してしまうことを警告しています。
    • 「See http://golang.org/issue/3501.」: この修正がGo Issue 3501に関連していることを示しています。
  3. if(n != Z && n->complex >= FNX):

    • この内部のif文は、n(代入される値)も関数呼び出しを含む複雑な式であるかどうかをチェックします。
    • もしnnnの両方が関数呼び出しを含む場合、現在のコンパイラロジックではこれを安全に処理できないため、diag(n, "cgen64 miscompile")を呼び出してコンパイルエラーを発生させます。これは、未対応の複雑なケースで誤ったコードが生成されるのを防ぐための防御的な措置です。
  4. reglcgen(&nod1, nn, Z);:

    • reglcgenは、与えられたノード(ここではnn)のアドレスをレジスタにロードする関数です。
    • &nod1は、アドレスがロードされたレジスタに関する情報が格納される一時的なノードです。
    • この行により、*f()f()が呼び出され、その戻り値(ポインタのアドレス)がレジスタに格納されます。
  5. m = cgen64(n, &nod1);:

    • cgen64関数を再帰的に呼び出します。
    • nは代入される値のノードです。
    • &nod1は、先ほど計算されたnnのアドレスが格納されているレジスタ情報を持つノードです。
    • この呼び出しにより、nの値が計算され、&nod1が指すアドレスにストアされるコードが生成されます。この時点でnnのアドレスは既に計算され、レジスタに保持されているため、nの計算中にレジスタが破壊される心配はありません。
  6. regfree(&nod1);:

    • nod1によって使用されたレジスタを解放します。
  7. if(m == 0) ブロック:

    • cgen64の再帰呼び出しが成功しなかった場合(m == 0)、これは予期せぬ状況であり、コンパイラのバグを示唆します。
    • コメントでは、「We computed &nn, which involved a function call, and didn't use it. The caller will recompute nn, calling the function a second time.」と説明されており、nnのアドレスを計算したにもかかわらず、それが使用されなかった場合、呼び出し元が再度nnを計算しようとし、関数が二度呼び出される可能性があることを示唆しています。
    • これもdiag(n, "cgen64 miscompile")を呼び出してコンパイルエラーとします。

この変更により、uint64値の計算と、その値をストアするポインタのアドレス計算(関数呼び出しを伴う場合)の順序が適切に制御され、レジスタの破壊によるバグが修正されました。

関連リンク

参考にした情報源リンク

  • https://github.com/golang/go/issues/3501
  • コミットメッセージ内のコードスニペットとアセンブリコードの比較
  • Goコンパイラのコードベース(特にsrc/cmd/8c/cgen64.c)の一般的な知識