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

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

このコミットは、Goコンパイラ(cmd/gc)におけるインライン化のバグを修正するものです。具体的には、関数がインライン化される際に、その関数内のローカル変数の型チェックが適切に行われないことによって発生する問題を解決しています。この修正により、インライン化されたコードが正しくコンパイルされ、予期せぬ実行時エラーや不正な動作を防ぎます。

コミット

commit 76500b14a1b578aec2ad9b374c055ce2c047bcb5
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Nov 1 18:59:32 2012 +0100

    cmd/gc: fix inlining bug with local variables.
    
    Fixes #4323.
    
    R=rsc, lvd, golang-dev
    CC=golang-dev
    https://golang.org/cl/6815061

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

https://github.com/golang/go/commit/76500b14a1b578aec2ad9b374c055ce2c047bcb5

元コミット内容

このコミットの元の内容は、「cmd/gc: ローカル変数に関するインライン化のバグを修正する」というものです。これは、Goコンパイラのインライン化処理において、関数内のローカル変数が正しく扱われない問題が存在し、その修正を行ったことを示しています。また、「Fixes #4323」とあり、これはGoの内部バグトラッカーにおけるIssue 4323を解決したことを意味します。

変更の背景

Goコンパイラは、プログラムの実行性能を向上させるために、関数のインライン化を行います。インライン化とは、呼び出し元の関数に関数本体のコードを直接埋め込む最適化手法です。これにより、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を削減し、プログラム全体の実行速度を向上させることができます。

しかし、このコミット以前のcmd/gcでは、関数をインライン化する際に、インライン化される関数内のローカル変数の宣言が適切に型チェックされないというバグが存在しました。具体的には、インライン化されたコード内で新しく生成されるローカル変数のノード(inlvar)が、その後のコンパイルフェーズで必要となる型情報を持たないまま処理されてしまう可能性がありました。これにより、コンパイルエラーが発生したり、あるいはコンパイルは通っても実行時に予期せぬ動作を引き起こす可能性がありました。

このバグは、特にチャネル(chan)のような複雑な型を持つローカル変数が関与する場合に顕著に現れたと考えられます。issue4323.goのテストケースでは、make(chan []byte)で作成されるチャネルがローカル変数として使用されており、これがインライン化の際に問題を引き起こしていたことが示唆されます。

前提知識の解説

Goコンパイラ (cmd/gc) の役割

cmd/gcは、Go言語の公式コンパイラであり、Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスは複数のフェーズに分かれており、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成などがあります。このコミットが関連するのは、主に型チェックと最適化(インライン化)のフェーズです。

インライン化 (Inlining) とその目的

インライン化は、コンパイラ最適化の一種です。関数呼び出しを、その関数の本体のコードで置き換えることで、関数呼び出しのオーバーヘッドを削減し、プログラムの実行速度を向上させます。また、インライン化によって、呼び出し元のコンテキストでより多くの最適化(例えば、定数伝播やデッドコード削除)が可能になることもあります。

Goにおける変数のスコープとライフタイム

Goでは、変数は宣言されたブロック内で有効なスコープを持ちます。ローカル変数は、関数内で宣言され、その関数の実行が終了すると通常は破棄されます(ただし、クロージャによって参照されている場合はヒープにエスケープすることもあります)。コンパイラは、これらの変数のスコープとライフタイムを正確に管理し、メモリ割り当てや解放を適切に行う必要があります。

ONAME, ODCL, PPARAMOUT などのGoコンパイラ内部のノードタイプ

Goコンパイラは、ソースコードを抽象構文木(AST)として内部的に表現します。ASTの各ノードは、プログラムの要素(変数、関数、演算子など)を表します。

  • ONAME: 変数名や関数名などの識別子を表すノード。
  • ODCL: 変数宣言を表すノード。
  • PPARAMOUT: 関数の戻り値のパラメータを表すノード。

typecheck 関数の役割

typecheck関数は、Goコンパイラの型チェックフェーズにおいて非常に重要な役割を果たします。この関数は、ASTのノードがGoの型システム規則に準拠しているかを確認します。例えば、変数の使用がその型と一致しているか、関数の引数と戻り値の型が宣言と一致しているかなどを検証します。型チェックは、プログラムの正しさを保証し、実行時エラーを未然に防ぐために不可欠なステップです。

inlvar 関数の役割

inlvar関数は、インライン化のプロセス中に、インライン化される関数のローカル変数を、呼び出し元のコンテキストで新しい変数として表現するために使用されます。これは、インライン化によって同じ変数が複数回宣言されることを避けるため、また、インライン化されたコードが呼び出し元のスコープ内で正しく動作するようにするために必要です。inlvarは、元の変数の情報(名前、型など)を基に、新しい変数のノードを作成します。

技術的詳細

このバグは、src/cmd/gc/inl.cファイル内のmkinlcall1関数、特にインライン化された関数のローカル変数を処理する部分で発生していました。

mkinlcall1関数は、関数呼び出しをインライン化する際に、インライン化される関数のASTを操作し、呼び出し元のASTに統合します。このプロセスの中で、インライン化される関数のローカル変数は、呼び出し元のコンテキストで新しい変数として再宣言される必要があります。この再宣言のために、inlvar関数が使用され、新しい変数のノード(ll->n->inlvar)が作成されます。

問題は、inlvar関数が必ずしも関数パラメータを処理するわけではないため、生成されたll->n->inlvarノードが、その後のコンパイルフェーズで必要となる型情報が不足している可能性があった点です。特に、チャネルのような複雑な型を持つローカル変数の場合、型チェックが不十分だと、後続のコンパイルステップで問題が発生しました。

修正は、inlvarによって生成された新しい変数ノードに対して、明示的にtypecheck関数を呼び出すことで行われました。

			ll->n->inlvar = inlvar(ll->n);
			// Typecheck because inlvar is not necessarily a function parameter.
			typecheck(&ll->n->inlvar, Erv);

この変更により、インライン化されたローカル変数が、その後のコンパイルフェーズに進む前に、Goの型システム規則に照らして完全に型チェックされることが保証されます。Ervは、式が値として評価されることを示すコンテキストであり、このコンテキストで型チェックを行うことで、変数が正しく使用されることを確認します。

これにより、インライン化されたコードが、元のコードと同じように厳密な型チェックを受け、コンパイラがその変数の型と使用方法を正確に理解できるようになります。結果として、コンパイルエラーや実行時エラーの原因となる型関連の問題が解消されます。

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

src/cmd/gc/inl.c

--- a/src/cmd/gc/inl.c
+++ b/src/cmd/gc/inl.c
@@ -556,6 +556,8 @@ mkinlcall1(Node **np, Node *fn)
 	for(ll = dcl; ll; ll=ll->next)
 		if(ll->n->op == ONAME) {
 			ll->n->inlvar = inlvar(ll->n);
+			// Typecheck because inlvar is not necessarily a function parameter.
+			typecheck(&ll->n->inlvar, Erv);
 			tninit = list(ninit, nod(ODCL, ll->n->inlvar, N));  // otherwise gen won't emit the allocations for heapallocs
 			if (ll->n->class == PPARAMOUT)  // we rely on the order being correct here
 				inlretvars = list(inlretvars, ll->n->inlvar);

test/fixedbugs/issue4323.go

--- /dev/null
+++ b/test/fixedbugs/issue4323.go
@@ -0,0 +1,31 @@
+// compile
+
+// 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 4323: inlining of functions with local variables
+// forgets to typecheck the declarations in the inlined copy.
+
+package main
+
+type reader struct {
+	C chan T
+}
+
+type T struct{ C chan []byte }
+
+var r = newReader()
+
+func newReader() *reader { return new(reader) }
+
+func (r *reader) Read(n int) ([]byte, error) {
+	req := T{C: make(chan []byte)}
+	r.C <- req
+	return <-req.C, nil
+}
+
+func main() {
+	s, err := r.Read(1)
+	_, _ = s, err
+}

コアとなるコードの解説

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

変更はmkinlcall1関数内、インライン化される関数のローカル変数を処理するループの中にあります。

			ll->n->inlvar = inlvar(ll->n);
			// Typecheck because inlvar is not necessarily a function parameter.
			typecheck(&ll->n->inlvar, Erv);
  • ll->n->inlvar = inlvar(ll->n);: ここで、インライン化される元の変数ノードll->nから、インライン化後の新しい変数ノードll->n->inlvarが生成されます。inlvar関数は、元の変数の属性(名前、型など)をコピーして新しいノードを作成します。
  • // Typecheck because inlvar is not necessarily a function parameter.: このコメントは、なぜtypecheckが必要なのかを説明しています。inlvarが生成するノードは、必ずしも関数のパラメータ(これらは通常、コンパイルの早い段階で型チェックされる)ではないため、明示的な型チェックが必要になることを示唆しています。
  • typecheck(&ll->n->inlvar, Erv);: これが追加された行です。inlvarによって生成された新しい変数ノードll->n->inlvarに対して、typecheck関数が呼び出されます。Ervは、式が値として評価されるコンテキストを示し、このコンテキストで型チェックを行うことで、変数がGoの型システム規則に完全に準拠していることを保証します。これにより、インライン化されたローカル変数が、その後のコンパイルフェーズで正しく扱われるようになります。

test/fixedbugs/issue4323.go の追加

このファイルは、修正されたバグを再現し、修正が正しく機能することを確認するためのテストケースです。

  • // Issue 4323: inlining of functions with local variables // forgets to typecheck the declarations in the inlined copy.:テストの目的を明確に示しています。ローカル変数のインライン化において、インライン化されたコピーの宣言が型チェックされないというIssue 4323のバグをテストするものです。
  • type reader struct { C chan T }type T struct{ C chan []byte }:ネストされたチャネル型を定義しています。これは、Goの型システムにおいて比較的複雑な型であり、インライン化の際に型チェックが不十分だと問題が発生しやすいケースを意図していると考えられます。
  • func (r *reader) Read(n int) ([]byte, error) { req := T{C: make(chan []byte)}; r.C <- req; return <-req.C, nil }:このReadメソッドがインライン化の対象となる関数です。この関数内でreq := T{C: make(chan []byte)}というローカル変数が宣言されており、これがバグのトリガーとなっていました。make(chan []byte)でチャネルが作成され、そのチャネルがT型の構造体のフィールドとして使用されています。
  • func main() { s, err := r.Read(1); _, _ = s, err }main関数内でr.Read(1)が呼び出されています。この呼び出しがインライン化されることで、Read関数内のローカル変数reqの宣言がmain関数内に展開され、その際に型チェックの不備が露呈する、というシナリオを想定しています。

このテストケースは、コンパイルが成功することを確認することで、バグが修正されたことを検証します。もしバグが修正されていなければ、このテストはコンパイルエラーになるか、あるいは不正な動作を引き起こす可能性があります。

関連リンク

参考にした情報源リンク

  • GoのIssue 4323は、Goの公開されているIssueトラッカーや一般的な脆弱性データベースでは直接的な情報が見つかりませんでした。これは、Goの内部的なバグトラッカーのIDである可能性が高いです。そのため、このコミットの背景や技術的詳細は、主にコミットメッセージとコードの差分から推測し、Goコンパイラの一般的な動作原理とインライン化の概念に基づいて解説しました。