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

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

このコミットは、Goコンパイラ(gc)における静的初期化コード生成時のバグ修正に関するものです。具体的には、スタックに割り当てられたAST(抽象構文木)ノードが誤って使用されることによって発生する問題を解決しています。この修正は、Go言語の安定性と正確性を向上させる上で重要です。

コミット

commit e1b1a5fea2c5da007b3fd883a781071928e84164
Author: Luuk van Dijk <lvd@golang.org>
Date:   Tue Dec 13 09:09:10 2011 +0100

    gc: fix use of stackallocated AST node in generation of static initialisation code.
    
    Fixes #2529
    
    R=rsc, rogpeppe
    CC=golang-dev
    https://golang.org/cl/5483048

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

https://github.com/golang/go/commit/e1b1a5fea2c5da007b3fd883a781071928e84164

元コミット内容

gc: fix use of stackallocated AST node in generation of static initialisation code.

Fixes #2529

R=rsc, rogpeppe
CC=golang-dev
https://golang.org/cl/5483048

変更の背景

このコミットは、Goコンパイラ(gc)が静的初期化コードを生成する際に発生していた潜在的なバグを修正するために行われました。Go言語では、グローバル変数やパッケージレベルの変数はプログラムの実行開始前に初期化されます。この初期化処理は「静的初期化」と呼ばれ、コンパイラによって生成される特殊なコードによって行われます。

問題は、コンパイラがAST(抽象構文木)ノードを処理する際に、一時的にスタックに割り当てられたノードを、静的初期化コードの生成という、より永続的なコンテキストで誤って参照してしまう可能性があったことです。スタックに割り当てられたデータは、そのスコープを抜けると無効になるため、このような誤った参照は未定義の動作やクラッシュを引き起こす可能性があります。

このバグは、GoのIssue #2529として報告されており、このコミットはその問題を解決することを目的としています。

前提知識の解説

Goコンパイラ (gc)

gcは、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、意味解析、中間コード生成、最適化、コード生成など、複数のフェーズが含まれます。

抽象構文木 (AST: Abstract Syntax Tree)

ASTは、ソースコードの構文構造を抽象的に表現したツリー構造のデータです。コンパイラの構文解析フェーズで生成され、その後の意味解析やコード生成フェーズで利用されます。ASTの各ノードは、変数、式、文、関数定義などのプログラム要素に対応します。

静的初期化 (Static Initialization)

静的初期化とは、プログラムの実行開始前に行われる変数やデータの初期化処理のことです。Go言語では、パッケージレベルの変数やグローバル変数がこれに該当します。これらの変数は、main関数が実行される前に、コンパイラによって生成された初期化コードによって適切な値が設定されます。

スタックとヒープ (Stack vs. Heap)

  • スタック: 関数呼び出しやローカル変数の格納に用いられるメモリ領域です。データはLIFO(後入れ先出し)の原則で管理され、関数の呼び出しと終了に伴って自動的に割り当て・解放されます。スタックに割り当てられたデータは、その関数が終了すると無効になります。
  • ヒープ: プログラムの実行中に動的にメモリを割り当てるために使用される領域です。ヒープに割り当てられたデータは、明示的に解放されるか、ガベージコレクションによって回収されるまで有効です。

コンパイラがASTノードを生成する際、一時的なノードをスタックに割り当てることがありますが、そのノードが静的初期化コードのような、より長い寿命を持つコードの一部として参照される場合、問題が発生します。

Node構造体とNodeList

Goコンパイラの内部では、ASTノードはNode構造体で表現されます。NodeListは、これらのNodeのリストを管理するためのものです。コンパイラは、これらの構造体を使ってプログラムの論理構造を構築し、最終的に実行可能なコードに変換します。

技術的詳細

このバグは、src/cmd/gc/sinit.cファイル内のstaticcopyおよびstaticassign関数に関連していました。これらの関数は、静的初期化コードを生成する際に、値のコピーや代入を処理します。

問題の核心は、これらの関数内で一時的に作成されるNode構造体(n1など)がスタック上に割り当てられていたことです。これらのスタック割り当てられたNodeは、staticassign関数に渡され、さらにその結果がoutというNodeListに追加される可能性がありました。

もしstaticassignが、スタック上のn1を直接参照するようなASTノードを生成し、それをoutリストに追加した場合、staticcopystaticassign関数が終了してスタックフレームが解放されると、n1が占めていたメモリは無効になります。しかし、outリスト内のノードは、その無効なメモリ領域を指し続けるため、後でそのノードが処理される際に、不正なメモリアクセスやクラッシュが発生する可能性がありました。

修正の目的は、スタックに割り当てられた一時的なNodeが、静的初期化コードの永続的な部分で誤って参照されないようにすることです。具体的には、staticassignの呼び出し方を変更し、staticassignが成功した場合には、その結果が直接利用されるようにし、失敗した場合(つまり、計算が必要な場合)にのみ、新しいNodeを生成してoutリストに追加するように変更されています。これにより、スタック上のn1が直接outリストに組み込まれることを防ぎ、常に有効なヒープ上のノードが参照されるようにしています。

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

変更は主にsrc/cmd/gc/sinit.cファイルに集中しています。

--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -302,18 +302,18 @@ staticcopy(Node *l, Node *r, NodeList **out)
 			tn1.type = e->expr->type;
 			if(e->expr->op == OLITERAL)
 				gdata(&n1, e->expr, n1.type->width);
-			else if(staticassign(&n1, e->expr, out)) {
-				// Done
-			} else {
-				// Requires computation, but we\'re
-				// copying someone else\'s computation.
+			else {
 				ll = nod(OXXX, N, N);
 				*ll = n1;
-				rr = nod(OXXX, N, N);
-				*rr = *orig;
-				rr->type = ll->type;
-				rr->xoffset += e->xoffset;
-				*out = list(*out, nod(OAS, ll, rr));
+				if(!staticassign(ll, e->expr, out)) {
+					// Requires computation, but we\'re
+					// copying someone else\'s computation.
+					rr = nod(OXXX, N, N);
+					*rr = *orig;
+					rr->type = ll->type;
+					rr->xoffset += e->xoffset;
+					*out = list(*out, nod(OAS, ll, rr));
+				}
 			}
 		}
 		return 1;
@@ -407,12 +407,11 @@ staticassign(Node *l, Node *r, NodeList **out)
 			tn1.type = e->expr->type;
 			if(e->expr->op == OLITERAL)
 				gdata(&n1, e->expr, n1.type->width);
-			else if(staticassign(&n1, e->expr, out)) {
-				// done
-			} else {
+			else {
 				a = nod(OXXX, N, N);
 				*a = n1;
-				*out = list(*out, nod(OAS, a, e->expr));
+				if(!staticassign(a, e->expr, out))
+					*out = list(*out, nod(OAS, a, e->expr));
 			}
 		}
 		return 1;

また、この修正を検証するための新しいテストケースが追加されています。

  • test/fixedbugs/bug382.dir/pkg.go
  • test/fixedbugs/bug382.go

コアとなるコードの解説

staticcopy関数の変更

変更前: staticcopy関数内で、e->exprがリテラルでない場合、staticassign(&n1, e->expr, out)を呼び出していました。もしstaticassignが成功した場合(trueを返した場合)、何もせず// Doneとしていました。失敗した場合(falseを返した場合)にのみ、新しいノードllrrを作成し、outリストにOAS(代入)ノードを追加していました。

変更後: else if(staticassign(&n1, e->expr, out))の条件分岐が削除され、elseブロックに統合されました。 新しいロジックでは、まずll = nod(OXXX, N, N); *ll = n1;として、スタック上のn1の内容をヒープに割り当てられた新しいノードllにコピーします。 その後、if(!staticassign(ll, e->expr, out))という条件でstaticassignを呼び出します。ここで重要なのは、staticassignに渡されるのがスタック上のn1ではなく、ヒープ上のllである点です。 もしstaticassignが失敗した場合(!staticassign(...)true)、つまり計算が必要な場合にのみ、rrノードを作成し、outリストにOASノードを追加します。

この変更により、staticassignが成功した場合でも、スタック上のn1が直接outリストに組み込まれることはなく、常にヒープ上の有効なノードが処理されるようになります。

staticassign関数の変更

変更前: staticassign関数内でも同様に、e->exprがリテラルでない場合、staticassign(&n1, e->expr, out)を呼び出していました。成功した場合は// done、失敗した場合はa = nod(OXXX, N, N); *a = n1; *out = list(*out, nod(OAS, a, e->expr));としていました。

変更後: else if(staticassign(&n1, e->expr, out))の条件分岐が削除され、elseブロックに統合されました。 新しいロジックでは、まずa = nod(OXXX, N, N); *a = n1;として、スタック上のn1の内容をヒープに割り当てられた新しいノードaにコピーします。 その後、if(!staticassign(a, e->expr, out))という条件でstaticassignを呼び出します。ここでも、スタック上のn1ではなく、ヒープ上のaが渡されます。 もしstaticassignが失敗した場合(!staticassign(...)true)、つまり計算が必要な場合にのみ、*out = list(*out, nod(OAS, a, e->expr));としてoutリストにOASノードを追加します。

これらの変更により、staticcopystaticassignの両方で、スタックに割り当てられた一時的なNodeが、静的初期化コードの永続的な部分で誤って参照されることがなくなりました。これにより、コンパイラが生成するコードの堅牢性が向上し、未定義の動作やクラッシュのリスクが低減されます。

テストケース (bug382.gopkg.go)

追加されたテストケースは、このバグを再現し、修正が正しく機能することを確認するために設計されています。

pkg.go:

package pkg
type T struct {}
var E T

これは、Tという空の構造体と、その型のパッケージレベル変数Eを定義しています。

bug382.go:

// $G $D/$F.dir/pkg.go && $G $D/$F.go || echo "Bug 382"

// Issue 2529

package main
import "./pkg"

var x = pkg.E

var fo = struct {F pkg.T}{F: x}

このテストケースでは、pkg.Eというパッケージレベルの変数をxという別のパッケージレベルの変数に代入し、さらにそのxを匿名構造体のフィールドFの初期化に使用しています。このような多段階の静的初期化が、スタック割り当てられたASTノードの誤用を引き起こす可能性があったシナリオを再現しています。このテストがコンパイルエラーや実行時エラーなしに成功することで、バグが修正されたことが確認されます。

関連リンク

  • Go Issue #2529: このコミットが修正したバグの報告。Goの公式Issueトラッカーで詳細を確認できます。
  • Go CL 5483048: このコミットに対応するGoのコードレビュー(Change List)。

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード (src/cmd/gc/)
  • Go言語のIssueトラッカー (https://github.com/golang/go/issues)
  • Go言語のコードレビューシステム (https://go.dev/cl/)