[インデックス 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 + 2
やtrue && 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
という命令は、0
と0
を比較するという意味です。これは常に真であり、通常は意味のない命令です。このような命令が生成されることは、コンパイラが何らかの論理的な誤りを犯していることを示唆しています。
技術的詳細
このコミットの技術的な核心は、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)
の追加により、walkexpr
が0 == 0
という式を処理した後、その結果が定数であるかどうかを再確認し、もし定数であればそれをtrue
という定数ノードに変換します。これにより、バックエンドには未評価の二項演算子ではなく、完全に評価された定数(この場合はtrue
)が渡されるようになり、不正なコード生成が防止されます。
この修正は、コンパイラのフロントエンド(walk
フェーズ)とバックエンド間のインターフェースをより明確にし、バックエンドがより単純で予測可能な入力を受け取ることを保証します。結果として、コンパイラの堅牢性が向上し、特定の条件下での不正なアセンブリコードの生成が回避されます。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
src/cmd/gc/walk.c
: Goコンパイラのwalk
フェーズの主要なロジックが含まれるファイルです。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
を呼び出すことで、以下のようなシナリオに対応します。
- 部分的な定数評価:
y%1 == 0
のような式では、まずy%1
がウォークされます。y%1
は常に0
であるため、この部分が0
という定数に置き換えられます。 - 式の再評価:
y%1
が0
に置き換えられた後、元の式は0 == 0
という形になります。この時点では、0 == 0
という式自体はまだ二項演算子ノードとして存在している可能性があります。 - 最終的な定数化:
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
: 任意の整数y
を1
で割った余りは常に0
です。したがって、y%1
は常に0
という定数になります。y%1 == 0
: これは0 == 0
となり、常にtrue
という定数式になります。
修正前のコンパイラでは、このif
文の条件式がバックエンドに渡される際に、0 == 0
という定数式が適切に評価されず、結果として8g
コンパイラが不正なCMPL $0, $0
命令を生成していました。この命令はリンク時にエラーを引き起こし、ビルドが失敗していました。
このテストケースは、コンパイラの修正が正しく機能し、このような実行時に定数となる式が適切に処理され、正しい機械語コードが生成されることを検証します。
関連リンク
- Go Issue 5841: https://go.dev/issue/5841
- Go CL 11107044: https://golang.org/cl/11107044
参考にした情報源リンク
- Go Issue 5841のウェブ検索結果
- Go言語のコンパイラに関する一般的な知識
- x86アセンブリ言語の
CMPL
命令に関する一般的な知識