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

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

このコミットは、Goコンパイラ(cmd/gc)において、定数式がバックエンド(コード生成部分)に渡される前に適切に評価されるようにするための修正です。これにより、バックエンドが定数式を不適切に処理することによって発生する問題を回避し、コンパイラの安定性と正確性を向上させます。具体的には、y%1 == 0のような実行時に定数となる式が、コンパイル時に正しく定数として扱われるように改善されています。

コミット

commit d7c99cdf9fa5548db179758ac9dd267f5f1c9e88
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Jul 25 09:42:05 2013 -0400

    cmd/gc: avoid passing unevaluated constant expressions to backends.
    
    Backends do not exactly expect receiving binary operators with
    constant operands or use workarounds to move them to
    register/stack in order to handle them.
    
    Fixes #5841.
    
    R=golang-dev, daniel.morsing, rsc
    CC=golang-dev
    https://golang.org/cl/11107044

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

https://github.com/golang/go/commit/d7c99cdf9fa5548db179758ac9dd267f5f1c9e88

元コミット内容

cmd/gc: avoid passing unevaluated constant expressions to backends.

Backends do not exactly expect receiving binary operators with
constant operands or use workarounds to move them to
register/stack in order to handle them.

Fixes #5841.

R=golang-dev, daniel.morsing, rsc
CC=golang-dev
https://golang.org/cl/11107044

変更の背景

このコミットは、Goコンパイラ(cmd/gc)が抱えていたIssue 5841を修正するために導入されました。Issue 5841は、8gコンパイラ(当時のx86アーキテクチャ向けGoコンパイラ)が不正なCMPL $0, $0命令を生成するというバグでした。これは、定数式がコンパイラのバックエンドに渡される際に、適切に評価されずに渡されてしまうことが原因で発生していました。

具体的には、y%1 == 0のような式において、y%1は常に0となるため、この式全体は実行時に常にtrueとなる定数式です。しかし、コンパイラがこの定数性を適切に認識せず、バックエンドに未評価の二項演算子(%==)と定数オペランドを渡してしまうと、バックエンドはこれをレジスタやスタックに移動させるなどの不必要な処理を試み、結果として不正なアセンブリコード(CMPL $0, $0など)を生成してしまう可能性がありました。この不正なコードは、リンク時にエラーを引き起こし、プログラムのビルドを妨げていました。

この問題は、Issue 5002と類似しており、コンパイラの最適化とコード生成の正確性に関わる重要なバグでした。

前提知識の解説

Goコンパイラ (cmd/gc, 8g)

Go言語の公式コンパイラは、gc(Go Compiler)と呼ばれます。かつては、ターゲットアーキテクチャごとに異なる名前のコンパイラが存在しました。例えば、x86アーキテクチャ向けには8g、ARMアーキテクチャ向けには5gなどがありました。現在では、これらのコンパイラは統合され、単一のgo tool compileコマンドとして提供されていますが、このコミットが作成された当時は8gのような名称が使われていました。

Goコンパイラは、ソースコードを解析し、中間表現(IR)に変換し、最終的にターゲットアーキテクチャの機械語コードを生成する役割を担います。このプロセスは複数のフェーズに分かれており、主要なフェーズには以下のようなものがあります。

  • パース (Parsing): ソースコードを抽象構文木(AST)に変換します。
  • 型チェック (Type Checking): ASTの各ノードの型を検証し、型エラーを検出します。
  • ウォーク (Walk): ASTを走査し、最適化やコード生成のための変換を行います。このフェーズで、定数伝播や不要なコードの削除などが行われます。
  • バックエンド (Backend): ウォークフェーズで変換された中間表現を受け取り、ターゲットアーキテクチャの機械語コードを生成します。

定数式 (Constant Expressions)

定数式とは、コンパイル時にその値が決定される式のことです。例えば、1 + 2true && falseのような式は定数式です。Go言語では、数値リテラル、文字列リテラル、ブールリテラル、およびそれらを用いた算術演算、論理演算、比較演算の結果が定数式となり得ます。

コンパイラは、可能な限り定数式をコンパイル時に評価し、その結果の定数値をコードに埋め込むことで、実行時の計算コストを削減し、最適化の機会を増やします。これを「定数伝播(Constant Propagation)」と呼びます。

バックエンド (Backend)

コンパイラのバックエンドは、中間表現を最終的な機械語コードに変換する部分です。バックエンドは、レジスタ割り当て、命令選択、スケジューリングなどの複雑なタスクを実行します。バックエンドは、通常、特定のアーキテクチャ(x86、ARMなど)に特化しており、そのアーキテクチャの命令セットとレジスタセットを最大限に活用するように設計されています。

バックエンドは、入力として受け取る中間表現が特定の形式であることを期待します。例えば、定数式はすでに評価され、単一の定数値として渡されることを期待することが多いです。未評価の二項演算子と定数オペランドの組み合わせは、バックエンドにとって予期せぬ入力となり、不適切なコード生成を引き起こす可能性があります。

CMPL 命令 (Compare Long)

CMPLは、x86アセンブリ言語における比較命令の一つです。通常、2つのオペランドを比較し、その結果に応じてCPUのフラグレジスタ(ZF, CF, SF, OFなど)を設定します。これらのフラグは、その後の条件分岐命令(JE (Jump if Equal), JL (Jump if Less) など)で使用されます。

CMPL $0, $0という命令は、00を比較するという意味です。これは常に真であり、通常は意味のない命令です。このような命令が生成されることは、コンパイラが何らかの論理的な誤りを犯していることを示唆しています。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのwalkフェーズにおける定数式の評価のタイミングと正確性の改善にあります。

Goコンパイラのwalkフェーズは、抽象構文木(AST)を走査し、様々な最適化や変換を行う重要な段階です。このフェーズでは、式が評価され、可能な場合は定数に変換されます。しかし、このコミット以前は、一部の式(特にy%1 == 0のような、実行時には定数となるが言語仕様上は「定数」と明示的に定義されていないもの)が、walkフェーズの途中で完全に定数に変換されないまま、バックエンドに渡される可能性がありました。

問題は、バックエンドが、二項演算子(例: %==)と、そのオペランドが両方とも定数であるような式を直接処理することを想定していない点にありました。バックエンドは、このような式を受け取ると、それらをレジスタやスタックに移動させようとするなど、不必要な複雑な処理を試みることがありました。これは、バックエンドが期待する入力形式が、すでに評価された単一の定数値であるためです。

このコミットでは、walkexpr関数(walkフェーズで式を走査する主要な関数)の最後に、evconst(n)という呼び出しを追加することでこの問題を解決しています。

evconst(n)関数は、与えられたノードnが定数式であるかどうかを再評価し、もし定数であればそのノードを対応する定数ノードに変換します。この変更により、walkexprが式を処理し、その引数が更新された後、その式自体が定数になっているかどうかを明示的にチェックするようになりました。

例えば、y%1 == 0という式の場合、walkフェーズでy%1が評価され、その結果が0になることが判明します。この時点で、式は0 == 0という形になります。この0 == 0という式は、実行時に常にtrueとなる定数式です。しかし、以前のコンパイラでは、この0 == 0が完全にtrueという定数に変換されずにバックエンドに渡されることがありました。

evconst(n)の追加により、walkexpr0 == 0という式を処理した後、その結果が定数であるかどうかを再確認し、もし定数であればそれをtrueという定数ノードに変換します。これにより、バックエンドには未評価の二項演算子ではなく、完全に評価された定数(この場合はtrue)が渡されるようになり、不正なコード生成が防止されます。

この修正は、コンパイラのフロントエンド(walkフェーズ)とバックエンド間のインターフェースをより明確にし、バックエンドがより単純で予測可能な入力を受け取ることを保証します。結果として、コンパイラの堅牢性が向上し、特定の条件下での不正なアセンブリコードの生成が回避されます。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/cmd/gc/walk.c: Goコンパイラのwalkフェーズの主要なロジックが含まれるファイルです。
  2. test/fixedbugs/issue5841.go: Issue 5841を再現するための新しいテストケースです。

src/cmd/gc/walk.c の変更

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1379,6 +1379,13 @@ walkexpr(Node **np, NodeList **init)\n 	fatal("missing switch %O", n->op);\n \n ret:\n+\t// Expressions that are constant at run time but not\n+\t// considered const by the language spec are not turned into\n+\t// constants until walk. For example, if n is y%1 == 0, the\n+\t// walk of y%1 may have replaced it by 0.\n+\t// Check whether n with its updated args is itself now a constant.\n+\tevconst(n);\n+\n \tullmancalc(n);\n \n \tif(debug['w'] && n != N)\n```

この変更は、`walkexpr`関数の`ret:`ラベルの直前に、`evconst(n);`という行を追加しています。これは、式`n`のウォーク処理が完了し、その引数が更新された後、`n`自体が定数になったかどうかをチェックし、必要であれば定数に変換するためのものです。コメントで示されているように、`y%1 == 0`のような式が対象となります。

### `test/fixedbugs/issue5841.go` の追加

```diff
--- /dev/null
+++ b/test/fixedbugs/issue5841.go
@@ -0,0 +1,16 @@
+// build
+
+// Copyright 2013 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Issue 5841: 8g produces invalid CMPL $0, $0.
+// Similar to issue 5002, used to fail at link time.
+
+package main
+
+func main() {
+	var y int
+	if y%1 == 0 {
+	}
+}

このファイルは、Issue 5841を再現するための最小限のGoプログラムです。if y%1 == 0という条件式が含まれており、y%1は常に0となるため、この条件は常に真となります。このテストは、修正前のコンパイラでは不正なCMPL $0, $0命令を生成し、リンク時に失敗することを確認するために使用されました。修正後は、このテストが正常にビルドされ、実行されることを保証します。

コアとなるコードの解説

src/cmd/gc/walk.c の変更点

walkexpr関数は、Goコンパイラのwalkフェーズにおいて、個々の式ノードを走査し、変換を行う中心的な関数です。この関数は再帰的に呼び出され、式のサブツリーを処理します。

追加されたevconst(n);の行は、walkexprが式の処理を終え、その式のオペランド(引数)がすべてウォークされ、更新された後に実行されます。このタイミングでevconstを呼び出すことで、以下のようなシナリオに対応します。

  1. 部分的な定数評価: y%1 == 0のような式では、まずy%1がウォークされます。y%1は常に0であるため、この部分が0という定数に置き換えられます。
  2. 式の再評価: y%10に置き換えられた後、元の式は0 == 0という形になります。この時点では、0 == 0という式自体はまだ二項演算子ノードとして存在している可能性があります。
  3. 最終的な定数化: evconst(n)が呼び出されると、0 == 0という式が再評価され、その結果がtrueという単一の定数ノードに変換されます。

このプロセスにより、バックエンドには、未評価の二項演算子と定数オペランドの組み合わせではなく、完全に評価された定数値(この場合はtrue)が渡されることが保証されます。これにより、バックエンドが予期しない入力形式を処理しようとすることによる不正なコード生成が回避されます。

test/fixedbugs/issue5841.go のテストケース

このテストケースは非常にシンプルですが、Issue 5841の根本原因を効果的に捉えています。

package main

func main() {
	var y int
	if y%1 == 0 {
	}
}
  • var y int: 整数型の変数yを宣言します。yの初期値は0です。
  • if y%1 == 0: ここが問題の核心です。
    • y%1: 任意の整数y1で割った余りは常に0です。したがって、y%1は常に0という定数になります。
    • y%1 == 0: これは0 == 0となり、常にtrueという定数式になります。

修正前のコンパイラでは、このif文の条件式がバックエンドに渡される際に、0 == 0という定数式が適切に評価されず、結果として8gコンパイラが不正なCMPL $0, $0命令を生成していました。この命令はリンク時にエラーを引き起こし、ビルドが失敗していました。

このテストケースは、コンパイラの修正が正しく機能し、このような実行時に定数となる式が適切に処理され、正しい機械語コードが生成されることを検証します。

関連リンク

参考にした情報源リンク

  • Go Issue 5841のウェブ検索結果
  • Go言語のコンパイラに関する一般的な知識
  • x86アセンブリ言語のCMPL命令に関する一般的な知識