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

[インデックス 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.ccomponentgen

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またはnrfunarg構造体である場合、その構造体の最初の要素のオフセットが、構造体全体の幅(nl->type->type->width)だけずれている可能性があることを考慮しています。これは、funarg構造体がスタック上で、その前のデータによって占められた領域の直後から始まる場合に、その「前のデータ」の幅を考慮してオフセットを調整する必要があるためです。

この調整により、componentgenfunarg構造体の各コンポーネントへのアクセスを正確なメモリ位置にマッピングできるようになり、複数の戻り値を持つ関数のコンパイルが正しく行われるようになりました。

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

変更は主に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->widthfunarg構造体の最初の要素の幅、または構造体全体の幅)を減算しています。この減算は、funarg構造体がスタック上のオフセット0から始まらない場合に、その実際の開始位置を考慮してオフセットを調整するためのものです。これにより、componentgenが構造体内の各フィールドにアクセスする際のオフセット計算が正確になります。

test/fixedbugs/issue4518.goのテストケースは、このバグがどのような状況で発生したかを具体的に示しています。 FGは両方ともint, intの複数の戻り値を返す関数です。 bogus1bogus2bogus3bogus4は、switch文のdefaultケース内でこれらの関数を呼び出し、その戻り値を直接returnしています。 特にbogus1bogus2は、F関数がインターフェース型の引数を持つため、コンパイラがより複雑なコードを生成する可能性があり、このバグが顕在化しやすかったと考えられます。 check関数は、戻り値が期待通り(3と7)であるかを検証し、そうでなければパニックを起こします。 main関数でbogus1bogus2を呼び出し、checkで検証することで、バグが修正されたことを確認しています。

関連リンク

参考にした情報源リンク

  • Go Issue 4518のGitHubページ
  • Go CL 6932045のGo Code Reviewページ
  • Go言語のコンパイラに関する一般的な知識(Goのソースコード、コンパイラ設計に関するドキュメントなど)
  • C言語のポインタとオフセット計算に関する一般的な知識
  • Go言語の関数呼び出し規約に関する情報(Goの内部実装に関する記事やドキュメント)
  • Go言語のテストフレームワークに関する情報
  • Go言語のswitch文とreturn文の動作に関する情報
  • Go言語のinterface{}の内部表現に関する情報
  • Go言語のpanicprintlnに関する情報
  • Go言語のDontInline()関数の目的(コンパイラのインライン化を抑制するための慣用的な関数