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

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

このコミットは、Goコンパイラ(cmd/gc)における特定のバグ、具体的にはLEAQ $0, SIというアセンブリ命令に関連する問題を修正するものです。このバグは、定数の実効アドレスを取得しようとすることで発生し、コンパイラが誤ったコードを生成する原因となっていました。修正は、コンパイラがノープ(no-op)変換を適切に処理し、定数の実効アドレスを誤って計算しないようにすることで行われました。

コミット

commit 2a9410c19c681f663aee1606289110881c62d640
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 7 11:59:18 2012 -0400

    cmd/gc: fix LEAQ $0, SI bug
    
    Cannot take effective address of constant.
    
    Fixes #3670.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6299055

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

https://github.com/golang/go/commit/2a9410c19c681f663aee1606289110881c62d640

元コミット内容

cmd/gc: fix LEAQ $0, SI bug Cannot take effective address of constant. Fixes #3670.

変更の背景

このコミットは、Goコンパイラ(cmd/gc)が特定の状況下で誤ったアセンブリコードを生成するバグを修正するために行われました。具体的には、コンパイラが定数(nilなど)に対してLEAQ(Load Effective Address)命令を誤って適用しようとすることが問題でした。LEAQ命令は通常、メモリ上のアドレスを計算するために使用されますが、定数にはメモリ上のアドレスという概念がないため、この操作は無効であり、コンパイラがクラッシュしたり、不正なコードを生成したりする可能性がありました。

この問題は、Goのreflectパッケージを使用してnilの型情報を取得しようとした際に顕在化しました。特に、reflect.TypeOf(T(nil))のように、インターフェース型へのノープ(no-op)変換を介してnilが渡された場合に、コンパイラがnilを定数として認識せず、その実効アドレスを取得しようとしていました。この挙動は、GoのIssue #3670として報告され、このコミットによって修正されました。

前提知識の解説

Goコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスでは、ソースコードの解析、抽象構文木(AST)の構築、型チェック、中間表現(IR)への変換、最適化、そして最終的な機械語コードの生成が行われます。このコミットで修正された問題は、このコード生成フェーズにおけるバグでした。

LEAQ (Load Effective Address) 命令

LEAQは、x86-64アーキテクチャにおけるアセンブリ命令の一つです。この命令は、オペランドで指定されたメモリ参照のアドレスを計算し、その結果をレジスタにロードします。例えば、LEAQ (%rax, %rbx, 4), %rcxは、%rax + %rbx * 4のアドレスを計算し、その値を%rcxに格納します。 重要なのは、LEAQがメモリの内容を読み込むのではなく、アドレス自体を計算するという点です。この特性から、アドレス計算だけでなく、レジスタ間の算術演算(特に乗算や加算)にも利用されることがあります。 しかし、この命令はメモリ上の「アドレス」を扱うため、$0のような直接的な定数(メモリ上の場所を持たない)に対してLEAQを適用しようとすると、意味をなさず、コンパイラが混乱する原因となります。

OCONVNOP (No-op Conversion)

Goコンパイラの内部では、ソースコードが中間表現(IR)に変換されます。OCONVNOPは、この中間表現における「ノープ(no-op)変換」を表すオペレーションコードです。ノープ変換とは、値の型は変わるものの、その値自体は変化しないような変換を指します。例えば、interface{}(nil)のように、具体的な型を持たないnilをインターフェース型に変換するような場合がこれに該当します。 このコミットのバグは、コンパイラがOCONVNOPを処理する際に、その変換の元となる値が定数であることを見落とし、誤ってその実効アドレスを計算しようとしたことに起因します。

reflect.TypeOf(nil)reflect.TypeOf(T(nil))

Goのreflectパッケージは、実行時に型情報を検査するための機能を提供します。

  • reflect.TypeOf(nil): これはnilの型情報を取得しようとしますが、nilは特定の型を持たないため、通常はnilTypeを返します。
  • reflect.TypeOf(T(nil)) (ここでTはインターフェース型): この場合、nilはまずインターフェース型Tに変換されます。インターフェース値は、内部的に「型」と「値」のペアとして表現されます。nilがインターフェース型に変換されると、そのインターフェース値は「型がnilで、値もnil」という状態になります。この変換自体はノープ変換の一種です。 このコミットで修正されたバグは、特にT(nil)のようなノープ変換が絡む場合に、コンパイラがnilを定数として正しく扱えず、LEAQ命令を生成してしまうというものでした。

技術的詳細

このバグは、Goコンパイラのコード生成フェーズ、特にsrc/cmd/gc/gen.c内のcgen_as関数で発生していました。cgen_as関数は、アセンブリコードを生成する際に、ノード(GoのASTやIRの要素)を処理する役割を担っています。

問題の核心は、コンパイラがOCONVNOP(ノープ変換)ノードを処理する際に、その変換の元となるノードが定数であるかどうかを適切にチェックしていなかった点にあります。reflect.TypeOf(T(nil))のようなコードが与えられた場合、コンパイラはまずnilをインターフェース型Tに変換するOCONVNOPノードを生成します。その後、このノードを処理する際に、nilが定数であるにもかかわらず、その実効アドレスを取得しようとするLEAQ $0, SIのようなアセンブリ命令を生成してしまいました。これは、LEAQ命令がメモリ上のアドレスを扱うものであり、メモリ上のアドレスを持たない定数には適用できないため、不正なコードとなります。

修正は、cgen_as関数に以下のwhileループを追加することで行われました。

while(nr != N && nr->op == OCONVNOP)
    nr = nr->left;

このループは、nr(右側のノード、つまり変換対象のノード)がOCONVNOPである限り、そのleft(左側のノード、つまり変換元のノード)を辿っていくようにします。これにより、OCONVNOPの連鎖をスキップし、最終的に変換元の実際のノード(この場合はnilを表す定数ノード)に到達することができます。コンパイラがこの定数ノードに直接アクセスできるようになることで、LEAQ命令を誤って生成するのを防ぎ、定数に対する適切なコード生成パスを選択できるようになります。

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

src/cmd/gc/gen.c

--- a/src/cmd/gc/gen.c
+++ b/src/cmd/gc/gen.c
@@ -647,6 +647,9 @@ cgen_as(Node *nl, Node *nr)
 		dump("cgen_as = ", nr);
 	}
 
+	while(nr != N && nr->op == OCONVNOP)
+		nr = nr->left;
+
 	if(nl == N || isblank(nl)) {
 		cgen_discard(nr);
 		return;

test/fixedbugs/bug444.go

--- /dev/null
+++ b/test/fixedbugs/bug444.go
@@ -0,0 +1,19 @@
+// run
+
+// Copyright 2012 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.\n
+
+// The no-op conversion here used to confuse the compiler
+// into doing a load-effective-address of nil.
+
+package main
+
+import "reflect"
+
+type T interface {}
+
+func main() {
+        reflect.TypeOf(nil)
+        reflect.TypeOf(T(nil)) // used to fail
+}

コアとなるコードの解説

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

追加されたwhileループは、cgen_as関数がノードを処理する前に、nr(右側のノード、通常は代入の右辺や関数の引数など)がOCONVNOP(ノープ変換)である場合に、そのノープ変換を「剥がして」変換元のノードに到達するようにします。

  • while(nr != N && nr->op == OCONVNOP): nrNULLではなく、かつその操作がOCONVNOPである限りループを続けます。
  • nr = nr->left;: OCONVNOPノードのleftフィールドは、その変換元のノードを指します。この行によって、nrは変換元のノードに更新されます。

この変更により、コンパイラはreflect.TypeOf(T(nil))のようなケースで、T(nil)というノープ変換の背後にある真の定数nilを正しく認識できるようになります。その結果、nilに対してLEAQのような不適切なアセンブリ命令を生成するのを防ぎ、正しいコードパスに進むことができるようになります。

test/fixedbugs/bug444.go の追加

このテストファイルは、修正されたバグが再発しないことを確認するために追加されました。

  • // run: このコメントは、このファイルがGoのテストスイートの一部として実行されることを示します。
  • type T interface {}: 空のインターフェース型Tを定義しています。
  • func main() { ... }:
    • reflect.TypeOf(nil): これは、nilの型情報を取得する通常のケースです。この行自体はバグを引き起こしませんが、比較のために含まれています。
    • reflect.TypeOf(T(nil)) // used to fail: この行が、以前はコンパイラを混乱させ、LEAQ $0, SIバグを引き起こしていたコードです。nilがインターフェース型Tにノープ変換されることで、コンパイラがnilを定数として正しく扱えず、その実効アドレスを取得しようとしていました。このテストが正常に実行されることで、バグが修正されたことが確認されます。

このテストは、特定のコーナーケースを捉え、コンパイラがノープ変換と定数を組み合わせたシナリオを正しく処理できるようになったことを保証します。

関連リンク

参考にした情報源リンク

このコミットは、Goコンパイラ(cmd/gc)における特定のバグ、具体的にはLEAQ $0, SIというアセンブリ命令に関連する問題を修正するものです。このバグは、定数の実効アドレスを取得しようとすることで発生し、コンパイラが誤ったコードを生成する原因となっていました。修正は、コンパイラがノープ(no-op)変換を適切に処理し、定数の実効アドレスを誤って計算しないようにすることで行われました。

コミット

commit 2a9410c19c681f663aee1606289110881c62d640
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 7 11:59:18 2012 -0400

    cmd/gc: fix LEAQ $0, SI bug
    
    Cannot take effective address of constant.
    
    Fixes #3670.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6299055

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

https://github.com/golang/go/commit/2a9410c19c681f663aee1606289110881c62d640

元コミット内容

cmd/gc: fix LEAQ $0, SI bug Cannot take effective address of constant. Fixes #3670.

変更の背景

このコミットは、Goコンパイラ(cmd/gc)が特定の状況下で誤ったアセンブリコードを生成するバグを修正するために行われました。具体的には、コンパイラが定数(nilなど)に対してLEAQ(Load Effective Address)命令を誤って適用しようとすることが問題でした。LEAQ命令は通常、メモリ上のアドレスを計算するために使用されますが、定数にはメモリ上のアドレスという概念がないため、この操作は無効であり、コンパイラがクラッシュしたり、不正なコードを生成したりする可能性がありました。

この問題は、Goのreflectパッケージを使用してnilの型情報を取得しようとした際に顕在化しました。特に、reflect.TypeOf(T(nil))のように、インターフェース型へのノープ(no-op)変換を介してnilが渡された場合に、コンパイラがnilを定数として認識せず、その実効アドレスを取得しようとしていました。この挙動は、GoのIssue #3670として報告され、このコミットによって修正されました。

前提知識の解説

Goコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスでは、ソースコードの解析、抽象構文木(AST)の構築、型チェック、中間表現(IR)への変換、最適化、そして最終的な機械語コードの生成が行われます。このコミットで修正された問題は、このコード生成フェーズにおけるバグでした。

LEAQ (Load Effective Address) 命令

LEAQは、x86-64アーキテクチャにおけるアセンブリ命令の一つです。この命令は、オペランドで指定されたメモリ参照のアドレスを計算し、その結果をレジスタにロードします。例えば、LEAQ (%rax, %rbx, 4), %rcxは、%rax + %rbx * 4のアドレスを計算し、その値を%rcxに格納します。 重要なのは、LEAQがメモリの内容を読み込むのではなく、アドレス自体を計算するという点です。この特性から、アドレス計算だけでなく、レジスタ間の算術演算(特に乗算や加算)にも利用されることがあります。 しかし、この命令はメモリ上の「アドレス」を扱うため、$0のような直接的な定数(メモリ上の場所を持たない)に対してLEAQを適用しようとすると、意味をなさず、コンパイラが混乱する原因となります。

OCONVNOP (No-op Conversion)

Goコンパイラの内部では、ソースコードが中間表現(IR)に変換されます。OCONVNOPは、この中間表現における「ノープ(no-op)変換」を表すオペレーションコードです。ノープ変換とは、値の型は変わるものの、その値自体は変化しないような変換を指します。例えば、interface{}(nil)のように、具体的な型を持たないnilをインターフェース型に変換するような場合がこれに該当します。 このコミットのバグは、コンパイラがOCONVNOPを処理する際に、その変換の元となる値が定数であることを見落とし、誤ってその実効アドレスを計算しようとしたことに起因します。

reflect.TypeOf(nil)reflect.TypeOf(T(nil))

Goのreflectパッケージは、実行時に型情報を検査するための機能を提供します。

  • reflect.TypeOf(nil): これはnilの型情報を取得しようとしますが、nilは特定の型を持たないため、通常はnilTypeを返します。
  • reflect.TypeOf(T(nil)) (ここでTはインターフェース型): この場合、nilはまずインターフェース型Tに変換されます。インターフェース値は、内部的に「型」と「値」のペアとして表現されます。nilがインターフェース型に変換されると、そのインターフェース値は「型がnilで、値もnil」という状態になります。この変換自体はノープ変換の一種です。 このコミットで修正されたバグは、特にT(nil)のようなノープ変換が絡む場合に、コンパイラがnilを定数として正しく扱えず、LEAQ命令を生成してしまうというものでした。

技術的詳細

このバグは、Goコンパイラのコード生成フェーズ、特にsrc/cmd/gc/gen.c内のcgen_as関数で発生していました。cgen_as関数は、アセンブリコードを生成する際に、ノード(GoのASTやIRの要素)を処理する役割を担っています。

問題の核心は、コンパイラがOCONVNOP(ノープ変換)ノードを処理する際に、その変換の元となるノードが定数であるかどうかを適切にチェックしていなかった点にあります。reflect.TypeOf(T(nil))のようなコードが与えられた場合、コンパイラはまずnilをインターフェース型Tに変換するOCONVNOPノードを生成します。その後、このノードを処理する際に、nilが定数であるにもかかわらず、その実効アドレスを取得しようとするLEAQ $0, SIのようなアセンブリ命令を生成してしまいました。これは、LEAQ命令がメモリ上のアドレスを扱うものであり、メモリ上のアドレスを持たない定数には適用できないため、不正なコードとなります。

修正は、cgen_as関数に以下のwhileループを追加することで行われました。

while(nr != N && nr->op == OCONVNOP)
    nr = nr->left;

このループは、nr(右側のノード、つまり変換対象のノード)がOCONVNOPである限り、そのleft(左側のノード、つまり変換元のノード)を辿っていくようにします。これにより、OCONVNOPの連鎖をスキップし、最終的に変換元の実際のノード(この場合はnilを表す定数ノード)に到達することができます。コンパイラがこの定数ノードに直接アクセスできるようになることで、LEAQ命令を誤って生成するのを防ぎ、定数に対する適切なコード生成パスを選択できるようになります。

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

src/cmd/gc/gen.c

--- a/src/cmd/gc/gen.c
+++ b/src/cmd/gc/gen.c
@@ -647,6 +647,9 @@ cgen_as(Node *nl, Node *nr)
 		dump("cgen_as = ", nr);
 	}
 
+	while(nr != N && nr->op == OCONVNOP)
+		nr = nr->left;
+
 	if(nl == N || isblank(nl)) {
 		cgen_discard(nr);
 		return;

test/fixedbugs/bug444.go

--- /dev/null
+++ b/test/fixedbugs/bug444.go
@@ -0,0 +1,19 @@
+// run
+
+// Copyright 2012 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.\n
+
+// The no-op conversion here used to confuse the compiler
+// into doing a load-effective-address of nil.
+
+package main
+
+import "reflect"
+
+type T interface {}
+
+func main() {
+        reflect.TypeOf(nil)
+        reflect.TypeOf(T(nil)) // used to fail
+}

コアとなるコードの解説

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

追加されたwhileループは、cgen_as関数がノードを処理する前に、nr(右側のノード、通常は代入の右辺や関数の引数など)がOCONVNOP(ノープ変換)である場合に、そのノープ変換を「剥がして」変換元のノードに到達するようにします。

  • while(nr != N && nr->op == OCONVNOP): nrNULLではなく、かつその操作がOCONVNOPである限りループを続けます。
  • nr = nr->left;: OCONVNOPノードのleftフィールドは、その変換元のノードを指します。この行によって、nrは変換元のノードに更新されます。

この変更により、コンパイラはreflect.TypeOf(T(nil))のようなケースで、T(nil)というノープ変換の背後にある真の定数nilを正しく認識できるようになります。その結果、nilに対してLEAQのような不適切なアセンブリ命令を生成するのを防ぎ、正しいコードパスに進むことができるようになります。

test/fixedbugs/bug444.go の追加

このテストファイルは、修正されたバグが再発しないことを確認するために追加されました。

  • // run: このコメントは、このファイルがGoのテストスイートの一部として実行されることを示します。
  • type T interface {}: 空のインターフェース型Tを定義しています。
  • func main() { ... }:
    • reflect.TypeOf(nil): これは、nilの型情報を取得する通常のケースです。この行自体はバグを引き起こしませんが、比較のために含まれています。
    • reflect.TypeOf(T(nil)) // used to fail: この行が、以前はコンパイラを混乱させ、LEAQ $0, SIバグを引き起こしていたコードです。nilがインターフェース型Tにノープ変換されることで、コンパイラがnilを定数として正しく扱えず、その実効アドレスを取得しようとしていました。このテストが正常に実行されることで、バグが修正されたことが確認されます。

このテストは、特定のコーナーケースを捉え、コンパイラがノープ変換と定数を組み合わせたシナリオを正しく処理できるようになったことを保証します。

関連リンク

参考にした情報源リンク