[インデックス 14680] ファイルの概要
このコミットは、Goコンパイラ(特に6g
、x86-64アーキテクチャ向けのコンパイラ)における、componentgen
関数がfunarg
構造体(関数引数として渡される構造体)を誤って処理していたバグを修正するものです。このバグは、複数の戻り値を返す関数が特定の状況下で誤ってコンパイルされる原因となっていました。
コミット
commit 81b46f1bcd082f255402d936f7d1e8c95389756a
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Dec 17 22:29:43 2012 +0100
cmd/6g: fix componentgen for funarg structs.
Fixes #4518.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6932045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/81b46f1bcd082f255402d936f7d1e8c95389756a
元コミット内容
cmd/6g: fix componentgen for funarg structs.
Fixes #4518.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6932045
変更の背景
このコミットは、Goコンパイラ6g
(x86-64アーキテクチャ向け)が、関数引数として渡される構造体(funarg
structs)のオフセット計算を誤っていた問題(Issue 4518)を修正するために行われました。具体的には、複数の戻り値を持つ関数が、switch
文のdefault
ケース内でreturn F(...)
のように呼び出された場合に、コンパイラが誤ったコードを生成し、期待される戻り値が得られないというバグがありました。
Go言語では、複数の戻り値を持つ関数は、実際にはそれらの戻り値を格納するための「タプル」のような構造体を暗黙的に使用することがあります。この構造体は、呼び出し規約の一部としてスタック上に配置されることが一般的です。componentgen
関数は、このような構造体の各要素(コンポーネント)へのアクセスを処理する役割を担っています。
元の実装では、funarg
として扱われる構造体が常にオフセット0から始まると仮定されていました。しかし、実際には、コンパイラの内部的な処理や呼び出し規約によっては、funarg
構造体がスタック上のオフセット0以外の場所から始まることがあり、この仮定が誤ったオフセット計算を引き起こし、結果として誤った値が読み書きされる原因となっていました。
前提知識の解説
Goコンパイラと6g
Go言語のコンパイラは、ソースコードを機械語に変換するツールです。Goのコンパイラは、ターゲットアーキテクチャごとに異なるバックエンドを持っています。6g
は、Go 1.0時代のx86-64アーキテクチャ(64ビットIntel/AMDプロセッサ)向けのコンパイラバックエンドを指します。現在のGoコンパイラは、より統合されたツールチェーンの一部として提供されていますが、当時の開発では6g
のような特定のアーキテクチャ名を冠したコンパイラが使われていました。
cgen.c
とcomponentgen
src/cmd/6g/cgen.c
は、Goコンパイラの6g
バックエンドにおけるコード生成(code generation)を担当するC言語のソースファイルです。このファイルには、Goの抽象構文木(AST)をターゲットアーキテクチャの命令に変換するためのロジックが含まれています。
componentgen
関数は、複合型(構造体や配列など)の個々の要素(コンポーネント)へのアクセスを処理するために使用されます。例えば、s.field
のようなアクセスや、複数の戻り値を持つ関数の戻り値へのアクセスなどです。この関数は、コンポーネントのメモリ上のオフセットを正確に計算し、それに対応する機械語命令を生成する責任があります。
funarg
構造体
Go言語では、関数が複数の戻り値を返す場合、コンパイラはこれらの戻り値をまとめて扱うために、内部的に「関数引数構造体(funarg
struct)」のようなものを生成することがあります。これは、関数呼び出し規約の一部として、戻り値をスタックやレジスタを通じて効率的に渡すためのメカニズムです。funarg
構造体は、通常の構造体とは異なり、コンパイラによって特殊な方法で扱われることがあります。特に、スタックフレーム内での配置オフセットが、通常の変数とは異なる場合があります。
オフセット計算
メモリ上のデータにアクセスするためには、そのデータの開始アドレスからの相対的な位置、すなわち「オフセット」を正確に知る必要があります。コンパイラは、変数、構造体のフィールド、配列の要素など、プログラム内のあらゆるデータについて、そのオフセットを計算します。この計算が誤っていると、プログラムは間違ったメモリ位置にアクセスし、バグやクラッシュの原因となります。
Issue 4518
Go言語のIssueトラッカーで報告されたバグです。このコミットの目的は、この特定のバグを修正することでした。Issue 4518の詳細は、通常、Goの公式Issueトラッカーで確認できます。この問題は、return F(...)
のような形式で複数の戻り値を持つ関数を呼び出す際に、6g
コンパイラが誤ったコードを生成するというものでした。
技術的詳細
このバグの根本原因は、componentgen
関数がfunarg
構造体のオフセットを計算する際に、その構造体が常にスタック上のオフセット0から始まると誤って仮定していた点にあります。しかし、Goコンパイラの内部的な呼び出し規約やスタックフレームのレイアウトによっては、funarg
構造体がスタック上の他の場所(例えば、関数の他の引数やローカル変数の後に続く形)に配置されることがあり、その場合、オフセット0から始まるとの仮定は成り立ちません。
具体的には、componentgen
関数内で、左辺(nl
)と右辺(nr
)のノードがTSTRUCT
型であり、かつfunarg
フラグが設定されている場合に、その構造体の実際の開始オフセットを考慮に入れる必要がありました。
修正は、loffset
(左辺のオフセット)とroffset
(右辺のオフセット)の計算に、funarg
構造体の実際の開始オフセットを反映させることで行われました。
// funarg structs may not begin at offset zero.
if(nl->type->etype == TSTRUCT && nl->type->funarg && nl->type->type)
loffset -= nl->type->type->width;
if(nr != N && nr->type->etype == TSTRUCT && nr->type->funarg && nr->type->type)
roffset -= nr->type->type->width;
このコードは、nl
またはnr
がfunarg
構造体である場合、その構造体の最初の要素のオフセットが、構造体全体の幅(nl->type->type->width
)だけずれている可能性があることを考慮しています。これは、funarg
構造体がスタック上で、その前のデータによって占められた領域の直後から始まる場合に、その「前のデータ」の幅を考慮してオフセットを調整する必要があるためです。
この調整により、componentgen
はfunarg
構造体の各コンポーネントへのアクセスを正確なメモリ位置にマッピングできるようになり、複数の戻り値を持つ関数のコンパイルが正しく行われるようになりました。
コアとなるコードの変更箇所
変更は主にsrc/cmd/6g/cgen.c
ファイル内のcomponentgen
関数に集中しています。
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -1632,6 +1632,12 @@ componentgen(Node *nr, Node *nl)
case TSTRUCT:
loffset = nodl.xoffset;
roffset = nodr.xoffset;
+ // funarg structs may not begin at offset zero.
+ if(nl->type->etype == TSTRUCT && nl->type->funarg && nl->type->type)
+ loffset -= nl->type->type->type->width;
+ if(nr != N && nr->type->etype == TSTRUCT && nr->type->funarg && nr->type->type)
+ roffset -= nr->type->type->width;
+
for(t=nl->type->type; t; t=t->down) {
nodl.xoffset = loffset + t->width;
nodl.type = t->type;
また、この修正を検証するための新しいテストケースがtest/fixedbugs/issue4518.go
として追加されています。
--- /dev/null
+++ b/test/fixedbugs/issue4518.go
@@ -0,0 +1,67 @@
+// 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.
+
+// Issue 4518. In some circumstances "return F(...)"
+// where F has multiple returns is miscompiled by 6g due to
+// bold assumptions in componentgen.
+
+package main
+
+func DontInline() {}
+
+func F(e interface{}) (int, int) {
+ DontInline()
+ return 3, 7
+}
+
+func G() (int, int) {
+ DontInline()
+ return 3, 7
+}
+
+func bogus1(d interface{}) (int, int) {
+ switch {
+ default:
+ return F(d)
+ }
+ return 0, 0
+}
+
+func bogus2() (int, int) {
+ switch {
+ default:
+ return F(3)
+ }
+ return 0, 0
+}
+
+func bogus3(d interface{}) (int, int) {
+ switch {
+ default:
+ return G()
+ }
+ return 0, 0
+}
+
+func bogus4() (int, int) {
+ switch {
+ default:
+ return G()
+ }
+ return 0, 0
+}
+
+func check(a, b int) {
+ if a != 3 || b != 7 {
+ println(a, b)
+ panic("a != 3 || b != 7")
+ }
+}
+
+func main() {
+ check(bogus1(42))
+ check(bogus2())
+}
コアとなるコードの解説
src/cmd/6g/cgen.c
の変更点では、componentgen
関数内でTSTRUCT
型を処理するcase
ブロックに新しい条件分岐が追加されています。
if(nl->type->etype == TSTRUCT && nl->type->funarg && nl->type->type)
loffset -= nl->type->type->width;
if(nr != N && nr->type->etype == TSTRUCT && nr->type->funarg && nr->type->type)
roffset -= nr->type->type->width;
nl->type->etype == TSTRUCT
: 現在処理しているノードの型が構造体であることを確認します。nl->type->funarg
: その構造体がfunarg
(関数引数として扱われる特殊な構造体)であることを確認します。nl->type->type
:funarg
構造体が実際に内部に型情報を持っていることを確認します。これは、構造体が空でないことを意味します。
これらの条件がすべて真である場合、loffset
(左辺のオフセット)またはroffset
(右辺のオフセット)からnl->type->type->width
(funarg
構造体の最初の要素の幅、または構造体全体の幅)を減算しています。この減算は、funarg
構造体がスタック上のオフセット0から始まらない場合に、その実際の開始位置を考慮してオフセットを調整するためのものです。これにより、componentgen
が構造体内の各フィールドにアクセスする際のオフセット計算が正確になります。
test/fixedbugs/issue4518.go
のテストケースは、このバグがどのような状況で発生したかを具体的に示しています。
F
とG
は両方ともint, int
の複数の戻り値を返す関数です。
bogus1
、bogus2
、bogus3
、bogus4
は、switch
文のdefault
ケース内でこれらの関数を呼び出し、その戻り値を直接return
しています。
特にbogus1
とbogus2
は、F
関数がインターフェース型の引数を持つため、コンパイラがより複雑なコードを生成する可能性があり、このバグが顕在化しやすかったと考えられます。
check
関数は、戻り値が期待通り(3と7)であるかを検証し、そうでなければパニックを起こします。
main
関数でbogus1
とbogus2
を呼び出し、check
で検証することで、バグが修正されたことを確認しています。
関連リンク
- Go Issue 4518: https://github.com/golang/go/issues/4518 (このコミットが修正した具体的なバグ報告)
- Go CL 6932045: https://golang.org/cl/6932045 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
参考にした情報源リンク
- Go Issue 4518のGitHubページ
- Go CL 6932045のGo Code Reviewページ
- Go言語のコンパイラに関する一般的な知識(Goのソースコード、コンパイラ設計に関するドキュメントなど)
- C言語のポインタとオフセット計算に関する一般的な知識
- Go言語の関数呼び出し規約に関する情報(Goの内部実装に関する記事やドキュメント)
- Go言語のテストフレームワークに関する情報
- Go言語の
switch
文とreturn
文の動作に関する情報 - Go言語の
interface{}
の内部表現に関する情報 - Go言語の
panic
とprintln
に関する情報 - Go言語の
DontInline()
関数の目的(コンパイラのインライン化を抑制するための慣用的な関数